From 2ffe7abac64f27bb02f9079212f8c0a412dfa05f Mon Sep 17 00:00:00 2001 From: logonkhi Date: Fri, 13 Jun 2025 17:10:58 +0900 Subject: [PATCH] =?UTF-8?q?Modal=20=EA=B0=9C=EB=B0=9C=20=EC=99=84=EB=A3=8C?= =?UTF-8?q?.=20Toolbar=20=EA=B0=9C=EB=B0=9C=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Resources/Prefabs/UI/Modal/Confirm.prefab | 1257 +++++++++++++++++ .../Prefabs/UI/Modal/Confirm.prefab.meta | 7 + Assets/Scenes/Main.unity | 278 +--- Assets/Scripts/SampleProject/AppMain.cs | 32 +- .../Scripts/UVC/Locale/LocalizationManager.cs | 2 +- Assets/Scripts/UVC/UI/Modal/Alert.cs | 121 +- Assets/Scripts/UVC/UI/Modal/Confirm.cs | 188 +++ Assets/Scripts/UVC/UI/Modal/Confirm.cs.meta | 2 + Assets/Scripts/UVC/UI/Modal/Modal.cs | 454 ++++-- Assets/Scripts/UVC/UI/Modal/ModalContent.cs | 139 +- Assets/Scripts/UVC/UI/Modal/ModalView.cs | 257 +++- Assets/Scripts/UVC/UI/ToolBar.meta | 8 + Assets/Scripts/UVC/UI/ToolBar/IToolbarItem.cs | 7 + .../UVC/UI/ToolBar/IToolbarItem.cs.meta | 2 + Assets/Scripts/UVC/UI/ToolBar/Toolbar.cs | 77 + Assets/Scripts/UVC/UI/ToolBar/Toolbar.cs.meta | 2 + .../UVC/UI/ToolBar/ToolbarButtonBase.cs | 82 ++ .../UVC/UI/ToolBar/ToolbarButtonBase.cs.meta | 2 + .../UVC/UI/ToolBar/ToolbarExpandableButton.cs | 60 + .../ToolBar/ToolbarExpandableButton.cs.meta | 2 + .../Scripts/UVC/UI/ToolBar/ToolbarManager.cs | 100 ++ .../UVC/UI/ToolBar/ToolbarManager.cs.meta | 2 + .../UVC/UI/ToolBar/ToolbarRadioButton.cs | 32 + .../UVC/UI/ToolBar/ToolbarRadioButton.cs.meta | 2 + .../UVC/UI/ToolBar/ToolbarRadioButtonGroup.cs | 43 + .../ToolBar/ToolbarRadioButtonGroup.cs.meta | 2 + .../UVC/UI/ToolBar/ToolbarSeparator.cs | 7 + .../UVC/UI/ToolBar/ToolbarSeparator.cs.meta | 2 + .../UVC/UI/ToolBar/ToolbarStandardButton.cs | 16 + .../UI/ToolBar/ToolbarStandardButton.cs.meta | 2 + .../UVC/UI/ToolBar/ToolbarToggleButton.cs | 38 + .../UI/ToolBar/ToolbarToggleButton.cs.meta | 2 + Assets/Scripts/UVC/UI/ToolBar/ToolbarView.cs | 453 ++++++ .../UVC/UI/ToolBar/ToolbarView.cs.meta | 2 + .../Scripts/UVC/UI/ToolBar/TooltipHandler.cs | 26 + .../UVC/UI/ToolBar/TooltipHandler.cs.meta | 2 + Assets/StreamingAssets/locale.json | 34 +- 37 files changed, 3278 insertions(+), 466 deletions(-) create mode 100644 Assets/Resources/Prefabs/UI/Modal/Confirm.prefab create mode 100644 Assets/Resources/Prefabs/UI/Modal/Confirm.prefab.meta create mode 100644 Assets/Scripts/UVC/UI/Modal/Confirm.cs create mode 100644 Assets/Scripts/UVC/UI/Modal/Confirm.cs.meta create mode 100644 Assets/Scripts/UVC/UI/ToolBar.meta create mode 100644 Assets/Scripts/UVC/UI/ToolBar/IToolbarItem.cs create mode 100644 Assets/Scripts/UVC/UI/ToolBar/IToolbarItem.cs.meta create mode 100644 Assets/Scripts/UVC/UI/ToolBar/Toolbar.cs create mode 100644 Assets/Scripts/UVC/UI/ToolBar/Toolbar.cs.meta create mode 100644 Assets/Scripts/UVC/UI/ToolBar/ToolbarButtonBase.cs create mode 100644 Assets/Scripts/UVC/UI/ToolBar/ToolbarButtonBase.cs.meta create mode 100644 Assets/Scripts/UVC/UI/ToolBar/ToolbarExpandableButton.cs create mode 100644 Assets/Scripts/UVC/UI/ToolBar/ToolbarExpandableButton.cs.meta create mode 100644 Assets/Scripts/UVC/UI/ToolBar/ToolbarManager.cs create mode 100644 Assets/Scripts/UVC/UI/ToolBar/ToolbarManager.cs.meta create mode 100644 Assets/Scripts/UVC/UI/ToolBar/ToolbarRadioButton.cs create mode 100644 Assets/Scripts/UVC/UI/ToolBar/ToolbarRadioButton.cs.meta create mode 100644 Assets/Scripts/UVC/UI/ToolBar/ToolbarRadioButtonGroup.cs create mode 100644 Assets/Scripts/UVC/UI/ToolBar/ToolbarRadioButtonGroup.cs.meta create mode 100644 Assets/Scripts/UVC/UI/ToolBar/ToolbarSeparator.cs create mode 100644 Assets/Scripts/UVC/UI/ToolBar/ToolbarSeparator.cs.meta create mode 100644 Assets/Scripts/UVC/UI/ToolBar/ToolbarStandardButton.cs create mode 100644 Assets/Scripts/UVC/UI/ToolBar/ToolbarStandardButton.cs.meta create mode 100644 Assets/Scripts/UVC/UI/ToolBar/ToolbarToggleButton.cs create mode 100644 Assets/Scripts/UVC/UI/ToolBar/ToolbarToggleButton.cs.meta create mode 100644 Assets/Scripts/UVC/UI/ToolBar/ToolbarView.cs create mode 100644 Assets/Scripts/UVC/UI/ToolBar/ToolbarView.cs.meta create mode 100644 Assets/Scripts/UVC/UI/ToolBar/TooltipHandler.cs create mode 100644 Assets/Scripts/UVC/UI/ToolBar/TooltipHandler.cs.meta diff --git a/Assets/Resources/Prefabs/UI/Modal/Confirm.prefab b/Assets/Resources/Prefabs/UI/Modal/Confirm.prefab new file mode 100644 index 00000000..c54535cc --- /dev/null +++ b/Assets/Resources/Prefabs/UI/Modal/Confirm.prefab @@ -0,0 +1,1257 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &157760054528398823 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1863887949234781852} + - component: {fileID: 7654955096077171671} + - component: {fileID: 6842567254505609782} + - component: {fileID: 2945644090061992249} + m_Layer: 5 + m_Name: cancelButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1863887949234781852 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 157760054528398823} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 7472250681771032410} + m_Father: {fileID: 8878455978135248687} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0} + m_AnchorMax: {x: 0.5, y: 0} + m_AnchoredPosition: {x: 50, y: 10} + m_SizeDelta: {x: 80, y: 30} + m_Pivot: {x: 0.5, y: 0} +--- !u!222 &7654955096077171671 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 157760054528398823} + m_CullTransparentMesh: 1 +--- !u!114 &6842567254505609782 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 157760054528398823} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &2945644090061992249 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 157760054528398823} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 6842567254505609782} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &1093717867300426814 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7472250681771032410} + - component: {fileID: 2682716320523083194} + - component: {fileID: 6092143913489455736} + m_Layer: 5 + m_Name: Text (TMP) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7472250681771032410 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1093717867300426814} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1863887949234781852} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &2682716320523083194 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1093717867300426814} + m_CullTransparentMesh: 1 +--- !u!114 &6092143913489455736 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1093717867300426814} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: Cancel + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 163d15b363a3bb343a8ba8275d295534, type: 2} + m_sharedMaterial: {fileID: -8103744068923331316, guid: 163d15b363a3bb343a8ba8275d295534, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4281479730 + m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 14 + m_fontSizeBase: 14 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!1 &1104576128895058025 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7106620200700405133} + - component: {fileID: 2762808303022281448} + - component: {fileID: 6984195261169084811} + m_Layer: 5 + m_Name: bg + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7106620200700405133 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1104576128895058025} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 8878455978135248687} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &2762808303022281448 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1104576128895058025} + m_CullTransparentMesh: 1 +--- !u!114 &6984195261169084811 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1104576128895058025} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: -895992892, guid: 73d757b5d1b754245969af12daf01e78, type: 3} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &1334662641111457779 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5494539156344885554} + - component: {fileID: 7293543872483431824} + - component: {fileID: 1270272885320112562} + - component: {fileID: 6539452975062971784} + m_Layer: 5 + m_Name: closeButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5494539156344885554 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1334662641111457779} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 2699345389104186032} + m_Father: {fileID: 8878455978135248687} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: -10, y: -10} + m_SizeDelta: {x: 29, y: 30} + m_Pivot: {x: 1, y: 1} +--- !u!222 &7293543872483431824 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1334662641111457779} + m_CullTransparentMesh: 1 +--- !u!114 &1270272885320112562 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1334662641111457779} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &6539452975062971784 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1334662641111457779} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1270272885320112562} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &2193905547316409483 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2699345389104186032} + - component: {fileID: 8057403921354161840} + - component: {fileID: 2686023620273265434} + m_Layer: 5 + m_Name: Text (TMP) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2699345389104186032 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2193905547316409483} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 5494539156344885554} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8057403921354161840 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2193905547316409483} + m_CullTransparentMesh: 1 +--- !u!114 &2686023620273265434 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2193905547316409483} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: X + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4281479730 + m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 24 + m_fontSizeBase: 24 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!1 &2199150456081301992 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6460714630850448276} + - component: {fileID: 5630343645790254490} + - component: {fileID: 1945917519109566323} + m_Layer: 5 + m_Name: messageText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6460714630850448276 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2199150456081301992} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 8878455978135248687} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -40, y: -100} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &5630343645790254490 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2199150456081301992} + m_CullTransparentMesh: 1 +--- !u!114 &1945917519109566323 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2199150456081301992} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: New Text + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: e938f39f708799f42bc6cb8f2d733c45, type: 2} + m_sharedMaterial: {fileID: 3963494727631305252, guid: e938f39f708799f42bc6cb8f2d733c45, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4278190080 + m_fontColor: {r: 0, g: 0, b: 0, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 14 + m_fontSizeBase: 14 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 1 + m_VerticalAlignment: 256 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!1 &2430457894950261084 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2292531325125449254} + - component: {fileID: 8251914459885537350} + - component: {fileID: 576750368261595047} + m_Layer: 5 + m_Name: titleText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2292531325125449254 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2430457894950261084} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 8878455978135248687} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 20, y: -10} + m_SizeDelta: {x: -70, y: 30} + m_Pivot: {x: 0, y: 1} +--- !u!222 &8251914459885537350 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2430457894950261084} + m_CullTransparentMesh: 1 +--- !u!114 &576750368261595047 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2430457894950261084} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: title + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 163d15b363a3bb343a8ba8275d295534, type: 2} + m_sharedMaterial: {fileID: -8103744068923331316, guid: 163d15b363a3bb343a8ba8275d295534, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4278190080 + m_fontColor: {r: 0, g: 0, b: 0, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 18 + m_fontSizeBase: 18 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 1 + m_VerticalAlignment: 256 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!1 &2913451268823470843 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8635697060592947593} + - component: {fileID: 6446731617233584224} + - component: {fileID: 5861331382376134956} + m_Layer: 5 + m_Name: shadow + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8635697060592947593 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2913451268823470843} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 8878455978135248687} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 5, y: -5} + m_SizeDelta: {x: 10, y: 10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &6446731617233584224 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2913451268823470843} + m_CullTransparentMesh: 1 +--- !u!114 &5861331382376134956 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2913451268823470843} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 887145076, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &4870865872226109854 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3379896220624033424} + - component: {fileID: 158648194781141200} + - component: {fileID: 6824421060178732060} + m_Layer: 5 + m_Name: Text (TMP) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3379896220624033424 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4870865872226109854} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2083401330524190855} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &158648194781141200 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4870865872226109854} + m_CullTransparentMesh: 1 +--- !u!114 &6824421060178732060 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4870865872226109854} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: Confirm + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 163d15b363a3bb343a8ba8275d295534, type: 2} + m_sharedMaterial: {fileID: -8103744068923331316, guid: 163d15b363a3bb343a8ba8275d295534, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4281479730 + m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 14 + m_fontSizeBase: 14 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!1 &7976514205510484745 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2083401330524190855} + - component: {fileID: 4187780467952822116} + - component: {fileID: 310464502753067420} + - component: {fileID: 6115370781225003563} + m_Layer: 5 + m_Name: confirmButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2083401330524190855 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7976514205510484745} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 3379896220624033424} + m_Father: {fileID: 8878455978135248687} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0} + m_AnchorMax: {x: 0.5, y: 0} + m_AnchoredPosition: {x: -50, y: 10} + m_SizeDelta: {x: 80, y: 30} + m_Pivot: {x: 0.5, y: 0} +--- !u!222 &4187780467952822116 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7976514205510484745} + m_CullTransparentMesh: 1 +--- !u!114 &310464502753067420 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7976514205510484745} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &6115370781225003563 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7976514205510484745} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 310464502753067420} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &8678504293771769994 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8878455978135248687} + - component: {fileID: 8346611057756904400} + m_Layer: 5 + m_Name: Confirm + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8878455978135248687 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8678504293771769994} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 8635697060592947593} + - {fileID: 7106620200700405133} + - {fileID: 5494539156344885554} + - {fileID: 1863887949234781852} + - {fileID: 2083401330524190855} + - {fileID: 6460714630850448276} + - {fileID: 2292531325125449254} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 400, y: 250} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &8346611057756904400 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8678504293771769994} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d70c9296cdb5be34d9c934fbdd52bdae, type: 3} + m_Name: + m_EditorClassIdentifier: + titleText: {fileID: 576750368261595047} + messageText: {fileID: 1945917519109566323} + confirmButton: {fileID: 6115370781225003563} + confirmButtonText: {fileID: 6824421060178732060} + cancelButton: {fileID: 2945644090061992249} + cancelButtonText: {fileID: 6092143913489455736} + closeButton: {fileID: 6539452975062971784} diff --git a/Assets/Resources/Prefabs/UI/Modal/Confirm.prefab.meta b/Assets/Resources/Prefabs/UI/Modal/Confirm.prefab.meta new file mode 100644 index 00000000..999084fd --- /dev/null +++ b/Assets/Resources/Prefabs/UI/Modal/Confirm.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 788059ac67f211f42ad47ab6abbc0488 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes/Main.unity b/Assets/Scenes/Main.unity index 0cf395e1..23f9266a 100644 --- a/Assets/Scenes/Main.unity +++ b/Assets/Scenes/Main.unity @@ -204,7 +204,7 @@ MonoBehaviour: m_Calls: - m_Target: {fileID: 632541408} m_TargetAssemblyTypeName: SampleProject.AppMain, Assembly-CSharp - m_MethodName: ShowAlertWithCustomConfirmText + m_MethodName: ShowAlert m_Mode: 1 m_Arguments: m_ObjectArgument: {fileID: 0} @@ -646,7 +646,7 @@ MonoBehaviour: m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_text: Alert.Show + m_text: Confirm.Show m_isRightToLeft: 0 m_fontAsset: {fileID: 11400000, guid: e938f39f708799f42bc6cb8f2d733c45, type: 2} m_sharedMaterial: {fileID: 3963494727631305252, guid: e938f39f708799f42bc6cb8f2d733c45, type: 2} @@ -824,7 +824,6 @@ RectTransform: - {fileID: 1694507572} - {fileID: 19718907} - {fileID: 2037570841} - - {fileID: 1100357418} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} @@ -832,142 +831,6 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0, y: 0} ---- !u!1 &502433643 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 502433644} - - component: {fileID: 502433646} - - component: {fileID: 502433645} - m_Layer: 5 - m_Name: Text (TMP) - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &502433644 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 502433643} - m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 1100357418} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 1, y: 1} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 0} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &502433645 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 502433643} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 1} - m_RaycastTarget: 1 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 1 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_text: Alert.Show - m_isRightToLeft: 0 - m_fontAsset: {fileID: 11400000, guid: e938f39f708799f42bc6cb8f2d733c45, type: 2} - m_sharedMaterial: {fileID: 3963494727631305252, guid: e938f39f708799f42bc6cb8f2d733c45, type: 2} - m_fontSharedMaterials: [] - m_fontMaterial: {fileID: 0} - m_fontMaterials: [] - m_fontColor32: - serializedVersion: 2 - rgba: 4281479730 - m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} - m_enableVertexGradient: 0 - m_colorMode: 3 - m_fontColorGradient: - topLeft: {r: 1, g: 1, b: 1, a: 1} - topRight: {r: 1, g: 1, b: 1, a: 1} - bottomLeft: {r: 1, g: 1, b: 1, a: 1} - bottomRight: {r: 1, g: 1, b: 1, a: 1} - m_fontColorGradientPreset: {fileID: 0} - m_spriteAsset: {fileID: 0} - m_tintAllSprites: 0 - m_StyleSheet: {fileID: 0} - m_TextStyleHashCode: -1183493901 - m_overrideHtmlColors: 0 - m_faceColor: - serializedVersion: 2 - rgba: 4294967295 - m_fontSize: 16 - m_fontSizeBase: 16 - m_fontWeight: 400 - m_enableAutoSizing: 0 - m_fontSizeMin: 18 - m_fontSizeMax: 72 - m_fontStyle: 0 - m_HorizontalAlignment: 2 - m_VerticalAlignment: 512 - m_textAlignment: 65535 - m_characterSpacing: 0 - m_wordSpacing: 0 - m_lineSpacing: 0 - m_lineSpacingMax: 0 - m_paragraphSpacing: 0 - m_charWidthMaxAdj: 0 - m_TextWrappingMode: 1 - m_wordWrappingRatios: 0.4 - m_overflowMode: 0 - m_linkedTextComponent: {fileID: 0} - parentLinkedComponent: {fileID: 0} - m_enableKerning: 0 - m_ActiveFontFeatures: 6e72656b - m_enableExtraPadding: 0 - checkPaddingRequired: 0 - m_isRichText: 1 - m_EmojiFallbackSupport: 1 - m_parseCtrlCharacters: 1 - m_isOrthographic: 1 - m_isCullingEnabled: 0 - m_horizontalMapping: 0 - m_verticalMapping: 0 - m_uvLineOffset: 0 - m_geometrySortingOrder: 0 - m_IsTextObjectScaleStatic: 0 - m_VertexBufferAutoSizeReduction: 0 - m_useMaxVisibleDescender: 1 - m_pageToDisplay: 1 - m_margin: {x: 0, y: 0, z: 0, w: 0} - m_isUsingLegacyAnimationComponent: 0 - m_isVolumetricText: 0 - m_hasFontAssetChanged: 0 - m_baseMaterial: {fileID: 0} - m_maskOffset: {x: 0, y: 0, z: 0, w: 0} ---- !u!222 &502433646 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 502433643} - m_CullTransparentMesh: 1 --- !u!1 &632541406 GameObject: m_ObjectHideFlags: 0 @@ -1122,139 +985,6 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: ec70649e0f460ce458cf6d62498ecf20, type: 3} m_Name: m_EditorClassIdentifier: ---- !u!1 &1100357417 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1100357418} - - component: {fileID: 1100357421} - - component: {fileID: 1100357420} - - component: {fileID: 1100357419} - m_Layer: 5 - m_Name: AlertButton (2) - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &1100357418 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1100357417} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: - - {fileID: 502433644} - m_Father: {fileID: 483439351} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0.5, y: 0.5} - m_AnchorMax: {x: 0.5, y: 0.5} - m_AnchoredPosition: {x: 0, y: -87} - m_SizeDelta: {x: 160, y: 30} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &1100357419 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1100357417} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Navigation: - m_Mode: 3 - m_WrapAround: 0 - m_SelectOnUp: {fileID: 0} - m_SelectOnDown: {fileID: 0} - m_SelectOnLeft: {fileID: 0} - m_SelectOnRight: {fileID: 0} - m_Transition: 1 - m_Colors: - m_NormalColor: {r: 1, g: 1, b: 1, a: 1} - m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} - m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} - m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} - m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} - m_ColorMultiplier: 1 - m_FadeDuration: 0.1 - m_SpriteState: - m_HighlightedSprite: {fileID: 0} - m_PressedSprite: {fileID: 0} - m_SelectedSprite: {fileID: 0} - m_DisabledSprite: {fileID: 0} - m_AnimationTriggers: - m_NormalTrigger: Normal - m_HighlightedTrigger: Highlighted - m_PressedTrigger: Pressed - m_SelectedTrigger: Selected - m_DisabledTrigger: Disabled - m_Interactable: 1 - m_TargetGraphic: {fileID: 1100357420} - m_OnClick: - m_PersistentCalls: - m_Calls: - - m_Target: {fileID: 632541408} - m_TargetAssemblyTypeName: SampleProject.AppMain, Assembly-CSharp - m_MethodName: ShowSimpleAlert - m_Mode: 1 - m_Arguments: - m_ObjectArgument: {fileID: 0} - m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine - m_IntArgument: 0 - m_FloatArgument: 0 - m_StringArgument: - m_BoolArgument: 0 - m_CallState: 2 ---- !u!114 &1100357420 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1100357417} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 1} - m_RaycastTarget: 1 - m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} - m_Maskable: 1 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} - m_Type: 1 - m_PreserveAspect: 0 - m_FillCenter: 1 - m_FillMethod: 4 - m_FillAmount: 1 - m_FillClockwise: 1 - m_FillOrigin: 0 - m_UseSpriteMesh: 0 - m_PixelsPerUnitMultiplier: 1 ---- !u!222 &1100357421 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1100357417} - m_CullTransparentMesh: 1 --- !u!1 &1101428664 GameObject: m_ObjectHideFlags: 0 @@ -1824,7 +1554,7 @@ GameObject: - component: {fileID: 2037570843} - component: {fileID: 2037570842} m_Layer: 5 - m_Name: AlertButton (1) + m_Name: ConfirmButton m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 @@ -1896,7 +1626,7 @@ MonoBehaviour: m_Calls: - m_Target: {fileID: 632541408} m_TargetAssemblyTypeName: SampleProject.AppMain, Assembly-CSharp - m_MethodName: ShowLocalizedAlert + m_MethodName: ShowConfirm m_Mode: 1 m_Arguments: m_ObjectArgument: {fileID: 0} diff --git a/Assets/Scripts/SampleProject/AppMain.cs b/Assets/Scripts/SampleProject/AppMain.cs index 6d0ec444..e384dfa6 100644 --- a/Assets/Scripts/SampleProject/AppMain.cs +++ b/Assets/Scripts/SampleProject/AppMain.cs @@ -47,28 +47,22 @@ namespace SampleProject //mqttService.Connect(); } - public async void ShowSimpleAlert() - { - Debug.Log("˸â ϴ..."); + public async void ShowAlert() + { await Alert.Show("˸", "̰ ˸ ޽Դϴ."); - Debug.Log("˸â ϴ."); - } - - public async void ShowAlertWithCustomConfirmText() - { await Alert.Show("", "͸ ϴ.", "˰ڽϴ"); - } - - public async void ShowLocalizedAlert() - { - // locale.json Ű ǵǾ ִٰ : - // "alert_title_error": "", "Error" - // "alert_message_network": "Ʈũ Ȯּ.", "Please check your network connection." - // "alert_button_retry": "õ", "Retry" - await Alert.ShowLocalized("error", "error_network_not", "button_retry"); - // Ǵ Ȯ ư ⺻ Ű(modal_confirm_button) Ϸ: - // await Alert.ShowLocalized("alert_title_error", "alert_message_network"); } + + public async void ShowConfirm() + { + bool result = await Confirm.Show("Ȯ", "̰ ˸ ޽Դϴ."); + ULog.Debug($"ڰ Ȯ ư ? {result}"); + result = await Confirm.Show("", "͸ ϴ.", "˰ڽϴ", "ƴϿ"); + ULog.Debug($"ڰ ˸ Ȯ߳? {result}"); + result = await Confirm.ShowLocalized("error", "error_network_not", "button_retry", "button_cancel"); + ULog.Debug($"ڰ Ʈũ ˸ Ȯ߳? {result}"); + } + } } diff --git a/Assets/Scripts/UVC/Locale/LocalizationManager.cs b/Assets/Scripts/UVC/Locale/LocalizationManager.cs index 1c0ee8fb..441253cd 100644 --- a/Assets/Scripts/UVC/Locale/LocalizationManager.cs +++ b/Assets/Scripts/UVC/Locale/LocalizationManager.cs @@ -75,7 +75,7 @@ namespace UVC.Locale { if (_instance == null) { - _instance = new LocalizationManager("en-US"); + _instance = new LocalizationManager("ko-KR"); _instance.LoadDefaultLocalizationData(); // 기본 언어 데이터 로드 } } diff --git a/Assets/Scripts/UVC/UI/Modal/Alert.cs b/Assets/Scripts/UVC/UI/Modal/Alert.cs index 4b6719bc..533b7090 100644 --- a/Assets/Scripts/UVC/UI/Modal/Alert.cs +++ b/Assets/Scripts/UVC/UI/Modal/Alert.cs @@ -1,74 +1,138 @@ using Cysharp.Threading.Tasks; using UVC.Locale; // ConfirmButtonText의 기본값을 위해 추가 +using UVC.Log; // ULog 사용 예시를 위해 추가 (필요에 따라) namespace UVC.UI.Modal { /// - /// 간단한 알림 메시지를 표시하는 정적 클래스입니다. - /// 확인 버튼만 있으며, 사용자의 확인을 기다립니다. + /// 📢 간단한 알림 메시지를 화면에 보여주는 친구예요. + /// 이 친구는 "확인" 버튼만 가지고 있어서, 사용자가 내용을 읽고 확인 버튼을 누를 때까지 기다려줘요. + /// 복잡한 선택 없이, 간단한 정보 전달이나 경고를 보여줄 때 사용하면 좋아요. /// + /// + /// + /// // 가장 기본적인 알림창 사용법 + /// async UniTask ShowMyFirstAlert() + /// { + /// await Alert.Show("알림", "게임 데이터가 성공적으로 저장되었습니다!"); + /// ULog.Debug("사용자가 알림을 확인했습니다."); + /// } + /// + /// // 버튼 글자도 바꿔볼까요? + /// async UniTask ShowCustomButtonAlert() + /// { + /// await Alert.Show("레벨 업!", "축하합니다! 레벨 5를 달성했어요!", confirmButtonText: "야호!"); + /// } + /// + /// public static class Alert { /// - /// Alert 모달 프리팹의 기본 경로입니다. + /// 🎨 알림창의 기본 디자인(프리팹) 파일이 어디 있는지 알려주는 경로예요. + /// 특별히 다른 디자인을 쓰고 싶지 않으면 이 기본 디자인을 사용해요. /// - private const string DefaultAlertPrefabPath = "Prefabs/UI/Modal/Alert"; + private const string DefaultPrefabPath = "Prefabs/UI/Modal/Alert"; /// - /// 지정된 제목과 메시지로 알림창을 표시합니다. - /// 사용자가 확인 버튼을 누를 때까지 기다립니다. + /// ✨ 지정된 제목과 메시지로 알림창을 화면에 뿅! 하고 보여줘요. + /// 사용자가 "확인" 버튼을 누를 때까지 코드는 여기서 잠시 멈춰 기다려요. /// - /// 알림창의 제목입니다. - /// 알림창에 표시될 메시지입니다. - /// 확인 버튼에 표시될 텍스트입니다. null일 경우 LocalizationManager에서 "modal_confirm_button" 키로 조회합니다. - /// 사용자 정의 알림 프리팹 경로입니다. null일 경우 기본 경로를 사용합니다. - /// 모달이 닫힐 때 완료되는 UniTask입니다. + /// 알림창 맨 위에 크게 보일 '제목'이에요. + /// 알림창에 보여줄 '메시지 내용'이에요. + /// "확인" 버튼에 보여줄 글자예요. 아무것도 안 적으면 기본 글자("확인" 또는 설정된 언어)가 나와요. + /// 만약 특별히 만들어둔 알림창 디자인이 있다면, 그 파일 경로를 여기에 적어주세요. 없으면 기본 디자인을 사용해요. + /// 사용자가 확인 버튼을 누르면 완료되는 작업(UniTask)이에요. 특별한 값을 돌려주진 않아요. + /// + /// + /// public class GameManager : MonoBehaviour + /// { + /// public async void OnPlayerSaveGame() + /// { + /// // (게임 저장 로직...) + /// bool success = true; // 저장 성공했다고 가정 + /// + /// if (success) + /// { + /// await Alert.Show("저장 완료", "게임 진행 상황이 안전하게 저장되었습니다.", "알겠습니다"); + /// ULog.Debug("저장 완료 알림을 플레이어가 확인했습니다."); + /// } + /// else + /// { + /// await Alert.Show("저장 실패", "오류가 발생하여 저장하지 못했습니다.", "다시 시도"); + /// // (다시 시도 로직 또는 다른 처리...) + /// } + /// } + /// } + /// + /// public static async UniTask Show( string title, string message, string confirmButtonText = null, string customPrefabPath = null) { - string prefabPath = string.IsNullOrEmpty(customPrefabPath) ? DefaultAlertPrefabPath : customPrefabPath; + string prefabPath = string.IsNullOrEmpty(customPrefabPath) ? DefaultPrefabPath : customPrefabPath; - // ModalContent 설정 + // ModalContent 레시피를 만들어요. 알림창은 취소 버튼이 필요 없으니 숨겨요. ModalContent content = new ModalContent(prefabPath) { Title = title, Message = message, ShowCancelButton = false, // Alert에서는 취소 버튼 숨김 - // ConfirmButtonText는 null이면 ModalContent의 getter가 LocalizationManager를 사용함 }; + // 확인 버튼 글자를 따로 정해줬다면 그걸로 설정해요. if (!string.IsNullOrEmpty(confirmButtonText)) { content.ConfirmButtonText = confirmButtonText; } - // else인 경우, ModalContent의 ConfirmButtonText getter가 - // LocalizationManager.Instance.GetString("modal_confirm_button")을 사용합니다. - // 만약 이 키가 아닌 다른 키를 기본값으로 사용하고 싶다면 여기서 설정할 수 있습니다. - // 예: content.ConfirmButtonText = LocalizationManager.Instance.GetString("alert_ok_button"); + // 아니면 ModalContent의 기본 설정(다국어 지원 "button_confirm" 키)을 따라요. + // 만약 다른 기본 키를 쓰고 싶다면 여기서 설정할 수도 있어요. + // 예: content.ConfirmButtonText = LocalizationManager.Instance.GetString("alert_default_ok_button"); - // Modal.Open 호출. Alert은 별도의 결과를 반환하지 않으므로 T는 bool 또는 object 같은 기본 타입을 사용할 수 있습니다. - // 여기서는 bool을 사용하고, 확인 버튼은 true를 반환하도록 Modal 시스템이 되어있다고 가정합니다. - // 실제 반환값은 사용하지 않으므로, UniTask을 받고 무시합니다. + // Modal 시스템에게 "이 레시피대로 모달 창 열어줘!" 라고 부탁해요. + // Alert은 사용자의 선택 결과(true/false)가 중요하지 않으므로, bool 타입으로 받고 결과는 무시해요. await Modal.Open(content); } /// - /// 다국어 키를 사용하여 제목과 메시지를 표시하는 알림창을 엽니다. + /// 🌍 다국어(여러 나라 언어)를 지원하는 알림창을 보여줘요. + /// 미리 준비된 '언어 키'를 알려주면, 게임 설정 언어에 맞는 글자를 자동으로 찾아서 보여줘요. /// - /// 제목으로 사용할 다국어 키입니다. - /// 메시지로 사용할 다국어 키입니다. - /// 확인 버튼 텍스트로 사용할 다국어 키입니다. null일 경우 "modal_confirm_button"을 사용합니다. - /// 사용자 정의 알림 프리팹 경로입니다. null일 경우 기본 경로를 사용합니다. - /// 모달이 닫힐 때 완료되는 UniTask입니다. + /// 제목에 사용할 '언어 키'예요. (예: "alert_title_welcome") + /// 메시지 내용에 사용할 '언어 키'예요. (예: "alert_message_item_acquired") + /// 확인 버튼 글자에 사용할 '언어 키'예요. 안 적으면 기본 키("button_confirm")를 사용해요. + /// 특별한 알림창 디자인 파일 경로예요. 없으면 기본 디자인을 사용해요. + /// 사용자가 확인 버튼을 누르면 완료되는 작업(UniTask)이에요. + /// + /// + /// public class QuestManager : MonoBehaviour + /// { + /// public async void OnQuestCompleted(string questNameKey) + /// { + /// // 예시: 퀘스트 완료 메시지를 현재 설정된 언어로 보여줍니다. + /// // titleLocalizationKey: "quest_completed_title" -> "퀘스트 완료" (한국어), "Quest Complete" (영어) + /// // messageLocalizationKey: "quest_completed_message" -> "{0} 퀘스트를 완료했습니다!" (한국어), "You have completed the {0} quest!" (영어) + /// // 여기서 {0} 부분은 실제 퀘스트 이름으로 바뀔 수 있도록 LocalizationManager에서 처리한다고 가정합니다. + /// + /// string localizedQuestName = LocalizationManager.Instance.GetString(questNameKey); // 예: "main_quest_01" -> "첫 번째 임무" + /// string formattedMessageKey = "quest_completed_message"; // 실제로는 메시지 포맷팅이 필요할 수 있음 + /// + /// // 실제 메시지는 LocalizationManager에서 포맷팅을 지원해야 함 + /// // 여기서는 간단히 키만 전달하는 것으로 가정 + /// await Alert.ShowLocalized("quest_completed_title", formattedMessageKey, confirmButtonLocalizationKey: "ui_button_great"); + /// ULog.Debug("퀘스트 완료 알림을 플레이어가 확인했습니다."); + /// } + /// } + /// + /// public static async UniTask ShowLocalized( string titleLocalizationKey, string messageLocalizationKey, string confirmButtonLocalizationKey = null, string customPrefabPath = null) { + // 언어 키를 사용해서 실제 보여줄 글자들을 가져와요. string title = LocalizationManager.Instance.GetString(titleLocalizationKey); string message = LocalizationManager.Instance.GetString(messageLocalizationKey); string confirmText = null; @@ -77,8 +141,9 @@ namespace UVC.UI.Modal { confirmText = LocalizationManager.Instance.GetString(confirmButtonLocalizationKey); } - // confirmText가 null이면 Show 메서드 내부에서 ModalContent의 기본 로직(modal_confirm_button 키 사용)이 적용됩니다. + // confirmText가 null (따로 안 정해줬으면)이면, Show() 메서드 안에서 ModalContent의 기본 글자 로직이 알아서 처리해줘요. + // 준비된 글자들로 알림창을 보여달라고 Show()에게 다시 부탁해요. await Show(title, message, confirmText, customPrefabPath); } } diff --git a/Assets/Scripts/UVC/UI/Modal/Confirm.cs b/Assets/Scripts/UVC/UI/Modal/Confirm.cs new file mode 100644 index 00000000..ff5e8e9f --- /dev/null +++ b/Assets/Scripts/UVC/UI/Modal/Confirm.cs @@ -0,0 +1,188 @@ +using Cysharp.Threading.Tasks; +using UVC.Locale; // ButtonText의 기본값을 위해 추가 +using UVC.Log; // ULog 사용 예시를 위해 추가 (필요에 따라) + +namespace UVC.UI.Modal +{ + /// + /// 🤔 사용자에게 "정말 ~하시겠어요?" 하고 물어보고, "예" 또는 "아니오" 선택을 받는 친구예요. + /// 중요한 결정이나 되돌릴 수 없는 행동 전에 한 번 더 확인받을 때 사용하면 좋아요. + /// 사용자가 어떤 버튼을 눌렀는지 (확인 또는 취소) 알려줘요. + /// + /// + /// + /// // 게임 종료 전에 정말 끌 건지 물어보는 예시 + /// public async void TryExitGame() + /// { + /// bool wantsToExit = await Confirm.Show("게임 종료", "정말로 게임을 종료하시겠습니까?", "네, 종료합니다", "아니요, 계속할래요"); + /// if (wantsToExit) + /// { + /// ULog.Debug("사용자가 게임 종료를 확인했습니다."); + /// // Application.Quit(); // 실제 게임 종료 코드 + /// } + /// else + /// { + /// ULog.Debug("사용자가 게임 종료를 취소했습니다."); + /// } + /// } + /// + /// // 아이템 삭제 확인 + /// public async void TryDeleteItem(string itemName) + /// { + /// string title = "아이템 삭제"; + /// string message = $"정말로 '{itemName}' 아이템을 삭제하시겠습니까? 이 행동은 되돌릴 수 없습니다."; + /// + /// // 기본 버튼 텍스트 사용 (ModalContent에서 설정된 "확인", "취소") + /// bool confirmed = await Confirm.Show(title, message); + /// + /// if (confirmed) + /// { + /// ULog.Debug($"'{itemName}' 아이템 삭제를 진행합니다."); + /// // (아이템 삭제 로직...) + /// } + /// } + /// + /// + public static class Confirm + { + /// + /// 🎨 확인창의 기본 디자인(프리팹) 파일이 어디 있는지 알려주는 경로예요. + /// + private const string DefaultPrefabPath = "Prefabs/UI/Modal/Confirm"; // 실제 프로젝트 경로에 맞게 수정하세요. + + /// + /// ✨ 지정된 제목과 메시지로 확인창을 화면에 뿅! 하고 보여줘요. + /// 사용자가 "확인" 또는 "취소" 버튼을 누를 때까지 코드는 여기서 잠시 멈춰 기다려요. + /// + /// 확인창 맨 위에 크게 보일 '제목'이에요. + /// 확인창에 보여줄 '질문 또는 메시지 내용'이에요. + /// "확인" 버튼에 보여줄 글자예요. 안 적으면 기본 글자("확인")가 나와요. + /// "취소" 버튼에 보여줄 글자예요. 안 적으면 기본 글자("취소")가 나와요. + /// 특별히 만들어둔 확인창 디자인 파일 경로예요. 없으면 기본 디자인을 사용해요. + /// 사용자가 "확인"을 누르면 true, "취소"를 누르면 false를 돌려주는 작업(UniTask<bool>)이에요. + /// + /// + /// public class ShopManager : MonoBehaviour + /// { + /// public async void OnPurchaseItem(string itemName, int price) + /// { + /// string purchaseTitle = "구매 확인"; + /// string purchaseMessage = $"{itemName} 아이템을 {price} 골드에 구매하시겠습니까?"; + /// + /// bool confirmed = await Confirm.Show(purchaseTitle, purchaseMessage, "구매", "나중에"); + /// + /// if (confirmed) + /// { + /// ULog.Debug($"{itemName} 구매를 진행합니다."); + /// // (구매 처리 로직...) + /// } + /// else + /// { + /// ULog.Debug($"{itemName} 구매를 취소했습니다."); + /// } + /// } + /// } + /// + /// + public static async UniTask Show( + string title, + string message, + string confirmButtonText = null, + string cancelButtonText = null, + string customPrefabPath = null) + { + string prefabPath = string.IsNullOrEmpty(customPrefabPath) ? DefaultPrefabPath : customPrefabPath; + + // ModalContent 레시피를 만들어요. 확인창은 확인/취소 버튼이 모두 필요해요. + ModalContent content = new ModalContent(prefabPath) + { + Title = title, + Message = message, + ShowConfirmButton = true, // Confirm에서는 확인 버튼 항상 표시 + ShowCancelButton = true // Confirm에서는 취소 버튼 항상 표시 + }; + + // 확인 버튼 글자를 따로 정해줬다면 그걸로 설정해요. + if (!string.IsNullOrEmpty(confirmButtonText)) + { + content.ConfirmButtonText = confirmButtonText; + } + + // 취소 버튼 글자를 따로 정해줬다면 그걸로 설정해요. + if (!string.IsNullOrEmpty(cancelButtonText)) + { + content.CancelButtonText = cancelButtonText; + } + + // Modal 시스템에게 "이 레시피대로 모달 창 열어줘!" 라고 부탁하고, 사용자의 선택(true/false)을 기다려요. + return await Modal.Open(content); + } + + /// + /// 🌍 다국어(여러 나라 언어)를 지원하는 확인창을 보여줘요. + /// 미리 준비된 '언어 키'를 알려주면, 게임 설정 언어에 맞는 글자를 자동으로 찾아서 보여줘요. + /// + /// 제목에 사용할 '언어 키'예요. (예: "confirm_title_exit_game") + /// 메시지 내용에 사용할 '언어 키'예요. (예: "confirm_message_are_you_sure") + /// 확인 버튼 글자에 사용할 '언어 키'예요. 안 적으면 기본 키("button_confirm")를 사용해요. + /// 취소 버튼 글자에 사용할 '언어 키'예요. 안 적으면 기본 키("button_cancel")를 사용해요. + /// 특별한 확인창 디자인 파일 경로예요. 없으면 기본 디자인을 사용해요. + /// 사용자가 "확인"을 누르면 true, "취소"를 누르면 false를 돌려주는 작업(UniTask<bool>)이에요. + /// + /// + /// public class SettingsManager : MonoBehaviour + /// { + /// public async void OnResetSettings() + /// { + /// // 예시: 설정 초기화 전에 다국어로 확인을 받습니다. + /// // titleLocalizationKey: "settings_reset_title" -> "설정 초기화" (한국어), "Reset Settings" (영어) + /// // messageLocalizationKey: "settings_reset_confirm_message" -> "모든 설정을 초기화하시겠습니까?" (한국어), "Are you sure you want to reset all settings?" (영어) + /// + /// bool confirmed = await Confirm.ShowLocalized( + /// "settings_reset_title", + /// "settings_reset_confirm_message", + /// confirmButtonLocalizationKey: "ui_button_reset", // "초기화" + /// cancelButtonLocalizationKey: "ui_button_keep_current" // "유지" + /// ); + /// + /// if (confirmed) + /// { + /// ULog.Debug("설정을 초기화합니다."); + /// // (설정 초기화 로직...) + /// } + /// else + /// { + /// ULog.Debug("설정 초기화를 취소했습니다."); + /// } + /// } + /// } + /// + /// + public static async UniTask ShowLocalized( + string titleLocalizationKey, + string messageLocalizationKey, + string confirmButtonLocalizationKey = null, + string cancelButtonLocalizationKey = null, + string customPrefabPath = null) + { + // 언어 키를 사용해서 실제 보여줄 글자들을 가져와요. + string title = LocalizationManager.Instance.GetString(titleLocalizationKey); + string message = LocalizationManager.Instance.GetString(messageLocalizationKey); + string confirmText = null; + string cancelText = null; + + if (!string.IsNullOrEmpty(confirmButtonLocalizationKey)) + { + confirmText = LocalizationManager.Instance.GetString(confirmButtonLocalizationKey); + } + + if (!string.IsNullOrEmpty(cancelButtonLocalizationKey)) + { + cancelText = LocalizationManager.Instance.GetString(cancelButtonLocalizationKey); + } + + // 준비된 글자들로 확인창을 보여달라고 Show()에게 다시 부탁하고, 사용자의 선택을 기다려요. + return await Show(title, message, confirmText, cancelText, customPrefabPath); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/Modal/Confirm.cs.meta b/Assets/Scripts/UVC/UI/Modal/Confirm.cs.meta new file mode 100644 index 00000000..a36633b4 --- /dev/null +++ b/Assets/Scripts/UVC/UI/Modal/Confirm.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 64b63f405dcd3d24fb7253329e140efe \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/Modal/Modal.cs b/Assets/Scripts/UVC/UI/Modal/Modal.cs index ada25dad..8bd9e740 100644 --- a/Assets/Scripts/UVC/UI/Modal/Modal.cs +++ b/Assets/Scripts/UVC/UI/Modal/Modal.cs @@ -7,173 +7,343 @@ using UVC.Log; namespace UVC.UI.Modal { + /// + /// ✨짠! 특별한 알림 상자를 보여주는 마법사, Modal 클래스예요! ✨ + /// 게임을 하다가 갑자기 "레벨 업!" 메시지가 뜨거나, "정말 게임을 끌 건가요?" 하고 물어보는 창 있죠? + /// 그런 창들을 '모달'이라고 불러요. Modal 클래스는 이런 모달 창을 쉽게 만들고 보여줄 수 있게 도와준답니다. + /// 마치 요술봉처럼, 필요할 때 "모달 나와라, 뚝딱!" 하고 명령하면 화면에 알림 상자를 뿅! 하고 나타나게 할 수 있어요. + /// 그리고 다 봤으면 "모달 들어가라, 뿅!" 하고 사라지게 할 수도 있죠. + /// public static class Modal { + /// + /// 📦 현재 열려있는 모달의 내용물(ModalContent)을 가리키는 포인터예요. + /// 모달이 열릴 때 설정되고, 닫힐 때 null이 됩니다. + /// OnClose 호출 시 사용됩니다. + /// + private static ModalContent currentContent; + + /// + /// 🧙‍♂️ 현재 화면에 떠 있는 모달 창 자체를 가리키는 비밀 포인터예요. + /// 모달이 열리면 여기에 그 모달 창이 저장되고, 닫히면 null(없음)이 돼요. + /// 한 번에 하나의 모달만 보여주기 위해 이 포인터를 사용해요. + /// private static GameObject currentModalInstance; + + /// + /// 🛡️ 모달 창이 뜰 때 뒤에 있는 다른 버튼들을 누르지 못하게 막아주는 '투명 방패'예요. + /// 이 방패도 모달이 열릴 때 나타났다가, 닫히면 사라져요. + /// private static GameObject currentBlockerInstance; + + /// + /// 📜 모달 창이 "네!" 또는 "아니오!" 같은 대답을 받을 때까지 기다리는 '약속 증서'예요. + /// UniTaskCompletionSource의 줄임말인 tcs는 'Task Completion Source'의 약자랍니다. + /// 모달이 열릴 때 이 약속 증서가 만들어지고, 사용자가 버튼을 누르면 여기에 결과가 적혀요. + /// private static IUniTaskSource activeTcs; + + /// + /// 🏷️ 모달이 돌려줄 대답의 종류(타입)를 기억하는 '이름표'예요. + /// 예를 들어, '예/아니오' 질문이면 bool 타입(참/거짓)이라는 이름표가 붙어요. + /// private static Type activeResultType; + /// + /// 🖼️ 모달 뒤에 깔릴 기본 '투명 방패' 디자인 파일이 어디 있는지 알려주는 '주소'예요. + /// 특별히 다른 방패를 쓰고 싶다고 말하지 않으면 이 기본 방패를 사용해요. + /// private const string DefaultBlockerPrefabPath = "Prefabs/UI/Modal/ModalBlocker"; + /// + /// ✨ 모달아, 열려라! ✨ + /// 이 마법 주문을 외치면 화면에 새로운 알림 상자(모달)가 뿅! 하고 나타나요. + /// 어떤 내용을 보여줄지, 버튼은 어떻게 할지 미리 정해서 알려줘야 해요. + /// + /// 예를 들어, "정말 게임을 종료할까요?" 라는 질문과 함께 [예], [아니오] 버튼이 있는 모달을 보여주고 싶다고 해봐요. + /// 이럴 때 이 Open 마법을 사용하면 된답니다! + /// + /// 모달이 닫힐 때 어떤 대답을 했는지 (예: '예' 버튼을 눌렀는지, '아니오' 버튼을 눌렀는지) 알려줄 수도 있어요. + /// 그 대답의 종류를 여기에 적어주면 돼요. 예를 들어, bool이라고 적으면 '참' 또는 '거짓'으로 대답을 받을 수 있어요. + /// + /// 모달이 닫힐 때 돌려받을 대답의 종류예요. 예를 들어, '예'/'아니오' 선택은 bool 타입으로 받을 수 있어요. + /// 모달에 보여줄 제목, 메시지, 버튼 모양 등을 정한 '모달 내용물 꾸러미'예요. + /// 모달이 뜰 때 뒤에 있는 화면을 살짝 가려주는 '가림막'의 디자인 파일 경로예요. 안 써주면 기본 가림막을 사용해요. + /// 모달이 닫힐 때 사용자가 선택한 결과(대답)를 돌려줘요. 예를 들어, '예' 버튼을 누르면 true를 돌려줄 수 있어요. + /// + /// + /// // "정말 게임을 종료할까요?" 모달을 열고, 사용자의 대답(true 또는 false)을 기다리는 예시예요. + /// async UniTaskVoid ShowExitConfirmModal() + /// { + /// // 1. 모달에 어떤 내용을 보여줄지 정해요. + /// // "MyConfirmModalPrefab" 부분에는 실제 만들어둔 모달 프리팹 파일의 경로를 적어주세요. + /// var myModalContent = new ModalContent("Prefabs/UI/MyConfirmModalPrefab") + /// { + /// Title = "게임 종료", + /// Message = "정말로 게임을 종료하시겠어요? 🥺", + /// ConfirmButtonText = "네! 끌래요", + /// CancelButtonText = "아니요! 더 할래요" + /// }; + /// + /// // 2. Modal.Open 마법으로 모달을 열어요! 사용자가 버튼을 누를 때까지 기다렸다가, 그 결과를 알려줘요. + /// // 여기서는 사용자가 '네! 끌래요'를 누르면 true, '아니요! 더 할래요'나 닫기 버튼을 누르면 false를 돌려받기로 약속했어요(). + /// bool userSaidYes = await Modal.Open(myModalContent); + /// + /// // 3. 사용자의 대답에 따라 다른 행동을 해요. + /// if (userSaidYes) + /// { + /// Debug.Log("흑흑, 게임을 종료합니다... 다음에 또 만나요! 👋"); + /// // Application.Quit(); // 진짜로 게임을 끄는 코드 + /// } + /// else + /// { + /// Debug.Log("야호! 게임을 계속합니다! 🥳"); + /// } + /// } + /// + /// public static async UniTask Open(ModalContent content, string blockerPrefabPath = DefaultBlockerPrefabPath) { + // 📜 이야기: 모달을 열기 전에, 이미 다른 모달이 열려있는지 확인해요. + // 만약 그렇다면, "앗! 이미 다른 모달이 열려있어요!"라고 알려주고 새 모달은 열지 않아요. + // 한 번에 하나의 모달만 보여주는 것이 규칙이거든요! if (currentModalInstance != null) { ULog.Warning("[Modal] 다른 모달이 이미 열려있습니다. 새 모달을 열기 전에 기존 모달을 닫아주세요."); - return default(T); + return default(T); // 기본값(예: bool이면 false, 숫자면 0)을 돌려주고 끝내요. } + // 📜 이야기: 새 모달을 위한 '약속 증서(tcs)'를 만들어요. + // 이 증서는 나중에 사용자가 버튼을 누르면 그 결과를 기록할 거예요. + // 그리고 이 증서와 결과 타입을 마법사의 비밀 도구함에 잘 보관해요. var tcs = new UniTaskCompletionSource(); activeTcs = tcs; activeResultType = typeof(T); + currentContent = content; // 현재 content 저장 + // --- 투명 방패(Blocker) 준비 --- CanvasGroup blockerCanvasGroup = null; + // 📜 이야기: '투명 방패' 디자인 파일을 불러와요. Resources.LoadAsync는 비동기(기다리지 않고 다음 일 하기)로 파일을 불러오는 마법이에요. GameObject blockerPrefabObj = await Resources.LoadAsync(blockerPrefabPath) as GameObject; if (blockerPrefabObj != null) { + // 화면에서 가장 큰 그림판(Canvas)을 찾아서 그 위에 방패를 놓을 거예요. Canvas mainCanvasForBlocker = UnityEngine.Object.FindFirstObjectByType(); if (mainCanvasForBlocker != null) { + // 방패를 복제해서(Instantiate) 그림판 위에 놓고, 가장 위로 오도록 순서를 조정해요. currentBlockerInstance = UnityEngine.Object.Instantiate(blockerPrefabObj, mainCanvasForBlocker.transform); currentBlockerInstance.transform.SetAsLastSibling(); + + // 방패가 부드럽게 나타나도록 CanvasGroup 컴포넌트를 사용해요. 없으면 새로 추가! blockerCanvasGroup = currentBlockerInstance.GetComponent(); if (blockerCanvasGroup == null) blockerCanvasGroup = currentBlockerInstance.AddComponent(); - blockerCanvasGroup.alpha = 0f; - _ = FadeUI(blockerCanvasGroup, 0.7f, 0.3f, true); + blockerCanvasGroup.alpha = 0f;// 처음엔 완전히 투명하게 + _ = FadeUI(blockerCanvasGroup, 0.7f, 0.3f, true);// 0.3초 동안 서서히 나타나게 (투명도 70%) } - else ULog.Error("[Modal] UIBlocker를 표시할 Canvas를 찾을 수 없습니다."); + else ULog.Error("[Modal] UIBlocker를 표시할 Canvas를 찾을 수 없습니다."); // 그림판을 못 찾으면 에러! } else ULog.Warning($"[Modal] UIBlocker 프리팹을 다음 경로에서 찾을 수 없습니다: {blockerPrefabPath}"); - GameObject modalPrefabObj = await Resources.LoadAsync(content.PrefabPath) as GameObject; + // --- 모달 창(Modal) 준비 --- + // 📜 이야기: 이제 진짜 모달 창 디자인 파일을 불러올 차례예요. 이것도 비동기로! + GameObject modalPrefabObj = await Resources.LoadAsync(currentContent.PrefabPath) as GameObject; if (modalPrefabObj == null) { - ULog.Error($"[Modal] 모달 프리팹을 다음 경로에서 찾을 수 없습니다: {content.PrefabPath}"); - await CleanupCurrentModalResources(null, false); // ModalView가 없으므로 null 전달 - tcs.TrySetResult(default(T)); - return await tcs.Task; + ULog.Error($"[Modal] 모달 프리팹을 다음 경로에서 찾을 수 없습니다: {currentContent.PrefabPath}"); + await CleanupCurrentModalResources(currentContent, null, false); // 열려던 것들(방패 등)을 깨끗이 치우고, + tcs.TrySetResult(default(T)); // 약속 증서에는 "실패했어요" (기본값) 라고 적어요. + return await tcs.Task; // 그리고 결과를 돌려주고 끝. } - Canvas mainCanvasForModal = UnityEngine.Object.FindObjectOfType(); - if (mainCanvasForModal == null) + // 모달 창도 가장 큰 그림판 위에 놓을 거예요. + Canvas mainCanvasForModal = UnityEngine.Object.FindFirstObjectByType(); + if (mainCanvasForModal == null) // 그림판을 못 찾으면, { ULog.Error("[Modal] 모달을 표시할 Canvas를 찾을 수 없습니다."); - await CleanupCurrentModalResources(null, false); - tcs.TrySetResult(default(T)); - return await tcs.Task; + await CleanupCurrentModalResources(currentContent, null, false); // 마찬가지로 정리하고, + tcs.TrySetResult(default(T)); // 실패 기록. + return await tcs.Task; // 결과 돌려주고 끝. } + // 모달 창을 복제해서 그림판 위에 놓고, 가장 위로 오도록 순서를 조정해요. (방패보다도 위!) currentModalInstance = UnityEngine.Object.Instantiate(modalPrefabObj, mainCanvasForModal.transform); currentModalInstance.transform.SetAsLastSibling(); + // 📜 이야기: 모달 창에는 'ModalView'라는 특별한 부품이 붙어있어야 해요. + // 이 부품이 모달 창의 글자, 버튼 등을 관리하거든요. ModalView modalView = currentModalInstance.GetComponent(); - if (modalView == null) + if (modalView == null) // ModalView 부품이 없다면, { - ULog.Error($"[Modal] 모달 프리팹에 ModalView 컴포넌트가 없습니다: {content.PrefabPath}"); - await CleanupCurrentModalResources(null, false); - tcs.TrySetResult(default(T)); - return await tcs.Task; + ULog.Error($"[Modal] 모달 프리팹에 ModalView 컴포넌트가 없습니다: {currentContent.PrefabPath}"); + await CleanupCurrentModalResources(currentContent, null, false); // 정리하고, + tcs.TrySetResult(default(T)); // 실패 기록. + return await tcs.Task; // 결과 돌려주고 끝. } - - + // 📜 이야기: 만약 모달 창이 (우리가 모르는 사이에) 갑자기 사라지면 어떻게 될까요? + // 그런 상황에 대비해서, 모달 창이 파괴될 때 자동으로 "취소"된 것처럼 처리하도록 등록해둬요. + // GetCancellationTokenOnDestroy()는 "이 게임 오브젝트가 파괴되면 알려줘!"라는 신호예요. var modalDestroyToken = currentModalInstance.GetCancellationTokenOnDestroy(); modalDestroyToken.RegisterWithoutCaptureExecutionContext(async () => { + // 이 코드는 모달 인스턴스가 파괴될 때 실행돼요. + // 만약 우리가 아직 결과를 기다리고 있는(Pending) 모달이었다면, if (Modal.activeTcs == tcs && tcs.Task.Status == UniTaskStatus.Pending) { ULog.Debug("[Modal] 활성 모달 인스턴스가 외부에서 파괴되어 취소로 처리합니다."); - // modalView 참조가 유효하다면 전달, 아니면 null + // 파괴된 모달에서 ModalView를 가져오려고 시도해요 (없을 수도 있지만). ModalView viewOnDestroy = currentModalInstance != null ? currentModalInstance.GetComponent() : null; - await CleanupCurrentModalResources(viewOnDestroy, false, true, tcs, typeof(T)); + // 그리고 "외부에서 파괴됐으니 취소할게요" 라고 알리면서 정리해요. + await CleanupCurrentModalResources(currentContent, viewOnDestroy, false, true, tcs, typeof(T)); } }); - if (modalView.titleText != null) modalView.titleText.text = content.Title; - if (modalView.messageText != null) modalView.messageText.text = content.Message; + // 📜 이야기: ModalView가 UI를 설정하도록 OnOpen 호출 전에 버튼 리스너만 설정합니다. + // ModalView.OnOpen 내부에서 content를 기반으로 title, message, 버튼 텍스트/활성화 등을 설정합니다. - SetupButton(modalView.confirmButton, modalView.confirmButtonText, content.ConfirmButtonText, content.ShowConfirmButton, - () => _ = HandleModalActionAsync(tcs, true, modalView)); + // --- 버튼 리스너 설정 --- + // ModalView의 버튼 객체에 Modal 시스템의 액션을 연결합니다. + // 버튼의 텍스트나 활성화 상태는 ModalView.OnOpen에서 content를 기반으로 설정됩니다. + SetupButtonClickListeners(modalView, currentContent, tcs); - SetupButton(modalView.cancelButton, modalView.cancelButtonText, content.CancelButtonText, content.ShowCancelButton, - () => _ = HandleModalActionAsync(tcs, false, modalView)); - SetupButton(modalView.closeButton, null, null, true, - () => _ = HandleModalActionAsync(tcs, false, modalView)); - - // OnOpen 호출 - modalView.OnOpen(); + // 📜 이야기: 모달 창이 화면에 나타나기 직전에, ModalContent와 ModalView에게 "이제 열릴 거야!"라고 순서대로 알려줘요. + if (content != null) await currentContent.OnOpen(); + await modalView.OnOpen(currentContent); // ModalView가 content를 기반으로 UI를 최종 구성합니다. + // 📜 이야기: 모든 준비가 끝났어요! 이제 사용자가 버튼을 누를 때까지 기다려요. + // tcs.Task는 '약속 증서'에 결과가 적힐 때까지 기다리는 마법이에요. return await tcs.Task; } - private static void SetupButton(Button button, TMPro.TextMeshProUGUI buttonTextComponent, string text, bool showButton, Action onClickAction) + /// + /// ModalView의 버튼들에 클릭 리스너를 설정합니다. + /// 버튼의 텍스트나 활성화 상태는 ModalView.OnOpen에서 처리됩니다. + /// + private static void SetupButtonClickListeners(ModalView modalView, ModalContent content, UniTaskCompletionSource tcs) { - if (button == null) return; - button.gameObject.SetActive(showButton); - if (showButton) + if (modalView.confirmButton != null) { - if (buttonTextComponent != null && !string.IsNullOrEmpty(text)) buttonTextComponent.text = text; - button.onClick.RemoveAllListeners(); - button.onClick.AddListener(() => onClickAction?.Invoke()); + // 기존 리스너 제거 후 새 리스너 추가 + modalView.confirmButton.onClick.RemoveAllListeners(); + // content.ShowConfirmButton 여부는 ModalView.OnOpen에서 버튼 자체의 활성화로 처리 + modalView.confirmButton.onClick.AddListener(() => _ = HandleModalActionAsync(content, tcs, true, modalView)); + } + + if (modalView.cancelButton != null) + { + modalView.cancelButton.onClick.RemoveAllListeners(); + modalView.cancelButton.onClick.AddListener(() => _ = HandleModalActionAsync(content, tcs, false, modalView)); + } + + if (modalView.closeButton != null) + { + modalView.closeButton.onClick.RemoveAllListeners(); + modalView.closeButton.onClick.AddListener(() => _ = HandleModalActionAsync(content, tcs, false, modalView)); } } - private static void SetButtonsInteractable(ModalView view, bool interactable) - { - if (view == null) return; - if (view.confirmButton != null) view.confirmButton.interactable = interactable; - if (view.cancelButton != null) view.cancelButton.interactable = interactable; - if (view.closeButton != null) view.closeButton.interactable = interactable; - } + /// + /// 🧑‍⚖️ 모달 버튼 클릭 판정 조수예요! 사용자가 모달의 버튼(확인, 취소, 닫기) 중 하나를 누르면 호출돼요. + /// 어떤 버튼을 눌렀는지에 따라 모달을 닫고 결과를 처리해요. + /// + /// 모달이 돌려줄 결과의 타입이에요. + /// 결과를 기록할 '약속 증서'예요. + /// '확인' 버튼을 눌렀으면 true, 그 외 (취소, 닫기)는 false예요. + /// 현재 사용 중인 모달의 ModalView예요. private static async UniTaskVoid HandleModalActionAsync( + ModalContent content, UniTaskCompletionSource tcs, bool isConfirmAction, ModalView modalViewContext) { + // 📜 이야기: 이 함수는 사용자가 버튼을 눌렀을 때 실행돼요. + // 그런데 만약 이전에 처리하던 약속 증서(activeTcs)와 지금 받은 증서(tcs)가 다르거나, + // 모달 창(currentModalInstance)이나 모달 뷰(modalViewContext)가 없다면, 뭔가 잘못된 상황이에요. + // 이럴 때는 아무것도 하지 않고 조용히 돌아가요. (예: 모달이 이미 닫히고 있는 중일 수 있어요) if (tcs != activeTcs || currentModalInstance == null || modalViewContext == null) { return; } - SetButtonsInteractable(modalViewContext, false); + // 📜 이야기: 사용자가 버튼을 하나 눌렀으니, 다른 버튼들은 잠깐 못 누르게 막아요. (실수로 두 번 누르는 것 방지) + modalViewContext.SetAllButtonsInteractable(false); - // GetResult 호출 시점 변경: CleanupCurrentModalResources 내부에서 호출하도록 변경하거나, - // 여기서 결과를 받고 Cleanup에는 결과값 자체를 넘겨주는 방식도 고려 가능. - // 현재는 Cleanup 내부에서 ModalView를 통해 GetResult를 호출하도록 유지. - await CleanupCurrentModalResources(modalViewContext, isConfirmAction, false, tcs, typeof(T)); + // 📜 이야기: 이제 모달을 닫고 뒷정리를 할 시간이에요! + // CleanupCurrentModalResources 조수에게 "이 모달 뷰를 사용했고, 사용자는 '확인'(또는 '취소')을 눌렀어요" 라고 알려주며 뒷정리를 부탁해요. + // 이 뒷정리 과정에서 '약속 증서'에 최종 결과가 기록될 거예요. + await CleanupCurrentModalResources(content, modalViewContext, isConfirmAction, false, tcs, typeof(T)); } + /// + /// 🧹 모달 뒷정리 전문 조수예요! 모달 창과 투명 방패를 없애고, '약속 증서'에 결과를 적어줘요. + /// 모달이 정상적으로 닫힐 때 (버튼 클릭) 또는 예기치 않게 파괴되었을 때 모두 이 조수가 마무리해요. + /// + /// 닫으려는 모달의 내용물(ModalContent)이에요. OnClose 호출 및 결과 반환에 사용될 수 있어요. + /// 닫으려는 모달의 ModalView예요. 결과값을 가져올 때 사용될 수 있어요. + /// 사용자가 '확인'을 눌렀으면 true, '취소'나 '닫기'를 눌렀으면 false예요. + /// 모달이 버튼 클릭이 아니라 외부 요인으로 파괴되었으면 true예요. + /// 결과를 기록할 '약속 증서'예요. 지정하지 않으면 현재 활성화된 증서(activeTcs)를 사용해요. + /// 결과의 타입이에요. 지정하지 않으면 현재 활성화된 타입(activeResultType)을 사용해요. private static async UniTask CleanupCurrentModalResources( + ModalContent content, ModalView modalViewToClose, // ModalView 인스턴스 전달 bool confirmOrCancel, bool wasExternallyDestroyed = false, IUniTaskSource tcsToResolve = null, Type resultTypeForTcs = null) { + // 📜 이야기: 뒷정리를 시작하기 전에, 지금 없애야 할 모달 창과 방패를 기억해둬요. + // 왜냐하면 뒷정리하는 동안 currentModalInstance 같은 전역 변수들이 null로 바뀔 거라서, + // 미리 지역 변수(local variable)에 저장해두지 않으면 나중에 어떤 걸 없애야 할지 헷갈릴 수 있거든요. var blockerInstanceToDestroy = currentBlockerInstance; var modalInstanceToDestroy = currentModalInstance; // 이 시점의 currentModalInstance 사용 + // 📜 이야기: 만약 이 함수를 부를 때 '약속 증서'나 '결과 타입'을 따로 알려주지 않았다면, + // 마법사의 비밀 도구함에 보관된 현재 활성화된 것들을 사용해요. if (tcsToResolve == null) tcsToResolve = activeTcs; if (resultTypeForTcs == null) resultTypeForTcs = activeResultType; + // 📜 이야기: 뒷정리 시에는 현재 활성화된 content를 사용합니다. + // Open 시 설정된 currentContent가 이 content와 동일해야 합니다. + ModalContent contentToClose = content ?? currentContent; + + // 📜 이야기: 이제 이 모달은 끝났으니, 마법사의 비밀 도구함에서 관련 정보들을 모두 지워요. + // 이렇게 해야 다음 모달을 열 때 깨끗한 상태에서 시작할 수 있어요. currentModalInstance = null; currentBlockerInstance = null; activeTcs = null; activeResultType = null; + currentContent = null; // 현재 content 참조도 초기화 - if (blockerInstanceToDestroy != null) + // --- 투명 방패(Blocker) 제거 --- + if (blockerInstanceToDestroy != null) // 없앨 방패가 있다면, { var blockerCG = blockerInstanceToDestroy.GetComponent(); if (blockerCG != null) await FadeUI(blockerCG, 0f, 0.3f, false); UnityEngine.Object.Destroy(blockerInstanceToDestroy); } - if (tcsToResolve != null && tcsToResolve.GetStatus(0) == UniTaskStatus.Pending) + // --- '약속 증서(TaskCompletionSource)'에 결과 기록 --- + // 📜 이야기: 이제 '약속 증서'에 사용자의 선택을 기록할 시간이에요. + // 이 증서에 결과가 적히면, Modal.Open 마법을 쓰고 기다리던 곳에서 그 결과를 받고 다음 일을 할 수 있게 돼요. + if (tcsToResolve != null && tcsToResolve.GetStatus(0) == UniTaskStatus.Pending) // 증서가 있고, 아직 결과가 안 적혔다면, { + + // OnClose 호출을 TrySetResult 직전에 배치 + if (contentToClose != null) await contentToClose.OnClose(); + ModalView viewForOnClose = modalInstanceToDestroy?.GetComponent() ?? modalViewToClose; + if (viewForOnClose != null) + { + await viewForOnClose.OnClose(contentToClose); + } + + + // 만약 결과 타입이 bool (참/거짓) 이라면, if (resultTypeForTcs == typeof(bool)) { + // 증서를 bool 타입용으로 바꿔서 confirmOrCancel 값을 직접 적어요. if (tcsToResolve is UniTaskCompletionSource boolCompletionSource) { boolCompletionSource.TrySetResult(confirmOrCancel); @@ -182,31 +352,40 @@ namespace UVC.UI.Modal else // bool이 아닌 다른 타입의 경우 ModalView.GetResult() 사용 { object resultFromView = null; - if (modalViewToClose != null) // modalViewToClose가 유효할 때만 GetResult 호출 + // modalViewToClose는 파괴되었을 수도 있으므로, modalInstanceToDestroy에서 다시 가져오거나 null 체크 강화 + ModalView viewForGetResult = modalInstanceToDestroy?.GetComponent() ?? modalViewToClose; + if (viewForGetResult != null) { - resultFromView = modalViewToClose.GetResult(); + resultFromView = viewForGetResult.GetResult(); } + // 📜 이야기: 이제 가져온 결과를 '약속 증서'에 적어야 하는데, 타입이 맞는지 잘 확인해야 해요. + // C#의 제네릭(Generic)이라는 기능을 사용해서 어떤 타입의 증서든 처리할 수 있게 만들어요. + // GetType().GetGenericTypeDefinition() 이 부분은 "이 증서가 UniTaskCompletionSource 형태인가요?" 라고 묻는 거예요. if (tcsToResolve.GetType().IsGenericType && tcsToResolve.GetType().GetGenericTypeDefinition() == typeof(UniTaskCompletionSource<>)) { + // 증서의 제네릭 타입(T)을 알아내요. var genericArg = tcsToResolve.GetType().GetGenericArguments()[0]; - if (genericArg == resultTypeForTcs) + if (genericArg == resultTypeForTcs) // 우리가 예상한 결과 타입과 같다면, { object resultToSet; + // ModalView에서 가져온 결과가 있고, 그 타입이 우리가 원하는 타입과 호환된다면, if (resultFromView != null && resultTypeForTcs.IsAssignableFrom(resultFromView.GetType())) { - resultToSet = resultFromView; + resultToSet = resultFromView; // 그 결과를 사용해요. } - else + else // 그렇지 않다면 (결과가 없거나, 타입이 안 맞으면), { - if (resultFromView != null) // 타입은 안맞지만 null은 아닐 때 경고 + if (resultFromView != null) // 타입은 안 맞지만 결과가 있긴 할 때 경고를 남겨요. { ULog.Warning($"[Modal] GetResult() 반환 타입({resultFromView.GetType()})과 모달 결과 타입({resultTypeForTcs})이 일치하지 않습니다. 기본값을 사용합니다."); } - resultToSet = GetDefault(resultTypeForTcs); + resultToSet = GetDefault(resultTypeForTcs); // 해당 타입의 기본값을 사용해요. } + // 📜 이야기: TrySetGenericResultHelper라는 또 다른 조수에게 "이 증서에 이 결과를 적어줘!" 라고 부탁해요. + // 이 조수는 어떤 타입(T)의 증서든 안전하게 결과를 적는 방법을 알고 있어요. (리플렉션 사용) var trySetResultHelperMethod = typeof(Modal).GetMethod(nameof(TrySetGenericResultHelper), System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static) .MakeGenericMethod(resultTypeForTcs); trySetResultHelperMethod.Invoke(null, new object[] { tcsToResolve, resultToSet }); @@ -215,15 +394,11 @@ namespace UVC.UI.Modal } } - if (modalInstanceToDestroy != null) // modalInstanceToDestroy는 Cleanup 시작 시점의 currentModalInstance + // --- 모달 창(Modal Instance) 제거 --- + if (modalInstanceToDestroy != null) // 없앨 모달 창이 있다면, { - // OnClose 호출 - // modalViewToClose가 null일 수 있으므로, modalInstanceToDestroy에서 다시 가져오거나 확인 - ModalView viewForOnClose = modalInstanceToDestroy.GetComponent(); - if (viewForOnClose != null) - { - viewForOnClose.OnClose(); - } + // 만약 모달이 외부 요인으로 파괴된 게 아니라면 (즉, 버튼 클릭 등으로 정상 종료된 거라면), + // 게임 세상에서 완전히 없애요. (외부에서 이미 파괴됐다면 또 없앨 필요 없으니까요) if (!wasExternallyDestroyed) { UnityEngine.Object.Destroy(modalInstanceToDestroy); @@ -231,51 +406,103 @@ namespace UVC.UI.Modal } } - // Helper for setting generic result + /// + /// ✍️ 제네릭 '약속 증서'에 결과 적기 도우미예요. + /// 어떤 타입()의 UniTaskCompletionSource든 안전하게 결과를 설정해줘요. + /// 혹시라도 타입이 안 맞아서 오류가 나면, 에러 메시지를 보여주고 기본값을 대신 넣어줘요. + /// + /// 결과의 타입이에요. + /// 결과를 적을 IUniTaskSource (UniTaskCompletionSource여야 해요). + /// 증서에 적을 실제 결과값이에요. private static void TrySetGenericResultHelper(IUniTaskSource tcs, object result) { - if (tcs is UniTaskCompletionSource genericTcs) + if (tcs is UniTaskCompletionSource genericTcs) // 받은 증서가 UniTaskCompletionSource 타입이 맞는지 확인! { try { + // 결과를 T 타입으로 바꿔서(캐스팅) 증서에 적어요. genericTcs.TrySetResult((T)result); } catch (InvalidCastException ex) { + // 에러를 기록하고, 대신 T 타입의 기본값을 넣어줘요. (프로그램이 멈추는 것 방지) ULog.Error($"[Modal] 결과를 {typeof(T)}로 캐스팅하는데 실패했습니다: {ex.Message}. 기본값을 사용합니다.", ex); genericTcs.TrySetResult(default(T)); } } } - + /// + /// ✨ 모달아, 사라져라! ✨ + /// 지금 화면에 떠 있는 모달 창을 닫고 싶을 때 이 마법 주문을 사용해요. + /// 모달 창이 스르륵 사라질 거예요. + /// + /// 보통은 모달 창 안에 있는 [닫기] 버튼이나 [취소] 버튼을 누르면 자동으로 닫히지만, + /// 특별한 경우에 코드로 직접 모달을 닫아야 할 때 사용할 수 있어요. + /// + /// 모달이 성공적으로 닫히면 알려줘요 (특별한 값을 돌려주진 않아요). + /// + /// + /// // 예를 들어, 5초 뒤에 자동으로 모달을 닫고 싶을 때 + /// async UniTask CloseModalAfter5Seconds() + /// { + /// // (모달이 이미 열려있다고 가정해요) + /// await UniTask.Delay(TimeSpan.FromSeconds(5)); // 5초 기다리기 + /// Debug.Log("시간이 다 되었어요! 모달을 닫습니다."); + /// await Modal.Close(); // 모달 닫기 마법! + /// } + /// + /// public static async UniTask Close() { + // 📜 이야기: 닫을 모달이 없거나, 이미 처리 중인 '약속 증서'가 없다면 할 일이 없어요. if (currentModalInstance == null && activeTcs == null) return; + // 📜 이야기: 현재 활성화된 '약속 증서'와 '결과 타입', 그리고 모달 뷰를 가져와요. + // 이 정보들은 모달을 안전하게 닫고 결과를 처리하는 데 필요해요. IUniTaskSource tcsToClose = activeTcs; Type resultTypeToClose = activeResultType; ModalView view = currentModalInstance?.GetComponent(); + ModalContent contentToClose = currentContent; // 현재 저장된 content 사용 - if (view != null) SetButtonsInteractable(view, false); + // 모달에 버튼들이 있다면, 실수로 또 누르지 못하게 잠시 비활성화해요. + if (view != null) view.SetAllButtonsInteractable(false); // ModalView의 메서드 사용 + // 📜 이야기: 지금부터 모달을 본격적으로 닫을 거니까, + // 현재 모달 인스턴스와 블로커 인스턴스를 지역 변수에 잠시 저장해둬요. + // 그리고 전역 변수들은 null로 만들어서 "지금은 열린 모달 없음!" 상태로 만들어요. var localModalInstance = currentModalInstance; var localBlockerInstance = currentBlockerInstance; + // currentContent는 CleanupCurrentModalResources에서 처리되므로 여기서는 local로 옮기지 않음 currentModalInstance = null; currentBlockerInstance = null; activeTcs = null; activeResultType = null; + currentContent = null; // 현재 content 참조도 초기화 + // 📜 이야기: 만약 닫으려는 '약속 증서'가 있고, 아직 결과가 정해지지 않았다면, + // "취소"된 것으로 처리해요. Close() 마법은 항상 '취소'로 간주하거든요. if (tcsToClose != null && tcsToClose.GetStatus(0) == UniTaskStatus.Pending) { + // OnClose 호출을 결과 설정 직전에 배치 + if (contentToClose != null) await contentToClose.OnClose(); + ModalView viewForOnClose = localModalInstance?.GetComponent(); // localModalInstance 사용 + if (viewForOnClose != null) + { + await viewForOnClose.OnClose(contentToClose); + } + if (resultTypeToClose == typeof(bool) && tcsToClose is UniTaskCompletionSource boolTcs) { - boolTcs.TrySetResult(false); // Close는 항상 false(취소)로 간주 + // 결과 타입이 bool이면 false (취소)를 설정해요. + boolTcs.TrySetResult(false); } + // 결과 타입이 bool이 아니고, 제네릭 UniTaskCompletionSource 형태라면, else if (resultTypeToClose != null && tcsToClose.GetType().IsGenericType && tcsToClose.GetType().GetGenericTypeDefinition() == typeof(UniTaskCompletionSource<>)) { - // Close 시에는 GetResult()를 사용하지 않고 기본값(취소)으로 처리 + // 해당 타입의 기본값으로 결과를 설정해요. (예: int면 0, string이면 null) + // TrySetDefaultResultHelper 조수에게 이 일을 맡겨요. var genericArg = tcsToClose.GetType().GetGenericArguments()[0]; if (genericArg == resultTypeToClose) { @@ -286,74 +513,105 @@ namespace UVC.UI.Modal } } + // --- 투명 방패(Blocker)와 모달 창(Modal Instance) 제거 --- + // 이 부분은 CleanupCurrentModalResources와 비슷하게 동작해요. if (localBlockerInstance != null) { var blockerCG = localBlockerInstance.GetComponent(); - if (blockerCG != null) await FadeUI(blockerCG, 0f, 0.3f, false); - UnityEngine.Object.Destroy(localBlockerInstance); + if (blockerCG != null) await FadeUI(blockerCG, 0f, 0.3f, false); // 부드럽게 사라지게 + UnityEngine.Object.Destroy(localBlockerInstance); // 완전히 제거 } if (localModalInstance != null) { - // OnClose 호출 - ModalView viewForOnClose = localModalInstance.GetComponent(); - if (viewForOnClose != null) - { - viewForOnClose.SendMessage("OnClose", SendMessageOptions.DontRequireReceiver); - } - UnityEngine.Object.Destroy(localModalInstance); + UnityEngine.Object.Destroy(localModalInstance); // 완전히 제거 } } + /// + /// ✍️ 제네릭 '약속 증서'에 기본값 적기 도우미예요. + /// 어떤 타입()의 UniTaskCompletionSource든 해당 타입의 기본값(default)으로 결과를 설정해줘요. + /// Close() 함수에서 모달을 강제로 닫을 때 사용돼요. + /// + /// 결과의 타입이에요. + /// 결과를 적을 IUniTaskSource (UniTaskCompletionSource여야 해요). private static void TrySetDefaultResultHelper(IUniTaskSource tcs) { - if (tcs is UniTaskCompletionSource genericTcs) + if (tcs is UniTaskCompletionSource genericTcs) // 받은 증서가 UniTaskCompletionSource 타입이 맞는지 확인! { - genericTcs.TrySetResult(default(T)); + genericTcs.TrySetResult(default(T)); // T 타입의 기본값 (예: bool은 false, int는 0, 클래스는 null)을 설정해요. } } + /// + /// ✨ UI를 부드럽게 나타나거나 사라지게 하는 마법이에요 (페이드 효과). + /// CanvasGroup의 투명도(alpha)를 조절해서 천천히 보이거나 안 보이게 만들어요. + /// + /// 투명도를 조절할 CanvasGroup 컴포넌트예요. + /// 목표 투명도예요. (0.0 = 완전 투명, 1.0 = 완전 불투명) + /// 페이드 효과에 걸리는 시간(초)이에요. + /// true면 나타나게(페이드 인), false면 사라지게(페이드 아웃) 해요. private static async UniTask FadeUI(CanvasGroup canvasGroup, float targetAlpha, float duration, bool fadeIn) { - if (canvasGroup == null) return; + if (canvasGroup == null) return; // CanvasGroup이 없으면 아무것도 안 해요. + // 📜 이야기: 이 CanvasGroup이 혹시라도 중간에 사라지면(파괴되면) 페이드 효과를 멈춰야 해요. + // 그래서 GetCancellationTokenOnDestroy()로 "파괴되면 알려줘!" 신호를 받아둬요. CancellationToken cancellationToken; try { cancellationToken = canvasGroup.GetCancellationTokenOnDestroy(); } - catch (Exception ex) + catch (Exception ex) // 아주 드물게 이 과정에서 오류가 날 수도 있어서 예외 처리를 해줘요. { - ULog.Error($"[Modal] CanvasGroup에 대한 취소 토큰을 가져오는 중 오류 발생: {ex.Message}", ex); + ULog.Warning($"[Modal] CanvasGroup에 대한 취소 토큰을 가져오는 중 오류 발생: {ex.Message}", ex); return; } - float startAlpha = canvasGroup.alpha; - float time = 0; - if (fadeIn) + float startAlpha = canvasGroup.alpha; // 현재 투명도에서 시작 + float time = 0; // 시간 기록 변수 + if (fadeIn) // 나타나는 효과(페이드 인)라면, { + // 사용자가 이 UI와 상호작용(클릭 등)할 수 있게 하고, 마우스 클릭을 막지 않도록 설정해요. canvasGroup.interactable = true; canvasGroup.blocksRaycasts = true; } + // 📜 이야기: duration(지정된 시간) 동안 투명도를 조금씩 바꿔요. while (time < duration) { + // 만약 중간에 "파괴 신호"가 오거나 CanvasGroup이 사라지면 즉시 멈춰요. if (cancellationToken.IsCancellationRequested || canvasGroup == null) return; + + // Mathf.Lerp는 두 값 사이를 부드럽게 변화시키는 마법이에요. + // time / duration은 현재 진행률 (0.0 ~ 1.0)을 나타내요. canvasGroup.alpha = Mathf.Lerp(startAlpha, targetAlpha, time / duration); - time += Time.deltaTime; + time += Time.deltaTime; // 지난 시간을 더해주고, + // 다음 프레임까지 잠깐 기다렸다가 다시 실행해요. (UniTask.Yield) + // SuppressCancellationThrow는 "파괴 신호"가 와도 오류를 내지 말고 조용히 멈추라는 뜻이에요. await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken).SuppressCancellationThrow(); } + + // 시간이 다 되었거나 중간에 멈췄을 때, 최종적으로 목표 투명도로 설정해요. if (cancellationToken.IsCancellationRequested || canvasGroup == null) return; canvasGroup.alpha = targetAlpha; - if (!fadeIn) + if (!fadeIn) // 사라지는 효과(페이드 아웃)였다면, { + // 이제 이 UI는 보이지 않으니 상호작용도 막고, 마우스 클릭도 막아요. canvasGroup.interactable = false; canvasGroup.blocksRaycasts = false; } } + /// + /// 🎁 특정 타입의 '기본값'을 돌려주는 작은 도우미예요. + /// 값 타입(struct, int, bool 등)이면 Activator.CreateInstance로 기본 인스턴스를 만들고, + /// 참조 타입(class)이면 null을 돌려줘요. + /// + /// 기본값을 알고 싶은 타입 정보예요. + /// 해당 타입의 기본값이에요. private static object GetDefault(Type type) { - if (type == null) return null; - if (type.IsValueType) return Activator.CreateInstance(type); - return null; + if (type == null) return null; // 타입 정보가 없으면 null + if (type.IsValueType) return Activator.CreateInstance(type); // 값 타입이면 기본 인스턴스 생성 (예: int는 0, bool은 false) + return null; // 참조 타입이면 null } } } \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/Modal/ModalContent.cs b/Assets/Scripts/UVC/UI/Modal/ModalContent.cs index e4371b9c..45b581a2 100644 --- a/Assets/Scripts/UVC/UI/Modal/ModalContent.cs +++ b/Assets/Scripts/UVC/UI/Modal/ModalContent.cs @@ -1,24 +1,47 @@ -using UVC.Locale; // LocalizationManager 사용을 위해 추가 +using Cysharp.Threading.Tasks; +using UVC.Locale; +using UVC.Log; // LocalizationManager 사용을 위해 추가 namespace UVC.UI.Modal { + /// + /// 📜 모달 창에 어떤 내용을 보여줄지, 어떻게 행동할지 정하는 '레시피' 또는 '주문서' 같은 친구예요. + /// 이 클래스의 객체를 만들어서 Modal.Open()에 전달하면, 여기에 적힌 대로 모달 창이 만들어져요. + /// + /// + /// + /// // "MyAwesomeModalPrefab"이라는 디자인을 사용하는 모달을 위한 레시피를 만들어요. + /// var myRecipe = new ModalContent("Prefabs/UI/MyAwesomeModalPrefab") + /// { + /// Title = "새로운 모험!", // 모달 창 제목 + /// Message = "모험을 시작할 준비가 되었나요?", // 모달 창 메시지 + /// ConfirmButtonText = "네, 갑시다!", // 확인 버튼 글자 + /// ShowCancelButton = false // 취소 버튼은 안 보여줄래요. + /// }; + /// + /// // 이렇게 만든 레시피(myRecipe)를 Modal.Open()에 전달하면 모달이 뿅 나타나요! + /// // bool userChoseConfirm = await Modal.Open<bool>(myRecipe); + /// + /// public class ModalContent { /// - /// 모달 창의 제목입니다. 다국어 지원을 위해 직접 설정하거나, 생성 후 별도로 설정할 수 있습니다. + /// 🏷️ 모달 창의 제목이에요. 여기에 글자를 적으면 모달 창 맨 위에 크게 보여요. + /// 예: "알림", "게임 저장", "친구 요청" /// public string Title { get; set; } /// - /// 모달 창에 표시될 메시지 또는 본문 내용입니다. 다국어 지원을 위해 직접 설정하거나, 생성 후 별도로 설정할 수 있습니다. + /// 💬 모달 창에 보여줄 주요 메시지 내용이에요. 사용자에게 전달하고 싶은 말을 여기에 적어요. + /// 예: "게임 설정을 저장했습니다.", "정말로 아이템을 구매하시겠어요?" /// public string Message { get; set; } private string _confirmButtonText; /// - /// 확인 버튼에 표시될 텍스트입니다. - /// 기본적으로 "modal_confirm_button" 키를 사용하여 에서 번역된 문자열을 가져옵니다. - /// 직접 설정하여 이 기본값을 재정의할 수 있습니다. + /// ✅ '확인' 버튼에 보여줄 글자예요. + /// 특별히 정해주지 않으면 기본적으로 "확인" (또는 설정된 언어에 맞게)이라고 나와요. + /// 직접 "네", "저장하기", "출발!" 처럼 원하는 글자로 바꿀 수 있어요. /// public string ConfirmButtonText { @@ -37,9 +60,9 @@ namespace UVC.UI.Modal private string _cancelButtonText; /// - /// 취소 버튼에 표시될 텍스트입니다. - /// 기본적으로 "modal_cancel_button" 키를 사용하여 에서 번역된 문자열을 가져옵니다. - /// 직접 설정하여 이 기본값을 재정의할 수 있습니다. + /// ❌ '취소' 버튼에 보여줄 글자예요. + /// 특별히 정해주지 않으면 기본적으로 "취소" (또는 설정된 언어에 맞게)이라고 나와요. + /// 직접 "아니요", "닫기", "나중에" 처럼 원하는 글자로 바꿀 수 있어요. /// public string CancelButtonText { @@ -57,24 +80,43 @@ namespace UVC.UI.Modal } /// - /// 확인 버튼 표시 여부를 결정합니다. (기본값: true) + /// 👍 '확인' 버튼을 화면에 보여줄지 말지 결정해요. (기본값: true, 보여줌) + /// false로 바꾸면 '확인' 버튼이 사라져요. /// public bool ShowConfirmButton { get; set; } = true; /// - /// 취소 버튼 표시 여부를 결정합니다. (기본값: true) + /// 👎 '취소' 버튼을 화면에 보여줄지 말지 결정해요. (기본값: true, 보여줌) + /// false로 바꾸면 '취소' 버튼이 사라져요. /// public bool ShowCancelButton { get; set; } = true; /// - /// 모달 콘텐츠로 사용될 프리팹의 경로입니다. + /// 👎 '닫기' 버튼을 화면에 보여줄지 말지 결정해요. (기본값: true, 보여줌) + /// false로 바꾸면 '닫기' 버튼이 사라져요. + /// + public bool ShowCloseButton { get; set; } = true; + + /// + /// 🎨 모달 창의 '디자인 도면' 파일이 어디 있는지 알려주는 경로예요. + /// Unity 에디터에서 만들어둔 프리팹(Prefab) 파일의 경로를 적어줘야 해요. + /// 예: "Prefabs/UI/MyCustomModal" /// public string PrefabPath { get; private set; } /// - /// ModalContent 생성자입니다. + /// 🧑‍🍳 ModalContent 레시피를 만드는 방법이에요. (생성자) + /// 모달 창을 어떤 디자인으로 만들지 알려줘야 해요. /// - /// 로드할 프리팹의 경로입니다. 이 프리팹 내부에 UI 요소들이 구성되어 있어야 합니다. + /// 사용할 모달 창 디자인(프리팹) 파일의 경로예요. + /// + /// + /// // "CommonModalPrefab" 디자인을 사용하는 레시피 만들기 + /// var content = new ModalContent("Prefabs/UI/CommonModalPrefab"); + /// content.Title = "안내"; + /// content.Message = "이것은 일반적인 안내 모달입니다."; + /// + /// public ModalContent(string prefabPath) { PrefabPath = prefabPath; @@ -83,9 +125,10 @@ namespace UVC.UI.Modal } /// - /// 특정 다국어 키를 사용하여 확인 버튼 텍스트를 설정합니다. + /// 🔑 '확인' 버튼의 글자를 다국어 키를 사용해서 설정해요. + /// 게임이 여러 언어를 지원할 때 유용해요. /// - /// 사용할 다국어 키입니다. + /// 미리 정해둔 다국어 키 (예: "ui_button_yes", "action_save") public void SetConfirmButtonTextFromKey(string localizationKey) { // 직접 값을 설정하는 대신, 키를 저장하고 getter에서 처리하도록 할 수도 있으나, @@ -105,9 +148,9 @@ namespace UVC.UI.Modal } /// - /// 특정 다국어 키를 사용하여 취소 버튼 텍스트를 설정합니다. + /// 🔑 '취소' 버튼의 글자를 다국어 키를 사용해서 설정해요. /// - /// 사용할 다국어 키입니다. + /// 미리 정해둔 다국어 키 (예: "ui_button_no", "action_cancel") public void SetCancelButtonTextFromKey(string localizationKey) { if (LocalizationManager.Instance != null) @@ -119,5 +162,65 @@ namespace UVC.UI.Modal _cancelButtonText = $"[{localizationKey}]"; } } + + /// + /// 🚀 모달 창이 화면에 나타나기 *직전*에 이 레시피가 할 일을 정해요. (비동기 작업 가능) + /// 예를 들어, 모달에 필요한 데이터를 미리 불러오거나 특별한 준비를 할 수 있어요. + /// `ModalView.OnOpen()` 보다 먼저 호출돼요. + /// + /// + /// 이 메서드를 상속받아서 특별한 준비 작업을 추가할 수 있어요: + /// + /// public class MySpecialModalContent : ModalContent + /// { + /// public MySpecialModalContent(string prefabPath) : base(prefabPath) { } + /// + /// public override async UniTask OnOpen() + /// { + /// await base.OnOpen(); // 부모 클래스의 OnOpen도 호출해주는 게 좋아요. + /// ULog.Debug("나만의 특별한 모달 내용 준비 시작!"); + /// // 예: await LoadSomeDataForTheModal(); + /// ULog.Debug("나만의 특별한 모달 내용 준비 완료!"); + /// } + /// } + /// + /// + public virtual async UniTask OnOpen() + { + // 기본적으로는 아무것도 하지 않아요. 필요하면 상속받아서 내용을 채워주세요! + //ULog.Debug($"[ModalContent] '{Title}' OnOpen called."); + await UniTask.CompletedTask; + } + + /// + /// 🎬 모달 창이 화면에서 사라지기 *직전*에 이 레시피가 할 일을 정해요. (비동기 작업 가능) + /// 예를 들어, 모달에서 사용했던 자원을 정리하거나 특별한 마무리를 할 수 있어요. + /// `ModalView.OnClose()` 보다 먼저 호출돼요. + /// + /// + /// 이 메서드를 상속받아서 특별한 마무리 작업을 추가할 수 있어요: + /// + /// public class MySpecialModalContent : ModalContent + /// { + /// public MySpecialModalContent(string prefabPath) : base(prefabPath) { } + /// + /// // ... OnOpen 재정의 ... + /// + /// public override async UniTask OnClose() + /// { + /// ULog.Debug("나만의 특별한 모달 내용 마무리 시작!"); + /// // 예: await ReleaseSomeDataUsedInModal(); + /// ULog.Debug("나만의 특별한 모달 내용 마무리 완료!"); + /// await base.OnClose(); // 부모 클래스의 OnClose도 호출해주는 게 좋아요. + /// } + /// } + /// + /// + public virtual async UniTask OnClose() + { + // 기본적으로는 아무것도 하지 않아요. 필요하면 상속받아서 내용을 채워주세요! + //ULog.Debug($"[ModalContent] '{Title}' OnClose called."); + await UniTask.CompletedTask; + } } } \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/Modal/ModalView.cs b/Assets/Scripts/UVC/UI/Modal/ModalView.cs index 12b1586e..e391bbfe 100644 --- a/Assets/Scripts/UVC/UI/Modal/ModalView.cs +++ b/Assets/Scripts/UVC/UI/Modal/ModalView.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Cysharp.Threading.Tasks; using TMPro; using UnityEngine; using UnityEngine.UI; @@ -11,62 +7,275 @@ using UVC.Log; namespace UVC.UI.Modal { /// - /// 모달 프리팹에 부착되어 UI 요소들을 관리하는 컴포넌트입니다. + /// 🖼️ 모달 창의 실제 '모습'을 담당하는 친구예요. Unity 에디터에서 만든 UI 요소들(버튼, 글상자 등)을 가지고 있어요. + /// `ModalContent`라는 레시피를 받아서, 그 내용대로 화면에 그림을 그려주는 역할을 해요. + /// 이 스크립트는 모달로 사용할 프리팹(Prefab)의 가장 바깥쪽 부모 게임 오브젝트에 붙여줘야 해요. /// + /// + /// 만약 입력창이 있는 특별한 모달을 만들고 싶다면, 이 ModalView를 상속받아서 만들 수 있어요: + /// + /// public class InputModalView : ModalView + /// { + /// [Header("My Special UI")] + /// public TMP_InputField myInputField; // Unity 에디터에서 연결해줘야 해요. + /// private string _inputValue = ""; + /// + /// public override async UniTask OnOpen(ModalContent content) + /// { + /// await base.OnOpen(content); // 부모의 OnOpen을 먼저 호출해서 기본 UI를 설정해요. + /// ULog.Debug("입력 모달이 열렸어요! 입력창을 초기화합니다."); + /// if (myInputField != null) + /// { + /// myInputField.text = ""; // 입력창 비우기 + /// // 입력창에 변화가 있을 때마다 _inputValue를 업데이트하도록 설정할 수 있어요. + /// myInputField.onValueChanged.AddListener(OnInputChanged); + /// } + /// } + /// + /// private void OnInputChanged(string newValue) + /// { + /// _inputValue = newValue; + /// ULog.Debug($"입력된 값: {newValue}"); + /// } + /// + /// public override object GetResult() + /// { + /// // 이 모달이 닫힐 때, 입력된 글자를 결과로 돌려줘요. + /// return _inputValue; + /// } + /// + /// public override async UniTask OnClose(ModalContent content) + /// { + /// ULog.Debug("입력 모달이 닫힙니다."); + /// if (myInputField != null) + /// { + /// myInputField.onValueChanged.RemoveListener(OnInputChanged); // 리스너 정리 + /// } + /// await base.OnClose(content); + /// } + /// } + /// + /// // 이 InputModalView를 사용하는 방법: + /// // 1. InputModalView 스크립트가 붙어있는 프리팹을 만들고, myInputField를 연결해요. + /// // 2. ModalContent 만들 때 이 프리팹 경로를 사용해요. + /// // var inputContent = new ModalContent("Prefabs/UI/MyInputModalPrefab") { Title = "이름 입력", Message = "이름을 입력해주세요." }; + /// // string enteredName = await Modal.Open(inputContent); + /// // if (!string.IsNullOrEmpty(enteredName)) { ULog.Debug($"환영합니다, {enteredName}님!"); } + /// + /// public class ModalView : MonoBehaviour { [Header("UI Elements")] + /// + /// 🏷️ 모달 창의 제목을 보여줄 글상자(TextMeshProUGUI)예요. + /// Unity 에디터의 인스펙터 창에서 실제 UI 요소를 끌어다 연결해줘야 해요. + /// public TextMeshProUGUI titleText; + /// + /// 💬 모달 창의 주요 메시지를 보여줄 글상자예요. 이것도 연결해주세요! + /// public TextMeshProUGUI messageText; + /// + /// ✅ '확인' 버튼이에요. 연결 필수! + /// public Button confirmButton; - public TextMeshProUGUI confirmButtonText; // confirmButton 내의 Text 컴포넌트 + /// + /// 확인 버튼 안에 있는 글상자예요. 확인 버튼 글자를 바꿀 때 사용돼요. + /// + public TextMeshProUGUI confirmButtonText; + /// + /// ❌ '취소' 버튼이에요. 이것도 연결해주세요! + /// public Button cancelButton; - public TextMeshProUGUI cancelButtonText; // cancelButton 내의 Text 컴포넌트 + /// + /// 취소 버튼 안에 있는 글상자예요. 취소 버튼 글자를 바꿀 때 사용돼요. + /// + public TextMeshProUGUI cancelButtonText; + /// + /// ✖️ 모달 창을 닫는 (보통 오른쪽 위에 있는 X 모양) 버튼이에요. + /// public Button closeButton; // 닫기 버튼 // 필요에 따라 다른 UI 요소들을 추가할 수 있습니다. // 예: public Image backgroundImage; // 예: public InputField inputField; - // Modal.cs에서 호출 가능하도록 public으로 변경 (또는 protected internal) - public virtual void OnOpen() // virtual로 변경하여 하위 클래스에서 재정의 가능하도록 함 + /// + /// 🚀 모달 창이 화면에 나타날 때 `Modal` 클래스가 호출하는 마법이에요! (비동기 작업 가능) + /// `ModalContent` 레시피를 받아서, 이 `ModalView`의 UI 요소들(제목, 메시지, 버튼 등)을 레시피대로 설정해요. + /// `ModalContent.OnOpen()`이 호출된 *후에* 실행돼요. + /// + /// 모달에 보여줄 내용과 설정을 담은 '레시피' (`ModalContent` 객체)예요. + public virtual async UniTask OnOpen(ModalContent content) { - // 모달이 열릴 때 실행할 로직 - ULog.Debug($"[ModalView] {gameObject.name} OnOpen called."); - //confirmButton, cancelButton 버튼 위치 가운 대로 설정. - if (confirmButton.IsActive() && !cancelButton.IsActive()) + //ULog.Debug($"[ModalView] {gameObject.name} OnOpen called."); + + // ModalContent 레시피에 적힌 대로 UI 요소들을 설정해요. + if (titleText != null && content != null) + { + titleText.text = content.Title; + } + if (messageText != null && content != null) + { + messageText.text = content.Message; + } + + // 확인 버튼 설정 + if (confirmButton != null && content != null) + { + confirmButton.gameObject.SetActive(content.ShowConfirmButton); + if (content.ShowConfirmButton && confirmButtonText != null && !string.IsNullOrEmpty(content.ConfirmButtonText)) + { + confirmButtonText.text = content.ConfirmButtonText; + } + } + + // 취소 버튼 설정 + if (cancelButton != null && content != null) + { + cancelButton.gameObject.SetActive(content.ShowCancelButton); + if (content.ShowCancelButton && cancelButtonText != null && !string.IsNullOrEmpty(content.CancelButtonText)) + { + cancelButtonText.text = content.CancelButtonText; + } + } + + // 닫기 버튼 설정 + if (closeButton != null && content != null) + { + closeButton.gameObject.SetActive(content.ShowCloseButton); + } + + + // 버튼 위치를 예쁘게 조정해요 (예: 버튼이 하나만 있으면 가운데로). + AdjustButtonPositions(); + await UniTask.CompletedTask; // 비동기 메서드라서 마지막에 이걸 붙여줘요. + } + + /// + /// 📐 활성화된 버튼(확인/취소)의 수에 따라 버튼 위치를 보기 좋게 조정해요. + /// 예를 들어, 버튼이 하나만 있다면 화면 가운데에 오도록 할 수 있어요. + /// 이 메서드는 `OnOpen`에서 호출돼요. + /// + protected virtual void AdjustButtonPositions() + { + bool isConfirmActive = confirmButton != null && confirmButton.gameObject.activeSelf; + bool isCancelActive = cancelButton != null && cancelButton.gameObject.activeSelf; + + // 확인 버튼만 활성화되어 있다면, + if (isConfirmActive && !isCancelActive) { RectTransform confirmButtonRect = confirmButton.GetComponent(); if (confirmButtonRect != null) { + // 예시: 확인 버튼을 부모 UI 요소의 가로 중앙으로 이동시켜요. + // (정확한 값은 여러분의 UI 레이아웃에 따라 달라질 수 있어요!) + confirmButtonRect.anchorMin = new Vector2(0.5f, confirmButtonRect.anchorMin.y); + confirmButtonRect.anchorMax = new Vector2(0.5f, confirmButtonRect.anchorMax.y); + confirmButtonRect.pivot = new Vector2(0.5f, confirmButtonRect.pivot.y); confirmButtonRect.anchoredPosition = new Vector2(0, confirmButtonRect.anchoredPosition.y); } } - else if (!confirmButton.IsActive() && cancelButton.IsActive()) + // 취소 버튼만 활성화되어 있다면, + else if (!isConfirmActive && isCancelActive) { RectTransform cancelButtonRect = cancelButton.GetComponent(); if (cancelButtonRect != null) { + // 예시: 취소 버튼도 부모 UI 요소의 가로 중앙으로 이동시켜요. + cancelButtonRect.anchorMin = new Vector2(0.5f, cancelButtonRect.anchorMin.y); + cancelButtonRect.anchorMax = new Vector2(0.5f, cancelButtonRect.anchorMax.y); + cancelButtonRect.pivot = new Vector2(0.5f, cancelButtonRect.pivot.y); cancelButtonRect.anchoredPosition = new Vector2(0, cancelButtonRect.anchoredPosition.y); } } + // 두 버튼이 모두 보이거나 모두 안 보일 때는 특별히 위치를 바꾸지 않아요 (기본 레이아웃 사용). + // 필요하다면 이 부분에 다른 정렬 로직을 추가할 수도 있어요! } - // Modal.cs에서 호출 가능하도록 public으로 변경 (또는 protected internal) - public virtual void OnClose() // virtual로 변경하여 하위 클래스에서 재정의 가능하도록 함 + + /// + /// 🎬 모달 창이 화면에서 사라질 때 `Modal` 클래스가 호출하는 마법이에요! (비동기 작업 가능) + /// `ModalContent.OnClose()`가 호출된 *후에* 실행돼요. + /// 모달이 닫히면서 특별히 정리해야 할 작업이 있다면 여기에 작성해요. + /// + /// 이 모달을 열 때 사용했던 '레시피' (`ModalContent` 객체)예요. + public virtual async UniTask OnClose(ModalContent content) { - // 모달이 닫힐 때 실행할 로직 - ULog.Debug($"[ModalView] {gameObject.name} OnClose called."); + //ULog.Debug($"[ModalView] {gameObject.name} OnClose called."); + // 예: 모달에서 사용했던 리소스를 해제하거나, UI 상태를 초기화하는 코드를 여기에 넣을 수 있어요. + await UniTask.CompletedTask; } - public virtual object GetResult() // virtual로 변경하여 하위 클래스에서 재정의 가능하도록 함 + /// + /// 🎁 모달 창이 닫힐 때, 이 모달이 어떤 '결과'를 만들었는지 알려주는 함수예요. + /// 기본적으로는 아무것도 안 알려줘요 (`null` 반환). + /// 만약 모달에서 사용자가 뭔가를 선택하거나 입력했다면, 이 함수를 **재정의(override)**해서 + /// 그 선택/입력 값을 돌려주도록 만들 수 있어요. + /// `Modal.Open()`를 호출할 때 `T`에 지정한 타입으로 이 결과가 변환돼요. + /// + /// 모달의 처리 결과예요. (예: 사용자가 입력한 글자, 선택한 아이템, 또는 그냥 true/false) + /// + /// 예를 들어, '예'/'아니오'를 선택하는 간단한 확인 모달이라면 이렇게 할 수 있어요: + /// + /// public class ConfirmModalView : ModalView + /// { + /// private bool _wasConfirmed = false; + /// + /// // (OnOpen 등 다른 메서드들은 필요에 따라 구현) + /// + /// // 확인 버튼이 눌렸을 때 호출될 메서드 (Modal.cs에서 연결해줌) + /// public void HandleConfirm() + /// { + /// _wasConfirmed = true; + /// // 실제로는 Modal.cs의 HandleModalActionAsync가 호출되어 모달이 닫힙니다. + /// } + /// + /// // 취소 버튼이 눌렸을 때 호출될 메서드 + /// public void HandleCancel() + /// { + /// _wasConfirmed = false; + /// } + /// + /// public override object GetResult() + /// { + /// // 사용자가 '확인'을 눌렀는지 여부를 bool 값으로 반환해요. + /// return _wasConfirmed; + /// } + /// } + /// // Modal.Open(...) 이렇게 호출하면, GetResult()가 반환한 bool 값을 받을 수 있어요. + /// + /// 또는, 입력 필드가 있는 모달이라면 입력된 텍스트를 반환할 수 있어요: + /// + /// public class InputModalView : ModalView + /// { + /// public TMP_InputField inputField; + /// // (OnOpen에서 inputField 초기화 및 이벤트 연결) + /// + /// public override object GetResult() + /// { + /// return inputField != null ? inputField.text : string.Empty; + /// } + /// } + /// // Modal.Open(...) 이렇게 호출하면, 입력된 문자열을 받을 수 있어요. + /// + /// + public virtual object GetResult() { - // 기본적으로는 null을 반환. 특정 모달 뷰에서 이 메서드를 재정의하여 - // 입력 필드의 값, 선택된 항목 등 구체적인 결과 데이터를 반환하도록 구현합니다. - // 예를 들어, 확인 버튼 시 true, 취소 버튼 시 false를 반환하도록 할 수도 있습니다. - // 이 값은 Modal.Open의 T 타입으로 변환 시도됩니다. return null; } + /// + /// 💡 모달 안에 있는 모든 주요 버튼들(확인, 취소, 닫기)을 한꺼번에 클릭할 수 있게 하거나 못하게 만들어요. + /// 예를 들어, 모달이 뭔가 중요한 작업을 처리하는 동안에는 버튼을 잠시 못 누르게 할 때 사용해요. + /// + /// true로 설정하면 버튼들을 누를 수 있고, false면 못 눌러요. + public virtual void SetAllButtonsInteractable(bool interactable) + { + if (confirmButton != null) confirmButton.interactable = interactable; + if (cancelButton != null) cancelButton.interactable = interactable; + if (closeButton != null) closeButton.interactable = interactable; + } + } } diff --git a/Assets/Scripts/UVC/UI/ToolBar.meta b/Assets/Scripts/UVC/UI/ToolBar.meta new file mode 100644 index 00000000..b365a7b3 --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 48af2b9eac1a64f438a95b8145971a9b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/UVC/UI/ToolBar/IToolbarItem.cs b/Assets/Scripts/UVC/UI/ToolBar/IToolbarItem.cs new file mode 100644 index 00000000..8011e66d --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/IToolbarItem.cs @@ -0,0 +1,7 @@ +namespace UVC.UI.ToolBar +{ + /// + /// 툴바에 추가될 수 있는 모든 항목(버튼, 구분선 등)의 기본 인터페이스입니다. + /// + public interface IToolbarItem { } +} diff --git a/Assets/Scripts/UVC/UI/ToolBar/IToolbarItem.cs.meta b/Assets/Scripts/UVC/UI/ToolBar/IToolbarItem.cs.meta new file mode 100644 index 00000000..de92cc9c --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/IToolbarItem.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b28bebc44774c6f4f95cb5e612baef9f \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/ToolBar/Toolbar.cs b/Assets/Scripts/UVC/UI/ToolBar/Toolbar.cs new file mode 100644 index 00000000..d2ca2773 --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/Toolbar.cs @@ -0,0 +1,77 @@ +namespace UVC.UI.ToolBar +{ + /// + /// 툴바의 전체적인 컨테이너 및 관리 클래스입니다. + /// IToolbarItem 객체들을 동적으로 추가하고 관리합니다. + /// + public class Toolbar + { + public System.Collections.Generic.List Items { get; private set; } + private System.Collections.Generic.Dictionary _radioGroups; + + public Toolbar() + { + Items = new System.Collections.Generic.List(); + _radioGroups = new System.Collections.Generic.Dictionary(); + } + + 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 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 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 생성, 이벤트 연결 등) + } +} diff --git a/Assets/Scripts/UVC/UI/ToolBar/Toolbar.cs.meta b/Assets/Scripts/UVC/UI/ToolBar/Toolbar.cs.meta new file mode 100644 index 00000000..7c067a74 --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/Toolbar.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2d72ec0e29e760440b1badbdf4b051f2 \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/ToolBar/ToolbarButtonBase.cs b/Assets/Scripts/UVC/UI/ToolBar/ToolbarButtonBase.cs new file mode 100644 index 00000000..8b15c931 --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/ToolbarButtonBase.cs @@ -0,0 +1,82 @@ +using System; +using UnityEngine; + +namespace UVC.UI.ToolBar +{ + /// + /// 모든 버튼의 기본 추상 클래스입니다. + /// 공통적인 속성 (예: 텍스트, 아이콘, 활성화 상태) 및 동작을 정의합니다. + /// + 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(); + } + } +} diff --git a/Assets/Scripts/UVC/UI/ToolBar/ToolbarButtonBase.cs.meta b/Assets/Scripts/UVC/UI/ToolBar/ToolbarButtonBase.cs.meta new file mode 100644 index 00000000..51d32b91 --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/ToolbarButtonBase.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 49e156554491e3c4fb49243701695feb \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/ToolBar/ToolbarExpandableButton.cs b/Assets/Scripts/UVC/UI/ToolBar/ToolbarExpandableButton.cs new file mode 100644 index 00000000..1475cdf9 --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/ToolbarExpandableButton.cs @@ -0,0 +1,60 @@ + + +using System; +using System.Collections.Generic; + +namespace UVC.UI.ToolBar +{ + /// + /// 클릭 시 하위 버튼 그룹을 확장하여 보여주는 버튼입니다. + /// 하위 버튼 선택 시, 주 버튼의 내용이 업데이트될 수 있습니다. + /// + public class ToolbarExpandableButton : ToolbarButtonBase + { + public enum ExpansionDirection { Horizontal, Vertical } + + public List SubButtons { get; private set; } + public ExpansionDirection Direction { get; set; } = ExpansionDirection.Vertical; + public Action OnSubButtonSelected { get; set; } + + public ToolbarExpandableButton() + { + SubButtons = new List(); + } + + 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 외 다른 상태도 변경된다면 필요. + } + } + } + } +} diff --git a/Assets/Scripts/UVC/UI/ToolBar/ToolbarExpandableButton.cs.meta b/Assets/Scripts/UVC/UI/ToolBar/ToolbarExpandableButton.cs.meta new file mode 100644 index 00000000..f689937e --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/ToolbarExpandableButton.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c71e02eeca4e94e4b8dedd8f9fb7e4a7 \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/ToolBar/ToolbarManager.cs b/Assets/Scripts/UVC/UI/ToolBar/ToolbarManager.cs new file mode 100644 index 00000000..350a7dc5 --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/ToolbarManager.cs @@ -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; + // } + } +} diff --git a/Assets/Scripts/UVC/UI/ToolBar/ToolbarManager.cs.meta b/Assets/Scripts/UVC/UI/ToolBar/ToolbarManager.cs.meta new file mode 100644 index 00000000..65a3f0ba --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/ToolbarManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2c8047638e9a7ca4495254682609d580 \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/ToolBar/ToolbarRadioButton.cs b/Assets/Scripts/UVC/UI/ToolBar/ToolbarRadioButton.cs new file mode 100644 index 00000000..494d9f27 --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/ToolbarRadioButton.cs @@ -0,0 +1,32 @@ +namespace UVC.UI.ToolBar +{ + /// + /// 라디오 버튼 그룹 내에서 사용되는 버튼입니다. + /// 그룹 내 하나의 버튼만 선택될 수 있습니다. + /// + 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에서 이미 호출될 수 있으므로 중복 호출을 피하거나 의도에 맞게 조정합니다. + } + } + } +} diff --git a/Assets/Scripts/UVC/UI/ToolBar/ToolbarRadioButton.cs.meta b/Assets/Scripts/UVC/UI/ToolBar/ToolbarRadioButton.cs.meta new file mode 100644 index 00000000..0fbedd82 --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/ToolbarRadioButton.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 451c776768fed71479e8c7a4a73818ea \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/ToolBar/ToolbarRadioButtonGroup.cs b/Assets/Scripts/UVC/UI/ToolBar/ToolbarRadioButtonGroup.cs new file mode 100644 index 00000000..871b58e2 --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/ToolbarRadioButtonGroup.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace UVC.UI.ToolBar +{ + /// + /// 라디오 버튼들을 그룹으로 관리하여 하나만 선택되도록 합니다. + /// ToolbarRadioButtonGroup, ToolbarExpandableButton 클래스는 이전 제안과 거의 동일하게 유지하되, + /// 상태 변경 시 NotifyStateChanged() 호출을 고려할 수 있습니다. + /// 예를 들어 ToolbarExpandableButton에서 SelectSubButton 후 주 버튼의 Text, Icon이 변경되면 NotifyStateChanged() 호출 + /// + public class ToolbarRadioButtonGroup + { + private List _buttons = new List(); + 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로 대체 가능 + } + } + } + } +} diff --git a/Assets/Scripts/UVC/UI/ToolBar/ToolbarRadioButtonGroup.cs.meta b/Assets/Scripts/UVC/UI/ToolBar/ToolbarRadioButtonGroup.cs.meta new file mode 100644 index 00000000..ec990948 --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/ToolbarRadioButtonGroup.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a6099bb327827504c8c4e36c74c7c507 \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/ToolBar/ToolbarSeparator.cs b/Assets/Scripts/UVC/UI/ToolBar/ToolbarSeparator.cs new file mode 100644 index 00000000..d65f5100 --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/ToolbarSeparator.cs @@ -0,0 +1,7 @@ +namespace UVC.UI.ToolBar +{ + /// + /// 툴바 구분선을 나타냅니다. + /// + public class ToolbarSeparator : IToolbarItem { } +} diff --git a/Assets/Scripts/UVC/UI/ToolBar/ToolbarSeparator.cs.meta b/Assets/Scripts/UVC/UI/ToolBar/ToolbarSeparator.cs.meta new file mode 100644 index 00000000..85d7001e --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/ToolbarSeparator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 41943a25123704b4f82ec6417863d158 \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/ToolBar/ToolbarStandardButton.cs b/Assets/Scripts/UVC/UI/ToolBar/ToolbarStandardButton.cs new file mode 100644 index 00000000..96526350 --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/ToolbarStandardButton.cs @@ -0,0 +1,16 @@ +namespace UVC.UI.ToolBar +{ + /// + /// 일반적인 클릭 버튼입니다. + /// + public class ToolbarStandardButton : ToolbarButtonBase + { + public override void ExecuteClick() + { + if (IsEnabled && OnClick != null) + { + OnClick.Invoke(); + } + } + } +} diff --git a/Assets/Scripts/UVC/UI/ToolBar/ToolbarStandardButton.cs.meta b/Assets/Scripts/UVC/UI/ToolBar/ToolbarStandardButton.cs.meta new file mode 100644 index 00000000..8779191c --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/ToolbarStandardButton.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0d7cc1da90c7117449ff98ba4600c3ce \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/ToolBar/ToolbarToggleButton.cs b/Assets/Scripts/UVC/UI/ToolBar/ToolbarToggleButton.cs new file mode 100644 index 00000000..295847c6 --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/ToolbarToggleButton.cs @@ -0,0 +1,38 @@ +using System; + +namespace UVC.UI.ToolBar +{ + /// + /// 클릭할 때마다 선택/해제 상태가 변경되는 토글 버튼입니다. + /// + public class ToolbarToggleButton : ToolbarButtonBase + { + public event Action OnToggleStateChanged; // IsSelected 변경 시 IsSelected 값을 전달하는 이벤트 + + private bool _isSelected; + public bool IsSelected + { + get => _isSelected; + set + { + if (_isSelected != value) + { + _isSelected = value; + OnToggleStateChanged?.Invoke(_isSelected); // IsSelected 값과 함께 이벤트 발생 + NotifyStateChanged(); // 일반 상태 변경 이벤트도 발생 + } + } + } + public Action OnToggle { get; set; } + + public override void ExecuteClick() + { + if (IsEnabled) + { + IsSelected = !IsSelected; // IsSelected의 setter가 OnStateChanged를 호출 + OnClick?.Invoke(); // OnClick은 상태 변경과 별개로 클릭 시 항상 호출되도록 할 수 있음 + OnToggle?.Invoke(IsSelected); // 기존 OnToggle 로직 유지 + } + } + } +} diff --git a/Assets/Scripts/UVC/UI/ToolBar/ToolbarToggleButton.cs.meta b/Assets/Scripts/UVC/UI/ToolBar/ToolbarToggleButton.cs.meta new file mode 100644 index 00000000..4fabdd8c --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/ToolbarToggleButton.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6407c881188c7c04c9cb4efb1dd7b4ce \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/ToolBar/ToolbarView.cs b/Assets/Scripts/UVC/UI/ToolBar/ToolbarView.cs new file mode 100644 index 00000000..f4500ae2 --- /dev/null +++ b/Assets/Scripts/UVC/UI/ToolBar/ToolbarView.cs @@ -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 _modelToGameObjectMap = new Dictionary(); + + 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(); + // if (layoutGroup == null) layoutGroup = toolbarContainer.gameObject.AddComponent(); + // layoutGroup.childControlHeight = true; layoutGroup.childControlWidth = false; // 예시 설정 + + RenderToolbar(); + + if (tooltipPrefab != null) + { + _activeTooltipInstance = Instantiate(tooltipPrefab, transform); // ToolbarView의 자식으로 생성 (Canvas 내 다른 곳이어도 됨) + _tooltipTextElement = _activeTooltipInstance.GetComponentInChildren(); + _tooltipRectTransform = _activeTooltipInstance.GetComponent(); + _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(); + 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(); + if (tooltipHandler == null) tooltipHandler = itemObj.AddComponent(); + + 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().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(); + 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(); + 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