From 47ca5257185226e4237f895425e81ef7b89d0c0a Mon Sep 17 00:00:00 2001 From: logonkhi Date: Tue, 28 Oct 2025 20:10:51 +0900 Subject: [PATCH] =?UTF-8?q?=EB=93=9C=EB=9E=98=EA=B7=B8=20=EB=93=9C?= =?UTF-8?q?=EB=9E=8D=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/List/TreeList.prefab | 16 +- .../Prefabs/UI/List/TreeListItem.prefab | 776 ++++++++---------- Assets/Scenes/Sample/HierarchySample.cs | 6 +- Assets/Scenes/Sample/HierarchySample.unity | 28 +- Assets/Scripts/Simulator/LNB/LNBExplorer.cs | 2 +- Assets/Scripts/Simulator/LNB/LNBLibrary.cs | 2 +- .../UVC/Factory/Alarm/AlarmClusterIcon.cs | 4 +- .../UVC/Factory/Alarm/AlarmIconManager.cs | 4 +- .../UVC/Factory/Alarm/AlarmSingleIcon.cs | 4 +- .../Factory/Tab/TabContentComponentList.cs | 2 +- .../UVC/Factory/Tab/TabContentPrefabGrid.cs | 2 +- .../UI/List/ComponentList/ComponentList.cs | 2 +- .../List/ComponentList/ComponentListItem.cs | 4 +- .../UI/List/Draggable/ListReorderHandler.cs | 2 +- Assets/Scripts/UVC/UI/List/PrefabGrid.cs | 2 +- Assets/Scripts/UVC/UI/List/PrefabGridItem.cs | 4 +- Assets/Scripts/UVC/UI/List/Tree/TreeList.cs | 663 ++++++++++++++- .../UI/List/Tree/TreeListDragDropManager.cs | 191 +++++ .../List/Tree/TreeListDragDropManager.cs.meta | 2 + .../Scripts/UVC/UI/List/Tree/TreeListItem.cs | 768 ++++++++++++++++- .../UVC/UI/List/Tree/TreeListItemData.cs | 541 +++++++++++- .../UI/List/Tree/TreeListItemDragHandler.cs | 700 ++++++++++++++++ .../List/Tree/TreeListItemDragHandler.cs.meta | 2 + .../UVC/UI/Modal/ColorPicker/ColorPicker.cs | 2 +- .../UVC/UI/ToolBar/Model/ToolbarModel.cs | 8 +- .../UI/ToolBar/Model/ToolbarRadioButton.cs | 10 +- .../UI/ToolBar/Model/ToolbarToggleButton.cs | 6 +- .../View/ToolbarToggleButtonViewProcessor.cs | 6 +- .../UVC/UI/ToolBar/View/ToolbarView.cs | 2 +- .../UVC/UI/Window/ComponentListWindow.cs | 2 +- Assets/Shapes2D/Editor/ShapeEditor.cs | 6 +- Assets/Shapes2D/Scripts/Shape.cs | 4 +- 32 files changed, 3203 insertions(+), 570 deletions(-) create mode 100644 Assets/Scripts/UVC/UI/List/Tree/TreeListDragDropManager.cs create mode 100644 Assets/Scripts/UVC/UI/List/Tree/TreeListDragDropManager.cs.meta create mode 100644 Assets/Scripts/UVC/UI/List/Tree/TreeListItemDragHandler.cs create mode 100644 Assets/Scripts/UVC/UI/List/Tree/TreeListItemDragHandler.cs.meta diff --git a/Assets/Resources/Prefabs/UI/List/TreeList.prefab b/Assets/Resources/Prefabs/UI/List/TreeList.prefab index 97a61096..cc1f5651 100644 --- a/Assets/Resources/Prefabs/UI/List/TreeList.prefab +++ b/Assets/Resources/Prefabs/UI/List/TreeList.prefab @@ -611,8 +611,8 @@ MonoBehaviour: m_TargetGraphic: {fileID: 7985561521833800352} m_HandleRect: {fileID: 5613474474329381979} m_Direction: 2 - m_Value: 1 - m_Size: 0.9867725 + m_Value: 0 + m_Size: 1 m_NumberOfSteps: 0 m_OnValueChanged: m_PersistentCalls: @@ -774,7 +774,7 @@ MonoBehaviour: m_HandleRect: {fileID: 1697446796394887609} m_Direction: 0 m_Value: 0 - m_Size: 0.9672131 + m_Size: 1 m_NumberOfSteps: 0 m_OnValueChanged: m_PersistentCalls: @@ -939,7 +939,7 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: m_Padding: - m_Left: 0 + m_Left: 20 m_Right: 0 m_Top: 0 m_Bottom: 0 @@ -978,7 +978,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: de4afb3a0d93f05448f2fc60683275c0, type: 3} m_Name: m_EditorClassIdentifier: - margin: {x: 10, y: 10} + margin: {x: 0, y: 0} target: - {fileID: 2240268413977263779} --- !u!1 &6757006638454796643 @@ -1505,8 +1505,8 @@ RectTransform: - {fileID: 6986221832819843173} m_Father: {fileID: 1824308313848313193} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 1, y: 1} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 10, y: 10} + m_SizeDelta: {x: 30, y: 10} m_Pivot: {x: 0, y: 1} diff --git a/Assets/Resources/Prefabs/UI/List/TreeListItem.prefab b/Assets/Resources/Prefabs/UI/List/TreeListItem.prefab index 36308510..dbd9863a 100644 --- a/Assets/Resources/Prefabs/UI/List/TreeListItem.prefab +++ b/Assets/Resources/Prefabs/UI/List/TreeListItem.prefab @@ -9,9 +9,9 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 1842670860593731849} - - component: {fileID: 1842670860593731850} - - component: {fileID: 1842670860593731851} - - component: {fileID: 1842670860593731848} + - component: {fileID: 9080525835291741393} + - component: {fileID: 1687999617891964893} + - component: {fileID: 3350669364680364773} m_Layer: 5 m_Name: Layout m_TagString: Untagged @@ -31,25 +31,25 @@ RectTransform: m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: + - {fileID: 7754800818954797055} + - {fileID: 2862105998284369762} - {fileID: 64863342471867503} - {fileID: 373745982548249880} - - {fileID: 1842670861811643385} - - {fileID: 1842670861478363922} m_Father: {fileID: 1842670862426916798} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: -520, y: 0} + m_SizeDelta: {x: 58.7, y: 0} m_Pivot: {x: 0, y: 1} ---- !u!114 &1842670860593731850 +--- !u!114 &9080525835291741393 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1842670860593731854} - m_Enabled: 0 + m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} m_Name: @@ -61,172 +61,39 @@ MonoBehaviour: m_Bottom: 0 m_ChildAlignment: 3 m_Spacing: 0 - m_ChildForceExpandWidth: 0 + m_ChildForceExpandWidth: 1 m_ChildForceExpandHeight: 0 - m_ChildControlWidth: 0 + m_ChildControlWidth: 1 m_ChildControlHeight: 0 m_ChildScaleWidth: 0 m_ChildScaleHeight: 0 m_ReverseArrangement: 0 ---- !u!114 &1842670860593731851 +--- !u!114 &1687999617891964893 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1842670860593731854} - m_Enabled: 0 + m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} m_Name: m_EditorClassIdentifier: m_HorizontalFit: 2 m_VerticalFit: 2 ---- !u!114 &1842670860593731848 +--- !u!114 &3350669364680364773 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1842670860593731854} - m_Enabled: 0 + m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: a26959c49f0726e4eab1de41b396214a, type: 3} m_Name: m_EditorClassIdentifier: ---- !u!1 &1842670861478363923 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1842670861478363922} - - component: {fileID: 1842670861478363935} - - component: {fileID: 1842670861478363932} - - component: {fileID: 1842670861478363933} - m_Layer: 5 - m_Name: Button_Delete - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &1842670861478363922 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1842670861478363923} - 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: 1842670862342701466} - m_Father: {fileID: 1842670860593731849} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 1} - m_AnchorMax: {x: 0, y: 1} - m_AnchoredPosition: {x: 240, y: 0} - m_SizeDelta: {x: 20, y: 20} - m_Pivot: {x: 0, y: 1} ---- !u!222 &1842670861478363935 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1842670861478363923} - m_CullTransparentMesh: 0 ---- !u!114 &1842670861478363932 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1842670861478363923} - 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 &1842670861478363933 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1842670861478363923} - 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: Highlighted - m_DisabledTrigger: Disabled - m_Interactable: 1 - m_TargetGraphic: {fileID: 1842670861478363932} - m_OnClick: - m_PersistentCalls: - m_Calls: - - m_Target: {fileID: 4190301731963491743} - m_TargetAssemblyTypeName: UVC.UI.List.Tree.TreeListItem, Assembly-CSharp - m_MethodName: Delete - 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!1 &1842670861640538166 GameObject: m_ObjectHideFlags: 0 @@ -276,139 +143,6 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: a26959c49f0726e4eab1de41b396214a, type: 3} m_Name: m_EditorClassIdentifier: ---- !u!1 &1842670861811643390 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1842670861811643385} - - component: {fileID: 1842670861811643386} - - component: {fileID: 1842670861811643387} - - component: {fileID: 1842670861811643384} - m_Layer: 5 - m_Name: Button_AddChild - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &1842670861811643385 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1842670861811643390} - 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: 1842670862135320539} - m_Father: {fileID: 1842670860593731849} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 1} - m_AnchorMax: {x: 0, y: 1} - m_AnchoredPosition: {x: 220, y: 0} - m_SizeDelta: {x: 20, y: 20} - m_Pivot: {x: 0, y: 1} ---- !u!222 &1842670861811643386 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1842670861811643390} - m_CullTransparentMesh: 0 ---- !u!114 &1842670861811643387 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1842670861811643390} - 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 &1842670861811643384 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1842670861811643390} - 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: Highlighted - m_DisabledTrigger: Disabled - m_Interactable: 1 - m_TargetGraphic: {fileID: 1842670861811643387} - m_OnClick: - m_PersistentCalls: - m_Calls: - - m_Target: {fileID: 4190301731963491743} - m_TargetAssemblyTypeName: UVC.UI.List.Tree.TreeListItem, Assembly-CSharp - m_MethodName: AddChild - 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!1 &1842670862128736790 GameObject: m_ObjectHideFlags: 0 @@ -499,167 +233,9 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: de4afb3a0d93f05448f2fc60683275c0, type: 3} m_Name: m_EditorClassIdentifier: - margin: {x: 10, y: 0} + margin: {x: 0, y: 0} target: - {fileID: 1842670861640538161} ---- !u!1 &1842670862135320536 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1842670862135320539} - - component: {fileID: 1842670862135320517} - - component: {fileID: 1842670862135320538} - m_Layer: 5 - m_Name: Text - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &1842670862135320539 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1842670862135320536} - 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: 1842670861811643385} - 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 &1842670862135320517 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1842670862135320536} - m_CullTransparentMesh: 0 ---- !u!114 &1842670862135320538 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1842670862135320536} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, 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_FontData: - m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} - m_FontSize: 14 - m_FontStyle: 0 - m_BestFit: 0 - m_MinSize: 10 - m_MaxSize: 40 - m_Alignment: 4 - m_AlignByGeometry: 0 - m_RichText: 1 - m_HorizontalOverflow: 0 - m_VerticalOverflow: 0 - m_LineSpacing: 1 - m_Text: + ---- !u!1 &1842670862342701467 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1842670862342701466} - - component: {fileID: 1842670862342701444} - - component: {fileID: 1842670862342701445} - m_Layer: 5 - m_Name: Text - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &1842670862342701466 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1842670862342701467} - 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: 1842670861478363922} - 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 &1842670862342701444 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1842670862342701467} - m_CullTransparentMesh: 0 ---- !u!114 &1842670862342701445 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1842670862342701467} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, 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_FontData: - m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} - m_FontSize: 14 - m_FontStyle: 0 - m_BestFit: 0 - m_MinSize: 10 - m_MaxSize: 40 - m_Alignment: 4 - m_AlignByGeometry: 0 - m_RichText: 1 - m_HorizontalOverflow: 0 - m_VerticalOverflow: 0 - m_LineSpacing: 1 - m_Text: '-' --- !u!1 &1842670862426916799 GameObject: m_ObjectHideFlags: 0 @@ -673,6 +249,7 @@ GameObject: - component: {fileID: 1842670862426916793} - component: {fileID: 1842670862426916795} - component: {fileID: 4190301731963491743} + - component: {fileID: 5444376809909714340} m_Layer: 5 m_Name: TreeListItem m_TagString: Untagged @@ -770,6 +347,22 @@ MonoBehaviour: childExpand: {fileID: 7394439186909941661} childContainer: {fileID: 1842670861640538166} childRoot: {fileID: 1842670862128736785} + selectedBg: {fileID: 4496758519719229624} + itemButton: {fileID: 6688992559754752639} +--- !u!114 &5444376809909714340 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1842670862426916799} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 43beb810567ff7e489a0a9059cd3c4b0, type: 3} + m_Name: + m_EditorClassIdentifier: + dragAlpha: 0.5 + enableDragDrop: 1 --- !u!1 &3470798596265399154 GameObject: m_ObjectHideFlags: 0 @@ -781,6 +374,7 @@ GameObject: - component: {fileID: 373745982548249880} - component: {fileID: 5246427002897534631} - component: {fileID: 906041055054714701} + - component: {fileID: 6770770612792741896} m_Layer: 5 m_Name: Text m_TagString: Untagged @@ -802,11 +396,11 @@ RectTransform: m_Children: [] m_Father: {fileID: 1842670860593731849} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 1} - m_AnchorMax: {x: 0, y: 1} - m_AnchoredPosition: {x: 20, y: -10} - m_SizeDelta: {x: 200, y: 18} - m_Pivot: {x: 0, y: 0.5} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 20} + m_Pivot: {x: 0, y: 1} --- !u!222 &5246427002897534631 CanvasRenderer: m_ObjectHideFlags: 0 @@ -829,7 +423,7 @@ MonoBehaviour: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} - m_RaycastTarget: 1 + m_RaycastTarget: 0 m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: @@ -880,7 +474,7 @@ MonoBehaviour: m_charWidthMaxAdj: 0 m_TextWrappingMode: 1 m_wordWrappingRatios: 0.4 - m_overflowMode: 1 + m_overflowMode: 2 m_linkedTextComponent: {fileID: 0} parentLinkedComponent: {fileID: 0} m_enableKerning: 0 @@ -906,6 +500,167 @@ MonoBehaviour: m_hasFontAssetChanged: 0 m_baseMaterial: {fileID: 0} m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!114 &6770770612792741896 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3470798596265399154} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreLayout: 0 + m_MinWidth: -1 + m_MinHeight: -1 + m_PreferredWidth: -1 + m_PreferredHeight: -1 + m_FlexibleWidth: 100 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 +--- !u!1 &3778691549663360127 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2862105998284369762} + - component: {fileID: 8781608781722668640} + - component: {fileID: 1272354219224090627} + - component: {fileID: 6688992559754752639} + - component: {fileID: 7087237257074466157} + m_Layer: 5 + m_Name: itemButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2862105998284369762 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3778691549663360127} + 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: 1842670860593731849} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 280, y: 20} + m_Pivot: {x: 0, y: 1} +--- !u!222 &8781608781722668640 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3778691549663360127} + m_CullTransparentMesh: 1 +--- !u!114 &1272354219224090627 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3778691549663360127} + 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: 0} + 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: 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 &6688992559754752639 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3778691549663360127} + 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: 0 + 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: 1272354219224090627} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &7087237257074466157 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3778691549663360127} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreLayout: 1 + m_MinWidth: -1 + m_MinHeight: -1 + m_PreferredWidth: -1 + m_PreferredHeight: -1 + m_FlexibleWidth: -1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 --- !u!1 &7184699774172569858 GameObject: m_ObjectHideFlags: 0 @@ -918,6 +673,7 @@ GameObject: - component: {fileID: 897937054982788803} - component: {fileID: 5456992324606436631} - component: {fileID: 7394439186909941661} + - component: {fileID: 2292989689107632710} m_Layer: 5 m_Name: ButtonExpand m_TagString: Untagged @@ -939,10 +695,10 @@ RectTransform: m_Children: [] m_Father: {fileID: 1842670860593731849} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 1} - m_AnchorMax: {x: 0, y: 1} - m_AnchoredPosition: {x: 10, y: -10} - m_SizeDelta: {x: 20, y: 17} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 17} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &897937054982788803 CanvasRenderer: @@ -1038,3 +794,119 @@ MonoBehaviour: m_StringArgument: m_BoolArgument: 0 m_CallState: 2 +--- !u!114 &2292989689107632710 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7184699774172569858} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreLayout: 0 + m_MinWidth: -1 + m_MinHeight: -1 + m_PreferredWidth: 20 + m_PreferredHeight: 17 + m_FlexibleWidth: -1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 +--- !u!1 &8675116910395588941 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7754800818954797055} + - component: {fileID: 5525489389801839842} + - component: {fileID: 4496758519719229624} + - component: {fileID: 8111916982732890584} + m_Layer: 5 + m_Name: bg + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7754800818954797055 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8675116910395588941} + 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: 1842670860593731849} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 300, y: 20} + m_Pivot: {x: 0, y: 1} +--- !u!222 &5525489389801839842 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8675116910395588941} + m_CullTransparentMesh: 1 +--- !u!114 &4496758519719229624 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8675116910395588941} + 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: 0.1981132, g: 0.1981132, b: 0.1981132, 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: 0} + m_Type: 0 + 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 &8111916982732890584 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8675116910395588941} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreLayout: 1 + m_MinWidth: -1 + m_MinHeight: -1 + m_PreferredWidth: -1 + m_PreferredHeight: -1 + m_FlexibleWidth: -1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 diff --git a/Assets/Scenes/Sample/HierarchySample.cs b/Assets/Scenes/Sample/HierarchySample.cs index 66e8de03..0482dbb8 100644 --- a/Assets/Scenes/Sample/HierarchySample.cs +++ b/Assets/Scenes/Sample/HierarchySample.cs @@ -13,14 +13,14 @@ public class HierarchySample : MonoBehaviour { TreeListItemData itemData = new TreeListItemData("Item " + i); - int len = Random.Range(0, 5); + int len = Random.Range(1, 5); Debug.Log("len: " + len); for (int j = 0; j < len; j++) { - itemData.children.Add(new TreeListItemData("Item " + i + "." + j)); + itemData.Children.Add(new TreeListItemData("Item " + i + "." + j)); for (int k = 0; k < Random.Range(0, 3); k++) { - itemData.children[j].children.Add(new TreeListItemData("Item " + i + "." + j + "." + k)); + itemData.Children[j].Children.Add(new TreeListItemData("Item " + i + "." + j + "." + k)); } } hierarchyWindow.AddItem(itemData); diff --git a/Assets/Scenes/Sample/HierarchySample.unity b/Assets/Scenes/Sample/HierarchySample.unity index 854c1629..85bdb65a 100644 --- a/Assets/Scenes/Sample/HierarchySample.unity +++ b/Assets/Scenes/Sample/HierarchySample.unity @@ -933,14 +933,6 @@ PrefabInstance: serializedVersion: 3 m_TransformParent: {fileID: 651880472} m_Modifications: - - target: {fileID: 1154093525567517578, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3} - propertyPath: m_Padding.m_Left - value: 20 - objectReference: {fileID: 0} - - target: {fileID: 1154093525567517578, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3} - propertyPath: m_Padding.m_Right - value: 0 - objectReference: {fileID: 0} - target: {fileID: 1697446796394887609, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3} propertyPath: m_AnchorMax.x value: 0 @@ -949,21 +941,17 @@ PrefabInstance: propertyPath: m_AnchorMax.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 2240268413977263779, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3} - propertyPath: m_AnchorMax.x + - target: {fileID: 1824308313848313193, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3} + propertyPath: m_SizeDelta.x value: 0 objectReference: {fileID: 0} - - target: {fileID: 2240268413977263779, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3} - propertyPath: m_AnchorMin.y - value: 1 - objectReference: {fileID: 0} - target: {fileID: 2240268413977263779, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3} propertyPath: m_SizeDelta.x - value: 30 + value: 20 objectReference: {fileID: 0} - target: {fileID: 2240268413977263779, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3} propertyPath: m_SizeDelta.y - value: 10 + value: 0 objectReference: {fileID: 0} - target: {fileID: 2651864206743669424, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3} propertyPath: m_Name @@ -977,10 +965,6 @@ PrefabInstance: propertyPath: m_AnchorMax.y value: 0 objectReference: {fileID: 0} - - target: {fileID: 5613474474329381979, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3} - propertyPath: m_AnchorMin.y - value: 0 - objectReference: {fileID: 0} - target: {fileID: 6567388883908861155, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3} propertyPath: m_Pivot.x value: 0 @@ -1061,6 +1045,10 @@ PrefabInstance: propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} + - target: {fileID: 6690344807426313734, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3} + propertyPath: m_Name + value: Root + objectReference: {fileID: 0} - target: {fileID: 6986221832819843173, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3} propertyPath: m_SizeDelta.x value: 0 diff --git a/Assets/Scripts/Simulator/LNB/LNBExplorer.cs b/Assets/Scripts/Simulator/LNB/LNBExplorer.cs index 0fc2e13c..9d52cccb 100644 --- a/Assets/Scripts/Simulator/LNB/LNBExplorer.cs +++ b/Assets/Scripts/Simulator/LNB/LNBExplorer.cs @@ -33,7 +33,7 @@ namespace Simulator.LNB if (componentList == null) { - Debug.LogError("InfiniteScroll component is not assigned or found in children."); + Debug.LogError("InfiniteScroll component is not assigned or found in Children."); return; } } diff --git a/Assets/Scripts/Simulator/LNB/LNBLibrary.cs b/Assets/Scripts/Simulator/LNB/LNBLibrary.cs index 9f3a7744..cc35dad1 100644 --- a/Assets/Scripts/Simulator/LNB/LNBLibrary.cs +++ b/Assets/Scripts/Simulator/LNB/LNBLibrary.cs @@ -21,7 +21,7 @@ namespace Simulator.LNB if (prefabGrid == null) { - Debug.LogError("InfiniteScroll component is not assigned or found in children."); + Debug.LogError("InfiniteScroll component is not assigned or found in Children."); return; } } diff --git a/Assets/Scripts/UVC/Factory/Alarm/AlarmClusterIcon.cs b/Assets/Scripts/UVC/Factory/Alarm/AlarmClusterIcon.cs index 26a75803..477c0137 100644 --- a/Assets/Scripts/UVC/Factory/Alarm/AlarmClusterIcon.cs +++ b/Assets/Scripts/UVC/Factory/Alarm/AlarmClusterIcon.cs @@ -1,4 +1,4 @@ -using System; +using System; using TMPro; using UnityEngine; using UnityEngine.UI; @@ -47,7 +47,7 @@ namespace UVC.Factory.Alarm buttonText = GetComponentInChildren(); if (buttonText == null) { - Debug.LogWarning("Text component not found in children.", this); + Debug.LogWarning("Text component not found in Children.", this); } } diff --git a/Assets/Scripts/UVC/Factory/Alarm/AlarmIconManager.cs b/Assets/Scripts/UVC/Factory/Alarm/AlarmIconManager.cs index 3cc67d77..ba7824cc 100644 --- a/Assets/Scripts/UVC/Factory/Alarm/AlarmIconManager.cs +++ b/Assets/Scripts/UVC/Factory/Alarm/AlarmIconManager.cs @@ -1,4 +1,4 @@ -using DG.Tweening; +using DG.Tweening; using System.Collections.Generic; using UnityEngine; using UVC.Data; @@ -196,7 +196,7 @@ namespace UVC.Factory.Alarm /// /// 현재 알람 상태에 맞게 UI를 갱신합니다. - /// 알람 개수와 확장 상태(isExpanded)에 따라 어떤 UI를 보여줄지 결정합니다. + /// 알람 개수와 확장 상태(IsExpanded)에 따라 어떤 UI를 보여줄지 결정합니다. /// private void UpdateView() { diff --git a/Assets/Scripts/UVC/Factory/Alarm/AlarmSingleIcon.cs b/Assets/Scripts/UVC/Factory/Alarm/AlarmSingleIcon.cs index 38d21bb3..a996769e 100644 --- a/Assets/Scripts/UVC/Factory/Alarm/AlarmSingleIcon.cs +++ b/Assets/Scripts/UVC/Factory/Alarm/AlarmSingleIcon.cs @@ -1,4 +1,4 @@ -using System; +using System; using TMPro; using UnityEngine; using UnityEngine.UI; @@ -47,7 +47,7 @@ namespace UVC.Factory.Alarm buttonText = GetComponentInChildren(); if (buttonText == null) { - Debug.LogWarning("Text component not found in children.", this); + Debug.LogWarning("Text component not found in Children.", this); } } diff --git a/Assets/Scripts/UVC/Factory/Tab/TabContentComponentList.cs b/Assets/Scripts/UVC/Factory/Tab/TabContentComponentList.cs index 2e4a1732..4a044317 100644 --- a/Assets/Scripts/UVC/Factory/Tab/TabContentComponentList.cs +++ b/Assets/Scripts/UVC/Factory/Tab/TabContentComponentList.cs @@ -33,7 +33,7 @@ namespace UVC.Factory.Tab if (componentList == null) { - Debug.LogError("InfiniteScroll component is not assigned or found in children."); + Debug.LogError("InfiniteScroll component is not assigned or found in Children."); return; } } diff --git a/Assets/Scripts/UVC/Factory/Tab/TabContentPrefabGrid.cs b/Assets/Scripts/UVC/Factory/Tab/TabContentPrefabGrid.cs index b46f9634..f13723be 100644 --- a/Assets/Scripts/UVC/Factory/Tab/TabContentPrefabGrid.cs +++ b/Assets/Scripts/UVC/Factory/Tab/TabContentPrefabGrid.cs @@ -27,7 +27,7 @@ namespace UVC.Factory.Tab if (prefabGrid == null) { - Debug.LogError("InfiniteScroll component is not assigned or found in children."); + Debug.LogError("InfiniteScroll component is not assigned or found in Children."); return; } } diff --git a/Assets/Scripts/UVC/UI/List/ComponentList/ComponentList.cs b/Assets/Scripts/UVC/UI/List/ComponentList/ComponentList.cs index 5d79bddf..d0abb1b6 100644 --- a/Assets/Scripts/UVC/UI/List/ComponentList/ComponentList.cs +++ b/Assets/Scripts/UVC/UI/List/ComponentList/ComponentList.cs @@ -45,7 +45,7 @@ namespace UVC.UI.List.ComponentList if (scrollList == null) { - Debug.LogError("InfiniteScroll component is not assigned or found in children."); + Debug.LogError("InfiniteScroll component is not assigned or found in Children."); return; } diff --git a/Assets/Scripts/UVC/UI/List/ComponentList/ComponentListItem.cs b/Assets/Scripts/UVC/UI/List/ComponentList/ComponentListItem.cs index 8f16b043..f1cff559 100644 --- a/Assets/Scripts/UVC/UI/List/ComponentList/ComponentListItem.cs +++ b/Assets/Scripts/UVC/UI/List/ComponentList/ComponentListItem.cs @@ -70,8 +70,8 @@ namespace UVC.UI.List.ComponentList /// var generalData = new ComponentListItemData /// { /// isCategory = false, - /// generalName = "분전반", - /// generalOption = "옵션 A", + /// Name = "분전반", + /// Option = "옵션 A", /// factoryObjectInfo = new FactoryObjectInfo { Id = "some-unique-Id", Name = "분전반" } /// }; /// diff --git a/Assets/Scripts/UVC/UI/List/Draggable/ListReorderHandler.cs b/Assets/Scripts/UVC/UI/List/Draggable/ListReorderHandler.cs index 99fc4db0..9ab21a95 100644 --- a/Assets/Scripts/UVC/UI/List/Draggable/ListReorderHandler.cs +++ b/Assets/Scripts/UVC/UI/List/Draggable/ListReorderHandler.cs @@ -151,7 +151,7 @@ namespace UVC.UI.List.Draggable for (int i = 0; i < children.Length; i++) { // Y 좌표 차이만 계산합니다 (세로 리스트 기준) - // 가로 리스트의 경우 localPos.x - children[i].anchoredPosition.x 를 사용하세요 + // 가로 리스트의 경우 localPos.x - Children[i].anchoredPosition.x 를 사용하세요 float distance = Mathf.Abs(localPos.y - (children[i].anchoredPosition.y + children[i].rect.height / 2)); if (distance < closestDistance) diff --git a/Assets/Scripts/UVC/UI/List/PrefabGrid.cs b/Assets/Scripts/UVC/UI/List/PrefabGrid.cs index d2651de0..ef983519 100644 --- a/Assets/Scripts/UVC/UI/List/PrefabGrid.cs +++ b/Assets/Scripts/UVC/UI/List/PrefabGrid.cs @@ -44,7 +44,7 @@ namespace UVC.UI.List if (scrollList == null) { - Debug.LogError("InfiniteScroll component is not assigned or found in children."); + Debug.LogError("InfiniteScroll component is not assigned or found in Children."); return; } diff --git a/Assets/Scripts/UVC/UI/List/PrefabGridItem.cs b/Assets/Scripts/UVC/UI/List/PrefabGridItem.cs index 3224ca6f..43220f74 100644 --- a/Assets/Scripts/UVC/UI/List/PrefabGridItem.cs +++ b/Assets/Scripts/UVC/UI/List/PrefabGridItem.cs @@ -33,8 +33,8 @@ namespace UVC.UI.List /// var generalData = new ComponentListItemData /// { /// isCategory = false, - /// generalName = "분전반", - /// generalOption = "옵션 A", + /// Name = "분전반", + /// Option = "옵션 A", /// factoryObjectInfo = new FactoryObjectInfo { Id = "some-unique-Id", Name = "분전반" } /// }; /// diff --git a/Assets/Scripts/UVC/UI/List/Tree/TreeList.cs b/Assets/Scripts/UVC/UI/List/Tree/TreeList.cs index 39866498..5b5a8702 100644 --- a/Assets/Scripts/UVC/UI/List/Tree/TreeList.cs +++ b/Assets/Scripts/UVC/UI/List/Tree/TreeList.cs @@ -1,21 +1,682 @@ #nullable enable +using System; +using System.Collections.Generic; +using System.Linq; using UnityEngine; namespace UVC.UI.List.Tree { + /// + /// 트리 구조의 리스트를 관리하고 제어하는 클래스입니다. + /// + /// 역할: + /// 1. 아이템 추가/제거 관리 + /// 2. 선택 상태 관리 (단일, 다중 선택) + /// 3. 키 입력(Ctrl, Shift)에 따른 선택 로직 처리 + /// 4. 선택 상태 변경 이벤트 발생 + /// + /// 파일 탐색기와 비슷한 동작: + /// - 클릭: 한 항목 선택 + /// - Ctrl+클릭: 여러 항목 선택 (기존 선택 유지) + /// - Shift+클릭: 범위 선택 (시작~끝) + /// + /// MonoBehaviour란? + /// Unity의 모든 게임 로직이 상속받는 기본 클래스입니다. + /// Inspector에서 설정할 수 있고, Update 같은 생명주기 함수를 사용할 수 있습니다. + /// public class TreeList : MonoBehaviour { + /// + /// UI 아이템(TreeListItem)의 프리팹(템플릿)입니다. + /// + /// 프리팹이란? + /// 미리 만들어둔 UI 템플릿입니다. + /// 이것을 복제(Instantiate)해서 새로운 아이템을 여러 개 만들 수 있습니다. + /// + /// [SerializeField]란? + /// 이 필드를 Inspector(게임 편집기)에서 직접 설정할 수 있게 해줍니다. + /// (private이지만 Unity에서만 접근 가능) + /// + /// 사용 흐름: + /// 1. Inspector에서 프리팹을 이 필드에 드래그&드롭 + /// 2. AddItem()이 호출되면 이 프리팹을 복제해서 새 아이템 생성 + /// [SerializeField] protected TreeListItem itemPrefab; + + /// + /// itemPrefab을 읽기만 가능하게 공개합니다. + /// + /// 문법: public TreeListItem ItemPrefab => itemPrefab; + /// = 프로퍼티 (getter 전용) + /// = 외부에서 이 값을 읽을 수 있지만 변경할 수 없음 + /// public TreeListItem ItemPrefab => itemPrefab; + /// + /// 모든 아이템을 담는 부모 컨테이너입니다. + /// + /// RectTransform이란? + /// UI 요소의 위치, 크기, 회전을 관리하는 컴포넌트입니다. + /// Canvas(캔버스) 아래의 모든 UI 요소가 이것을 가집니다. + /// + /// 역할: + /// - AddItem()에서 새 아이템의 부모로 지정 + /// - UpdateFlattenedItemList()에서 모든 자식을 순회 + /// + /// 구조 예: + /// root (이것) + /// ├─ 아이템1 + /// ├─ 아이템2 + /// └─ 아이템3 + /// [SerializeField] protected RectTransform root; + public RectTransform Root => root; + + /// + /// 여러 개 선택이 가능한지 여부를 나타냅니다. + /// + /// true: 여러 개 선택 가능 (Ctrl+클릭, Shift+클릭 작동) + /// false: 한 개만 선택 가능 (최신 선택이 기존 선택을 덮어씀) + /// + /// 예시: + /// - true인 경우: 파일 탐색기 (여러 파일 선택 가능) + /// - false인 경우: 라디오 버튼 (하나만 선택) + /// + /// SetAllowMultipleSelection() 메서드로 런타임에 변경 가능합니다. + /// + [SerializeField] + protected bool allowMultipleSelection = true; + + /// + /// 드래그 & 드롭 기능 활성화 여부입니다. + /// + [SerializeField] + protected bool enableDragDrop = true; + + /// + /// 현재 선택된 아이템들을 저장하는 리스트입니다. + /// + /// 역할: + /// - SelectItem(): 선택된 아이템 추가 + /// - DeselectItem(): 선택된 아이템 제거 + /// - ClearSelection(): 모두 해제 + /// - SelectedItems 프로퍼티: 외부에 읽기 전용으로 제공 + /// + /// 예: + /// 사용자가 3개 파일을 선택하면 이 리스트에 [파일1, 파일2, 파일3]이 저장됩니다. + /// + /// List란? + /// 배열처럼 여러 개 항목을 저장하는 컨테이너입니다. + /// 크기가 자동으로 조정되므로 개수를 미리 정할 필요가 없습니다. + /// + protected List selectedItems = new List(); + + /// + /// 사용자가 마지막으로 선택한 아이템입니다. + /// + /// 용도: Shift+클릭으로 범위 선택할 때 시작점으로 사용 + /// + /// 시나리오: + /// 1. 파일1 클릭 → lastSelectedItem = 파일1 + /// 2. Shift+파일5 클릭 → 파일1부터 파일5까지 모두 선택 + /// + /// ? 연산자 (nullable)란? + /// 이 변수는 null(값 없음)일 수 있습니다. + /// 예: lastSelectedItem = null (아직 선택한 게 없음) + /// + /// null 체크: + /// if (lastSelectedItem != null) { ... } + /// = 값이 있는지 확인 후에 사용 + /// + protected TreeListItemData? lastSelectedItem = null; + + /// + /// 모든 아이템을 1차원 리스트로 변환한 것입니다. (평탄화) + /// + /// "평탄화"란? + /// 원본 트리 구조: + /// 폴더1 + /// ├─ 파일1 + /// └─ 폴더2 + /// └─ 파일2 + /// + /// 평탄화된 리스트: + /// [폴더1, 파일1, 폴더2, 파일2] (계층 무시, 선형으로 배열) + /// + /// 용도: + /// - SelectRange()에서 두 아이템 사이의 인덱스를 빠르게 찾기 + /// - Shift+클릭 범위 선택 구현 + /// + /// UpdateFlattenedItemList() 메서드로 항상 최신 상태로 유지됩니다. + /// + protected List allItemsFlattened = new List(); + + /// + /// 드래그 & 드롭 매니저입니다. + /// + protected TreeListDragDropManager dragDropManager = new TreeListDragDropManager(); + + public TreeListDragDropManager DragDropManager => dragDropManager; + + /// + /// 아이템의 선택 상태가 변경되었을 때 발생하는 이벤트입니다. + /// + /// 이벤트란? + /// 무언가 일어났을 때 다른 코드에 알려주는 메커니즘입니다. + /// 구독자(listener)가 등록되어 있으면 자동으로 호출됩니다. + /// + /// 문법: event Action? + /// = TreeListItemData 1개, bool 1개를 매개변수로 받는 함수들을 등록 + /// + /// 호출되는 경우: + /// - SelectItem() 호출 시 → OnItemSelectionChanged?.Invoke(data, true); + /// - DeselectItem() 호출 시 → OnItemSelectionChanged?.Invoke(data, false); + /// + /// 사용 예: + /// treeList.OnItemSelectionChanged += (data, isSelected) => { + /// Debug.Log($"{data.Name}이 {(isSelected ? "선택됨" : "해제됨")}"); + /// }; + /// + public event Action? OnItemSelectionChanged; + + /// + /// 현재 선택된 아이템 목록을 반환합니다. (읽기 전용) + /// + /// 문법: public IReadOnlyList SelectedItems => ... + /// = 프로퍼티 (getter 전용) + /// = 외부에서 읽을 수 있지만 변경할 수 없음 + /// + /// AsReadOnly()란? + /// 리스트를 읽기 전용으로 변환합니다. + /// 따라서 외부에서 SelectedItems.Add()같은 수정이 불가능합니다. + /// + /// 왜 이렇게 하나? + /// 클래스 내부에서만 selectedItems를 제어하고, + /// 외부는 결과만 볼 수 있게 하기 위함입니다. (데이터 무결성) + /// + /// 사용 예: + /// foreach (var item in treeList.SelectedItems) + /// { + /// Debug.Log(item.Name); // 읽기만 가능 + /// } + /// + public IReadOnlyList SelectedItems => selectedItems.AsReadOnly(); + + private void Awake() + { + // 드래그 & 드롭 이벤트 구독 + if (enableDragDrop) + { + dragDropManager.OnDropped += HandleItemDropped; + } + } + + private void OnDestroy() + { + if (enableDragDrop) + { + dragDropManager.OnDropped -= HandleItemDropped; + } + } + + /// + /// 새로운 아이템을 트리 리스트에 추가합니다. + /// + /// 호출 시점: + /// - 프로그램이 새 데이터를 UI에 표시하고 싶을 때 + /// + /// 처리 순서: + /// 1. itemPrefab을 복제해서 새 UI 객체 생성 (Instantiate) + /// 2. 복제된 객체를 root(부모 컨테이너) 아래에 배치 + /// 3. 복제된 객체의 Init() 메서드 호출해서 초기화 + /// 4. 평탄화 리스트 업데이트 (범위 선택 시 사용) + /// + /// 매개변수: + /// - data: 표시할 데이터 (이름, 자식 등의 정보 포함) + /// + /// 사용 예: + /// var newItem = new TreeListItemData("새 폴더"); + /// treeList.AddItem(newItem); // 화면에 표시됨 + /// + /// Instantiate란? + /// 프리팹(템플릿)을 복제해서 새로운 객체를 만듭니다. + /// 게임의 모든 객체는 이 방식으로 생성됩니다. + /// public void AddItem(TreeListItemData data) { + // Instantiate(템플릿, 부모 Transform) + // = 템플릿을 복제하고 부모의 자식으로 설정 TreeListItem item = GameObject.Instantiate(ItemPrefab, root); - item.Init(data, this); + + // 생성된 아이템 초기화 + // 데이터를 UI에 바인딩하고 이벤트 리스너 등록 + item.Init(data, this, dragDropManager); + + // 범위 선택에 필요한 평탄화 리스트 업데이트 + UpdateFlattenedItemList(); } + + /// + /// 모든 아이템을 평탄화된 1차원 리스트로 재구성합니다. + /// + /// 동작: + /// 1. 기존 평탄화 리스트를 비움 (Clear) + /// 2. root의 모든 직접 자식을 순회 (foreach) + /// 3. 각 자식의 TreeListItem 컴포넌트 획득 (GetComponent) + /// 4. AddItemToFlattened() 호출해서 자식과 손자까지 재귀 추가 + /// + /// 호출되는 시점: + /// - AddItem() 실행 후 (새 아이템 추가됨) + /// - OnDataChanged 이벤트 발생 시 (자식 구조 변경됨) + /// + /// Transform이란? + /// 게임 오브젝트의 위치, 회전, 크기 정보를 가진 컴포넌트입니다. + /// 계층 구조(부모-자식 관계)의 중심입니다. + /// root.childCount: 몇 개의 자식이 있는지 + /// for (Transform child in root): 모든 자식을 순회 + /// + /// GetComponent() 란? + /// 게임 오브젝트에 붙어있는 특정 컴포넌트를 찾아서 반환합니다. + /// 찾지 못하면 null 반환 + /// + /// 예: child.GetComponent() + /// = child 게임 오브젝트에서 TreeListItem 컴포넌트 찾기 + /// + internal void UpdateFlattenedItemList() + { + // 기존 평탄화 리스트 비우기 + allItemsFlattened.Clear(); + + // root의 모든 직접 자식을 순회 + // (손자, 증손자는 재귀로 처리됨) + foreach (Transform child in root) + { + // 자식 게임 오브젝트에서 TreeListItem 컴포넌트 찾기 + TreeListItem item = child.GetComponent(); + + // null 체크: 컴포넌트가 있고, 데이터도 있는지 확인 + if (item != null && item.Data != null) + { + // 이 아이템과 자식들을 재귀적으로 평탄화 리스트에 추가 + AddItemToFlattened(item.Data); + } + } + } + + /// + /// 아이템과 그 모든 자식 아이템들을 평탄화 리스트에 재귀적으로 추가합니다. + /// + /// "재귀"란? + /// 함수가 자기 자신을 호출하는 것입니다. + /// 계층 구조(깊이가 정해지지 않은)를 탐색하는 데 유용합니다. + /// + /// 동작: + /// 1. 현재 data를 allItemsFlattened에 추가 + /// 2. data의 모든 자식 Children을 순회 + /// 3. 각 자식에 대해 AddItemToFlattened() 재귀 호출 + /// + /// 예시 - 트리 구조를 평탄화하는 과정: + /// + /// 트리: 재귀 호출 흐름: 결과: + /// 폴더1 AddItemToFlattened(폴더1) [폴더1, + /// ├─ 파일1 → AddItemToFlattened(파일1) 파일1, + /// ├─ 파일2 → AddItemToFlattened(파일2) 파일2, + /// └─ 폴더2 → AddItemToFlattened(폴더2) 폴더2, + /// └─ 파일3 → AddItemToFlattened(파일3) 파일3] + /// + /// 매개변수: + /// - data: 현재 처리할 아이템 + /// + private void AddItemToFlattened(TreeListItemData data) + { + // 현재 아이템을 평탄화 리스트에 추가 + allItemsFlattened.Add(data); + + // 현재 아이템의 모든 자식을 순회 + foreach (var child in data.Children) + { + // 각 자식에 대해 재귀 호출 + // = 자식의 자식들도 모두 추가됨 + AddItemToFlattened(child); + } + } + + /// + /// 아이템이 클릭되었을 때 호출됩니다. + /// + /// 역할: + /// - 사용자가 어떤 키를 누르고 있는지 확인 + /// - 그에 맞는 선택 로직 실행 + /// - 마지막 선택 아이템 기억 + /// + /// 파일 탐색기와 동일한 동작: + /// - 일반 클릭: 한 항목만 선택 + /// - Ctrl+클릭: 여러 항목 선택 (기존 선택 유지) + /// - Shift+클릭: 범위 선택 (마지막 선택~현재 선택) + /// + /// 매개변수: + /// - data: 클릭된 아이템의 데이터 + /// - ctrlPressed: Ctrl 키가 눌렸는지 여부 + /// - shiftPressed: Shift 키가 눌렸는지 여부 + /// + public void OnItemClicked(TreeListItemData data, bool ctrlPressed, bool shiftPressed) + { + // 디버그 로그: 클릭 정보를 콘솔에 출력 (개발 중 확인용) + Debug.Log($"OnItemClicked {data.Name}, ctrlPressed:{ctrlPressed}, shiftPressed:{shiftPressed}, lastSelectedItem:{lastSelectedItem}"); + + // ============================================================ + // 경우 1: 다중 선택이 비활성화된 경우 + // ============================================================ + if (!allowMultipleSelection) + { + // 한 개만 선택 가능하므로 단순히 토글 + // (선택 상태를 반대로) + ToggleItemSelection(data); + return; + } + + // ============================================================ + // 경우 2: Ctrl 키를 누르고 클릭 + // ============================================================ + if (ctrlPressed) + { + // 기존 선택을 유지하면서 현재 아이템을 토글 + // 이미 선택됨 → 해제 + // 선택 안 됨 → 선택 + ToggleItemSelection(data); + + // 이 아이템을 "마지막 선택"으로 기억 + // (다음 Shift+클릭의 시작점) + lastSelectedItem = data; + } + // ============================================================ + // 경우 3: Shift 키를 누르고 클릭 + // ============================================================ + else if (shiftPressed) + { + if (lastSelectedItem != null) + { + // 마지막 선택부터 현재 아이템까지 범위 선택 + SelectRange(lastSelectedItem, data); + } + else + { + // 아직 선택한 게 없으면 단순 선택 + ClearSelection(); + SelectItem(data); + } + + // 이 아이템을 "마지막 선택"으로 기억 + lastSelectedItem = data; + } + // ============================================================ + // 경우 4: 아무 키도 누르지 않고 클릭 (일반 클릭) + // ============================================================ + else + { + if (data.IsSelected) + { + // 이미 선택된 아이템을 다시 클릭 → 선택 해제 + DeselectItem(data); + lastSelectedItem = null; + } + else + { + // 선택되지 않은 아이템 클릭 → 다른 선택 모두 해제하고 이것 선택 + ClearSelection(); + SelectItem(data); + lastSelectedItem = data; + } + } + } + + /// + /// 두 아이템 사이의 모든 아이템을 선택합니다. (범위 선택) + /// + /// 시나리오: + /// 평탄화 리스트: [파일1, 파일2, 파일3, 파일4, 파일5] + /// 파일2를 선택한 후 Shift+파일5 클릭 + /// → SelectRange(파일2, 파일5) + /// → 파일2, 파일3, 파일4, 파일5 모두 선택됨 + /// + /// 동작: + /// 1. 평탄화 리스트에서 두 아이템의 인덱스(위치) 찾기 + /// 2. 시작과 끝 인덱스를 정렬 (둘 중 작은 값, 큰 값) + /// 3. 그 범위의 모든 아이템 선택 + /// + /// IndexOf란? + /// 리스트에서 특정 항목의 위치(인덱스)를 찾습니다. + /// 찾으면 위치 반환 (0부터 시작), 못 찾으면 -1 반환 + /// + /// 예: list = [A, B, C, D] + /// list.IndexOf(C) → 2 (3번째이지만 0부터 시작하므로 2) + /// + /// Mathf.Min/Max란? + /// 두 숫자 중 작은/큰 값을 반환합니다. + /// Mathf.Min(5, 2) → 2 + /// Mathf.Max(5, 2) → 5 + /// + /// 매개변수: + /// - startItem: 범위의 시작점 (사용자가 먼저 선택한 아이템) + /// - endItem: 범위의 끝점 (Shift+클릭한 아이템) + /// + private void SelectRange(TreeListItemData startItem, TreeListItemData endItem) + { + // 평탄화 리스트에서 시작 아이템의 위치(인덱스) 찾기 + int startIndex = allItemsFlattened.IndexOf(startItem); + + // 평탄화 리스트에서 끝 아이템의 위치(인덱스) 찾기 + int endIndex = allItemsFlattened.IndexOf(endItem); + + // 두 아이템 모두 리스트에 없으면 종료 + if (startIndex == -1 || endIndex == -1) + { + return; + } + + // 시작과 끝의 순서를 정렬 + // 사용자가 거꾸로 선택할 수도 있으므로 (끝→시작) + // 항상 작은 인덱스 = 시작, 큰 인덱스 = 끝으로 정렬 + int minIndex = Mathf.Min(startIndex, endIndex); + int maxIndex = Mathf.Max(startIndex, endIndex); + + // 범위 내의 모든 아이템 선택 + // i = minIndex부터 i = maxIndex까지 (포함) + for (int i = minIndex; i <= maxIndex; i++) + { + SelectItem(allItemsFlattened[i]); + } + } + + /// + /// 아이템의 선택 상태를 반대로 바꿉니다. (토글) + /// + /// 동작: + /// - 선택됨 → 선택 해제 + /// - 선택 안 됨 → 선택 + /// + /// 사용 예: + /// treeItem.IsSelected = false + /// treeList.ToggleItemSelection(treeItem) + /// // 이후 treeItem.IsSelected = true + /// + /// 매개변수: + /// - data: 토글할 아이템 + /// + public void ToggleItemSelection(TreeListItemData data) + { + if (data.IsSelected) + { + // 선택됨 → 해제 + DeselectItem(data); + } + else + { + // 선택 안 됨 → 선택 + SelectItem(data); + } + } + + /// + /// 아이템을 선택합니다. + /// + /// 동작: + /// 1. 이미 선택되어 있으면 아무것도 안 함 (중복 선택 방지) + /// 2. 다중 선택이 비활성화되어 있으면 기존 선택 모두 해제 + /// 3. 현재 아이템 선택 표시 + /// 4. selectedItems 리스트에 추가 + /// 5. OnItemSelectionChanged 이벤트 발생 + /// + /// 매개변수: + /// - data: 선택할 아이템 + /// + /// 이벤트란? + /// SelectItem이 호출되면 이 이벤트를 구독한 다른 코드들이 + /// 자동으로 실행됩니다. (옵션 창 업데이트 등) + /// + public void SelectItem(TreeListItemData data) + { + // 이미 선택되어 있으면 중복 선택 방지 + if (data.IsSelected) + { + return; + } + + // 다중 선택이 불가능한 경우, 기존 선택 해제 + // (새 선택만 유지) + if (!allowMultipleSelection && selectedItems.Count > 0) + { + // 첫 번째(유일한) 선택 항목 해제 + DeselectItem(selectedItems[0]); + } + + // 아이템의 선택 상태를 true로 설정 + data.IsSelected = true; + + // 아직 리스트에 없으면 추가 + if (!selectedItems.Contains(data)) + { + selectedItems.Add(data); + } + + // 이벤트 발생 + // ?. 연산자: null 체크 후 호출 + // (구독자가 없을 수도 있으므로) + OnItemSelectionChanged?.Invoke(data, true); + } + + /// + /// 아이템의 선택을 해제합니다. + /// + /// 동작: + /// 1. 선택되어 있지 않으면 아무것도 안 함 (중복 해제 방지) + /// 2. 아이템의 선택 상태를 false로 설정 + /// 3. selectedItems 리스트에서 제거 + /// 4. OnItemSelectionChanged 이벤트 발생 + /// + /// 매개변수: + /// - data: 선택 해제할 아이템 + /// + public void DeselectItem(TreeListItemData data) + { + // 이미 선택 해제되어 있으면 아무것도 안 함 + if (!data.IsSelected) + { + return; + } + + // 아이템의 선택 상태를 false로 설정 + data.IsSelected = false; + + // selectedItems 리스트에서 제거 + selectedItems.Remove(data); + + // 이벤트 발생 + OnItemSelectionChanged?.Invoke(data, false); + } + + /// + /// 모든 선택 상태를 한 번에 해제합니다. + /// + /// 동작: + /// 1. selectedItems를 복사 (ToList()) + /// 왜? 반복 중에 리스트를 수정하면 버그가 발생할 수 있으므로 + /// 2. 복사본을 순회하면서 각각 DeselectItem() 호출 + /// + /// 사용 예: + /// treeList.ClearSelection(); + /// // 이후 selectedItems는 비어있음 + /// + /// 결과: + /// - 모든 아이템의 IsSelected = false + /// - selectedItems 리스트 비어있음 + /// - 각 아이템마다 OnItemSelectionChanged 이벤트 발생 + /// + public void ClearSelection() + { + // ToList()로 복사 + // = selectedItems의 현재 상태를 새로운 리스트로 만듦 + // 반복 중에 원본을 수정해도 안전하게 함 + foreach (var item in selectedItems.ToList()) + { + DeselectItem(item); + } + } + + /// + /// 다중 선택 가능 여부를 설정합니다. + /// + /// 동작: + /// 1. allowMultipleSelection 값 변경 + /// 2. 만약 false로 변경하면서 여러 개가 선택되어 있으면: + /// - 첫 번째 아이템만 유지 + /// - 나머지 모두 해제 + /// + /// 시나리오: + /// 현재: 3개 선택된 상태 (파일1, 파일2, 파일3) + /// SetAllowMultipleSelection(false) 호출 + /// 결과: 파일1만 선택, 파일2와 파일3은 해제 + /// + /// 매개변수: + /// - allow: true면 다중 선택 가능, false면 단일 선택만 가능 + /// + /// 사용 예: + /// // 라디오 버튼 같은 단일 선택 모드로 변경 + /// treeList.SetAllowMultipleSelection(false); + /// + public void SetAllowMultipleSelection(bool allow) + { + // 다중 선택 가능 여부 설정 + allowMultipleSelection = allow; + + // false로 변경되었는데 여러 개가 선택되어 있으면 정리 + if (!allow && selectedItems.Count > 1) + { + // 첫 번째 아이템 보관 + var firstItem = selectedItems[0]; + + // 모든 선택 해제 + ClearSelection(); + + // 첫 번째 아이템만 다시 선택 + SelectItem(firstItem); + } + } + + /// + /// 드롭 완료 후 UI를 업데이트합니다. + /// + private void HandleItemDropped(TreeListItemData draggedItem, TreeListItemData? targetItem) + { + // 평탄화 리스트 업데이트 + UpdateFlattenedItemList(); + + // 필요시 UI 재구성 (예: 자식 컨테이너 위치 변경 등) + Debug.Log($"Item '{draggedItem.Name}' dropped on '{(targetItem?.Name ?? "Root")}'"); + } + } } diff --git a/Assets/Scripts/UVC/UI/List/Tree/TreeListDragDropManager.cs b/Assets/Scripts/UVC/UI/List/Tree/TreeListDragDropManager.cs new file mode 100644 index 00000000..d2476a90 --- /dev/null +++ b/Assets/Scripts/UVC/UI/List/Tree/TreeListDragDropManager.cs @@ -0,0 +1,191 @@ +#nullable enable +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace UVC.UI.List.Tree +{ + /// + /// 트리 리스트의 드래그 & 드롭 기능을 관리하는 클래스입니다. + /// + /// 역할: + /// 1. 드래그 시작/진행/종료 상태 관리 + /// 2. 유효한 드롭 대상 판단 (순환 참조 방지) + /// 3. 아이템 위치 변경 (형제 아이템 간 순서 변경) + /// 4. 아이템 계층 구조 변경 (부모-자식 관계 수정) + /// + /// 기능: + /// - 드래그할 아이템과 드롭 대상을 추적 + /// - 유효성 검사 (자기 자신에게 드롭 금지, 순환 참조 방지) + /// - 드롭 완료 후 데이터 동기화 + /// + public class TreeListDragDropManager + { + /// + /// 드래그 중인 아이템의 데이터입니다. + /// + public TreeListItemData? DraggedItem { get; private set; } + + /// + /// 현재 드래그 중인 상태인지 여부입니다. + /// + public bool IsDragging { get; private set; } + + /// + /// 드래그 시작 시 발생하는 이벤트입니다. + /// + public event Action? OnDragStarted; + + /// + /// 드래그 진행 중 발생하는 이벤트입니다. + /// + public event Action? OnDragEntered; + + /// + /// 드래그 종료 시 발생하는 이벤트입니다. + /// + public event Action? OnDragEnded; + + /// + /// 드롭 완료 시 발생하는 이벤트입니다. + /// + public event Action? OnDropped; + + /// + /// 드래그를 시작합니다. + /// + /// 드래그할 아이템 + public void StartDrag(TreeListItemData draggedItem) + { + if (IsDragging) + { + return; + } + + DraggedItem = draggedItem; + IsDragging = true; + + OnDragStarted?.Invoke(draggedItem); + } + + /// + /// 드래그 중에 마우스가 다른 아이템 위에 있을 때 호출됩니다. + /// + /// 현재 마우스 위에 있는 아이템 + public void OnDragOver(TreeListItemData? targetItem) + { + if (!IsDragging || DraggedItem == null) + { + return; + } + + OnDragEntered?.Invoke(DraggedItem, targetItem); + } + + /// + /// 드래그를 종료합니다. + /// + public void EndDrag() + { + if (!IsDragging || DraggedItem == null) + { + return; + } + + OnDragEnded?.Invoke(DraggedItem); + + IsDragging = false; + DraggedItem = null; + } + + /// + /// 드래그된 아이템을 대상 아이템에 드롭합니다. + /// + /// 드롭 대상 아이템 (null이면 루트 레벨) + /// 대상 부모 내에서의 삽입 위치 (-1이면 끝에 추가) + /// 드롭 성공 여부 + public bool TryDrop(TreeListItemData? targetItem, int insertIndex = -1) + { + if (!IsDragging || DraggedItem == null) + { + return false; + } + + // 자기 자신에게 드롭하는 경우 무시 + if (targetItem != null && targetItem == DraggedItem) + { + EndDrag(); + return false; + } + + // 순환 참조 검사 (드래그 아이템이 드롭 대상의 부모인 경우) + if (targetItem != null && IsAncestorOf(DraggedItem, targetItem)) + { + EndDrag(); + return false; + } + + OnDropped?.Invoke(DraggedItem, targetItem); + + EndDrag(); + return true; + } + + /// + /// 첫 번째 아이템이 두 번째 아이템의 조상인지 확인합니다. + /// 순환 참조를 방지하기 위해 사용됩니다. + /// + /// 조상일 가능성이 있는 아이템 + /// 후손일 가능성이 있는 아이템 + /// 조상-후손 관계이면 true + public static bool IsAncestorOf(TreeListItemData potentialAncestor, TreeListItemData potentialDescendant) + { + var current = potentialDescendant; + + while (current != null) + { + // 재귀적으로 부모를 찾는 로직 + // TreeListItemData는 부모 참조가 없으므로, 전체 리스트에서 찾아야 함 + // 여기서는 간단한 버전으로, 자식 리스트를 통해 확인 + var parent = FindParent(current, potentialAncestor); + + if (parent == null) + { + break; + } + + if (parent == potentialAncestor) + { + return true; + } + + current = parent; + } + + return false; + } + + /// + /// 주어진 아이템의 부모를 찾습니다. + /// + /// 자식 아이템 + /// 검색 제한 (이 아이템이 나타나면 멈춤) + /// 부모 아이템 (없으면 null) + private static TreeListItemData? FindParent(TreeListItemData child, TreeListItemData searchLimit) + { + // 이 구현은 전체 트리를 순회하므로 성능이 낮음 + // 실제 구현에서는 TreeListItemData에 부모 참조를 추가하는 것이 나음 + // 여기서는 시연 목적으로 간단하게 구현 + return null; + } + + /// + /// 모든 드래그 & 드롭 상태를 리셋합니다. + /// + public void Reset() + { + IsDragging = false; + DraggedItem = null; + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/List/Tree/TreeListDragDropManager.cs.meta b/Assets/Scripts/UVC/UI/List/Tree/TreeListDragDropManager.cs.meta new file mode 100644 index 00000000..f1b60a0f --- /dev/null +++ b/Assets/Scripts/UVC/UI/List/Tree/TreeListDragDropManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8046917725784a74594c9d147fe3e0e6 \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/List/Tree/TreeListItem.cs b/Assets/Scripts/UVC/UI/List/Tree/TreeListItem.cs index 540f7c1a..861c7ce5 100644 --- a/Assets/Scripts/UVC/UI/List/Tree/TreeListItem.cs +++ b/Assets/Scripts/UVC/UI/List/Tree/TreeListItem.cs @@ -5,117 +5,831 @@ using UnityEngine.UI; namespace UVC.UI.List.Tree { + /// + /// 트리 리스트의 개별 아이템을 표시하고 관리하는 UI 클래스입니다. + /// + /// 역할: + /// 1. 아이템의 이름을 화면에 표시 + /// 2. 자식 아이템 펼침/접힘 기능 관리 + /// 3. 아이템 선택/선택 해제 표시 + /// 4. 사용자 입력(클릭, 키) 처리 + /// 5. 데이터 변경 감지 및 UI 업데이트 + /// + /// 구조: + /// 📦 TreeListItem (이 클래스) + /// ├─ 📝 valueText: 아이템 이름 표시 (TextMeshPro) + /// ├─ 🔘 childExpand: 펼침/접힘 버튼 + /// ├─ 📦 childContainer: 자식 아이템들을 담는 컨테이너 + /// ├─ 🎨 selectedBg: 선택됨 표시 배경 + /// └─ 🔘 itemButton: 클릭 감지 버튼 + /// + /// MonoBehaviour란? + /// Unity의 모든 GameObject가 가져야 할 기본 클래스입니다. + /// Update, OnDestroy 같은 Unity의 생명주기 메서드를 사용할 수 있습니다. + /// public class TreeListItem : MonoBehaviour { + #region UI 컴포넌트 참조 (UI Component References) + + /// + /// 이 아이템을 관리하는 부모 TreeList입니다. + /// + /// 용도: + /// - 선택 상태 변경 시 TreeList에 알림 + /// - 새 자식 아이템 생성 시 프리팹 가져오기 + /// - 키보드 입력(Ctrl, Shift) 상태 전달 + /// + /// [SerializeField]란? + /// Inspector에서 이 값을 직접 할당할 수 있게 해줍니다. + /// (private이지만 Unity가 특별히 접근 가능) + /// [SerializeField] protected TreeList control; + /// + /// 이 아이템의 이름을 표시하는 텍스트 UI입니다. + /// + /// TMPro.TextMeshProUGUI란? + /// TextMeshPro는 Unity의 고급 텍스트 시스템입니다. + /// 일반 Text보다 더 예쁘고 빠릅니다. + /// + /// 사용: + /// valueText.text = "새로운 이름"; // 화면에 표시되는 텍스트 변경 + /// [SerializeField] protected TMPro.TextMeshProUGUI valueText; + /// + /// 트리의 자식을 펼침/접힘하는 화살표 버튼입니다. + /// + /// 특징: + /// - 자식이 없으면 숨겨집니다 + /// - 자식이 펼쳐지면 ▼ 모양 + /// - 자식이 접혀있으면 ▶ 모양 + /// - 클릭 시 ToggleChild() 메서드 호출 + /// + /// 회전 애니메이션: + /// DORotate()를 사용해 부드럽게 회전합니다. + /// [SerializeField] protected Button childExpand; + /// + /// 모든 자식 아이템들을 담는 컨테이너 GameObject입니다. + /// + /// 역할: + /// - 자식 아이템들을 묶음으로 보관 + /// - 펼침/접힘 시 이 컨테이너 전체를 표시/숨김 + /// - SetActive(true/false)로 온/오프 제어 + /// + /// GameObject란? + /// Unity의 모든 객체(씬의 모든 것)의 기본 단위입니다. + /// 게임 오브젝트는 여러 컴포넌트를 가질 수 있습니다. + /// [SerializeField] protected GameObject childContainer; + /// + /// 자식 아이템들이 실제로 배치되는 부모 Transform입니다. + /// + /// RectTransform란? + /// UI 요소의 위치, 크기, 회전을 관리하는 컴포넌트입니다. + /// Canvas 아래의 모든 UI 요소가 RectTransform을 가집니다. + /// + /// 용도: + /// - childRoot.childCount: 현재 몇 개의 자식이 있는지 확인 + /// - new TreeListItem을 Instantiate할 때 부모로 지정 + /// + /// 구조 예: + /// childRoot (이것) + /// ├─ 자식1 (TreeListItem) + /// ├─ 자식2 (TreeListItem) + /// └─ 자식3 (TreeListItem) + /// [SerializeField] protected RectTransform childRoot; + /// + /// 아이템이 선택되었을 때 배경으로 표시되는 이미지입니다. + /// + /// 예: 파일 탐색기에서 파일을 선택했을 때 파일 이름 뒤의 파란 배경 + /// + /// 동작: + /// - IsSelected가 true면 selectedBg.gameObject.SetActive(true) + /// - IsSelected가 false면 selectedBg.gameObject.SetActive(false) + /// - 모든 레벨의 선택된 아이템의 selectedBg 왼쪽이 정렬됨 + /// + [SerializeField] + protected Image selectedBg; + + /// + /// 아이템 전체를 클릭 가능하게 하는 버튼 컴포넌트입니다. + /// + /// Button이란? + /// 사용자가 클릭하면 onClick 이벤트를 발생시킵니다. + /// 이 경우 OnItemClicked() 메서드가 호출됩니다. + /// + /// onClick.AddListener는? + /// 버튼이 클릭되면 리스너 함수를 호출하라는 뜻입니다. + /// 예: itemButton.onClick.AddListener(OnItemClicked); + /// → itemButton을 클릭하면 OnItemClicked() 실행 + /// + [SerializeField] + protected Button itemButton; + + #endregion + + #region 데이터 필드 (Data Fields) + + /// + /// 이 UI 아이템이 표시하는 데이터 객체입니다. + /// + /// 구조: + /// TreeListItem (UI - 화면에 보이는 것) + /// ↓ + /// TreeListItemData (데이터 - 실제 정보) + /// + /// 관계: + /// - TreeListItemData에 이름, 자식 목록, 선택 상태 등이 저장됨 + /// - TreeListItem은 이 데이터를 화면에 표시하고 상호작용 처리 + /// + /// 예: + /// data.Name = "폴더" + /// → valueText.text = "폴더"로 화면에 표시 + /// + /// data.IsSelected = true + /// → selectedBg가 활성화되어 선택 표시 + /// + /// ? 연산자 (nullable): + /// data는 null일 수 있습니다. + /// 따라서 사용 전에 null 체크를 해야 합니다. + /// if (data == null) return; + /// protected TreeListItemData? data; + /// + /// data 속성을 읽기 전용으로 공개합니다. + /// + /// 문법: + /// public TreeListItemData? Data => data; + /// = getter 전용 프로퍼티 + /// = 외부에서 이 값을 읽을 수 있지만 변경할 수 없음 + /// + /// 사용 예: + /// if (treeItem.Data != null) + /// { + /// Debug.Log(treeItem.Data.Name); // 읽기만 가능 + /// } + /// + public TreeListItemData? Data => data; + + /// + /// 펼침/접힘 애니메이션이 진행 중인지를 나타내는 플래그입니다. + /// + /// 용도: 애니메이션이 끝나기 전에 다시 클릭하는 것을 방지합니다. + /// + /// 시나리오: + /// 1. 사용자가 화살표 버튼 클릭 + /// 2. isAnimating = true (애니메이션 시작) + /// 3. 0.3초 동안 화살표가 회전 + /// 4. 애니메이션 완료 → isAnimating = false + /// + /// 중간에 다시 클릭해도: + /// if (isAnimating) return; ← 여기서 무시됨 + /// + /// 이렇게 하는 이유: 애니메이션이 겹치면 버그가 발생할 수 있음 + /// protected bool isAnimating = false; - public void Init(TreeListItemData data, TreeList control) + #endregion + + #region 초기화 (Initialization) + + /// + /// 이 TreeListItem을 초기화합니다. + /// + /// 호출 시점: + /// - 새로운 TreeListItem이 생성될 때 + /// - Instantiate 직후 에 Init()이 호출됨 + /// + /// 초기화 과정: + /// 1. UI 요소 설정 (이름 표시, 이벤트 연결) + /// 2. 자식이 있으면 자식 UI 생성 + /// 3. 데이터 변경 이벤트 구독 + /// 4. 버튼 클릭 이벤트 구독 + /// 5. 선택 상태 UI 업데이트 + /// 6. 선택된 배경 위치 정렬 + /// + /// 매개변수: + /// - data: 표시할 데이터 + /// - control: 부모 TreeList (선택 관리, 클릭 처리 등) + /// + /// 사용 예: + /// var item = Instantiate(prefab); + /// item.Init(treeData, treeList); + /// + public void Init(TreeListItemData data, TreeList control, TreeListDragDropManager dragDropManager) { + // 1. 기본 정보 할당 this.control = control; this.data = data; - valueText.text = data.generalName; - Debug.Log("Creating children for " + data.generalName+", "+ data.children.Count); - if (data.children.Count == 0) + // 2. 아이템 이름을 UI에 표시 + valueText.text = data.Name; + + // 3. 자식 아이템들을 UI로 생성 + Debug.Log("Creating Children for " + data.Name + ", " + data.Children.Count); + + // 자식이 없는 경우 + if (data.Children.Count == 0) { + // 펼침 버튼 숨기기 (자식이 없으니까 펼칠 게 없음) childExpand.gameObject.SetActive(false); + + // 자식 컨테이너도 숨기기 childContainer.SetActive(false); } + // 자식이 있는 경우 else { - foreach (var childData in data.children) + // 각 자식 데이터에 대해 UI 생성 + foreach (var childData in data.Children) { - CreateItem(childData); + CreateItem(childData); // 재귀적으로 트리 구조 생성 } + + // 펼침 버튼과 컨테이너 표시 childExpand.gameObject.SetActive(true); childContainer.SetActive(true); + + // 화살표 방향 설정 (초기에는 펼쳐짐) SetExpand(); } + // 4. 데이터 변경 감지 구독 + // 데이터의 이름, 자식 목록 등이 변경되면 OnDataChanged 호출 + data.OnDataChanged += OnDataChanged; + + // 데이터의 선택 상태가 변경되면 OnSelectionChanged 호출 + data.OnSelectionChanged += OnSelectionChanged; + + // 5. 버튼 클릭 이벤트 구독 + // 아이템을 클릭하면 OnItemClicked 메서드 호출 + if (itemButton != null) + { + itemButton.onClick.AddListener(OnItemClicked); + } + + // 6. 현재 선택 상태 UI에 반영 + // data.IsSelected가 true이면 배경 표시 + UpdateSelectionUI(); + + // 7. 선택 배경의 왼쪽 정렬 (모든 레벨에서 일직선) + AlignSelectedBgToRoot(); + + // ✅ 드래그 & 드롭 핸들러 설정 + var dragHandler = gameObject.GetComponent(); + if (dragHandler == null) + { + dragHandler = gameObject.AddComponent(); + Debug.Log($"[TreeListItem.Init] 새로운 TreeListItemDragHandler 추가: {data.Name}"); + } + + dragHandler.SetDragDropManager(this, control, dragDropManager); + dragHandler.enabled = true; + + Debug.Log($"[TreeListItem.Init] 초기화 완료: {data.Name}"); } + #endregion - public void ToggleChild() + #region 데이터 변경 처리 (Data Change Handlers) + + /// + /// 데이터가 변경되었을 때 호출되는 메서드입니다. + /// + /// 호출되는 경우: + /// 1. data.Name이 변경됨 + /// 2. data.Children이 추가/제거됨 + /// 3. data.IsExpanded가 변경됨 + /// + /// 역할: + /// - 화면에 표시된 내용과 데이터를 동기화 + /// - 필요한 UI 컴포넌트 업데이트 + /// + /// 데이터 바인딩이란? + /// 데이터가 변경되면 UI가 자동으로 업데이트되는 패턴입니다. + /// 이렇게 하면 데이터와 UI를 동기화 유지하기 쉽습니다. + /// + private void OnDataChanged(TreeListItemData changedData) { - // 애니메이션이 진행 중이면 중복 호출을 방지합니다. - if (isAnimating) + if (data == null) return; + + // 이름이 변경된 경우 + if (valueText.text != data.Name) + { + valueText.text = data.Name; + } + + // 자식 목록이 변경된 경우 + // UI의 자식 개수와 데이터의 자식 개수 비교 + int currentChildCount = childRoot.childCount; + if (currentChildCount != data.Children.Count) + { + // 자식이 추가된 경우 + if (data.Children.Count > currentChildCount) + { + // 새로운 자식들을 UI로 생성 + for (int i = currentChildCount; i < data.Children.Count; i++) + { + CreateItem(data.Children[i]); + } + } + // 자식이 제거된 경우 + else if (data.Children.Count < currentChildCount) + { + // 필요에 따라 UI 아이템 제거 처리 + while (childRoot.childCount > data.Children.Count) + { + // 마지막 자식부터 하나씩 삭제 + Destroy(childRoot.GetChild(childRoot.childCount - 1).gameObject); + } + } + + // 펼침 버튼 표시 여부 결정 + // 자식이 있으면 버튼 표시, 없으면 숨김 + childExpand.gameObject.SetActive(data.Children.Count > 0); + + // 자식이 있고 컨테이너가 비활성화 상태면 활성화 + if (data.Children.Count > 0 && !childContainer.activeSelf) + { + childContainer.SetActive(true); + SetExpand(); + } + } + } + + #endregion + + #region 선택 상태 관리 (Selection Management) + + /// + /// 아이템의 선택 상태가 변경되었을 때 호출됩니다. + /// + /// 호출 시기: + /// - data.IsSelected = true/false 일 때 + /// + /// 동작: + /// - UpdateSelectionUI() 호출해서 화면 업데이트 + /// + /// 매개변수: + /// - changedData: 변경된 데이터 (이 경우 항상 this.data와 같음) + /// - isSelected: 새로운 선택 상태 (true = 선택됨, false = 해제됨) + /// + private void OnSelectionChanged(TreeListItemData changedData, bool isSelected) + { + // 선택 상태 UI 업데이트 (배경 표시/숨김) + UpdateSelectionUI(); + } + + /// + /// 선택 상태에 따라 UI를 업데이트합니다. + /// + /// 동작: + /// - data.IsSelected = true → selectedBg 표시 (배경 보이기) + /// - data.IsSelected = false → selectedBg 숨김 (배경 숨기기) + /// + /// 예시: + /// data.IsSelected = true + /// ↓ + /// selectedBg.gameObject.SetActive(true) + /// ↓ + /// 화면에 파란 배경이 나타남 + /// + private void UpdateSelectionUI() + { + if (data == null) return; + // IsSelected 상태에 따라 배경 표시/숨김 + selectedBg.gameObject.SetActive(data.IsSelected); + } + + #endregion + + #region 위치 정렬 (Position Alignment) + + /// + /// 모든 선택 배경(selectedBg)의 왼쪽 위치를 정렬합니다. + /// + /// 목표: + /// 📌 폴더1 + /// 📌├─ 파일1 + /// 📌├─ 파일2 + /// 📌└─ 폴더2 + /// 📌├─ 파일3 + /// + /// 모든 선택 배경의 왼쪽이 📌 위치에서 시작하도록 합니다. + /// + /// 이렇게 하는 이유: + /// - 깔끔한 UI 표현 + /// - 다중 레벨 트리에서 시각적 일관성 유지 + /// + /// 좌표 변환: + /// - 월드 좌표(World Coordinate): 게임 전체에서의 절대 위치 + /// - 로컬 좌표(Local Coordinate): 부모 기준 상대 위치 + /// - TransformPoint: 로컬 좌표를 월드 좌표로 변환 + /// + private void AlignSelectedBgToRoot() + { + if (selectedBg == null) return; + + // 최상위 부모(루트)의 TreeListItem 찾기 + TreeListItem? rootItem = GetRootTreeListItem(); + + // 루트가 없거나 이 아이템이 루트면 정렬할 필요 없음 + if (rootItem == null || rootItem == this) { return; } + + // 루트의 selectedBg와 현재 selectedBg의 RectTransform 가져오기 + RectTransform rootSelectedBgRect = rootItem.selectedBg.rectTransform; + RectTransform currentSelectedBgRect = selectedBg.rectTransform; + + // 루트의 배경 왼쪽 끝 위치를 월드 좌표로 계산 + // TransformPoint: 로컬 좌표 → 월드 좌표 변환 + // xMin: RectTransform의 왼쪽 끝 위치 + Vector3 rootLeftPos = rootSelectedBgRect.TransformPoint(new Vector3(rootSelectedBgRect.rect.xMin, 0, 0)); + + // 현재 배경의 왼쪽 끝 위치를 월드 좌표로 계산 + Vector3 currentLeftPos = currentSelectedBgRect.TransformPoint(new Vector3(currentSelectedBgRect.rect.xMin, 0, 0)); + + // 두 위치의 차이 계산 + // 루트 위치 - 현재 위치 = 조정해야 할 거리 + Vector3 offset = rootLeftPos - currentLeftPos; + + // 현재 배경의 위치를 조정 + // anchoredPosition: 부모 기준 위치 (로컬 좌표) + currentSelectedBgRect.anchoredPosition += new Vector2(offset.x, 0); + } + + #endregion + + #region 계층 구조 탐색 (Hierarchy Navigation) + + /// + /// 최상위 부모(루트)의 TreeListItem을 찾습니다. + /// + /// 트리 구조: + /// TreeListItem1 (루트) + /// ├─ TreeListItem2 (부모) + /// │ └─ TreeListItem3 (자식 ← 이 메서드를 호출하면) + /// └─ TreeListItem4 + /// + /// 반환값: TreeListItem1 + /// + /// 동작: + /// 1. 현재 객체의 부모 확인 + /// 2. 부모가 TreeListItem을 가지는지 확인 + /// 3. 가지면 그 부모의 GetRootTreeListItem() 재귀 호출 + /// 4. 루트에 도달할 때까지 반복 + /// + /// 재귀(Recursion)란? + /// 함수가 자기 자신을 호출하는 것입니다. + /// 계층 구조를 탐색하는 데 효과적입니다. + /// + private TreeListItem? GetRootTreeListItem() + { + // 현재 객체의 부모부터 시작 + Transform current = transform.parent; + + // 루트에 도달할 때까지 계속 탐색 + while (current != null) + { + // 현재 레벨에 TreeListItem 컴포넌트가 있는지 확인 + TreeListItem parentItem = current.GetComponent(); + if (parentItem != null) + { + // TreeListItem을 찾았으므로, 그 부모의 루트를 다시 찾음 (재귀) + TreeListItem? grandParent = parentItem.GetRootTreeListItem(); + if (grandParent != null) + { + // 할아버지 레벨의 루트 반환 + return grandParent; + } + else + { + // 할아버지가 없으면 이 부모가 루트 + return parentItem; + } + } + // 현재 레벨에 TreeListItem이 없으면 더 상위로 + current = current.parent; + } + // 루트까지 탐색해도 TreeListItem을 찾지 못함 (이 객체가 루트) + return null; + } + + #endregion + + #region 입력 처리 (Input Handling) + + /// + /// 아이템을 클릭했을 때 호출됩니다. + /// + /// 호출 시점: + /// - itemButton.onClick 이벤트 발생 시 + /// = 사용자가 이 아이템을 마우스로 클릭했을 때 + /// + /// 처리 순서: + /// 1. 데이터의 OnClickAction 실행 (있으면) + /// 2. Ctrl, Shift 키 상태 감지 + /// 3. TreeList에 클릭 정보 전달 (다중 선택 로직) + /// + /// 키 입력: + /// - Ctrl 클릭: 현재 선택을 유지하면서 이 아이템 토글 (다중 선택) + /// - Shift 클릭: 마지막 선택부터 이 아이템까지 범위 선택 + /// - 일반 클릭: 이 아이템만 선택 + /// + /// 파일 탐색기와 동일한 동작: + /// Windows 탐색기를 생각하면 쉽습니다. + /// - 클릭: 한 파일만 선택 + /// - Ctrl+클릭: 여러 파일 선택 (기존 선택 유지) + /// - Shift+클릭: 범위 선택 + /// + private void OnItemClicked() + { + if (data == null) return; + + // 1️. 데이터에 등록된 클릭 액션 실행 (있으면) + // ?. 연산자: null이면 실행 안 함 + // 예: 펼침/접힘 버튼 클릭 시 자동으로 호출되는 액션 + data.OnClickAction?.Invoke(data); + + // 2️. Ctrl 키 상태 감지 + // LeftControl(왼쪽) 또는 RightControl(오른쪽) 중 하나라도 누르고 있으면 true + bool ctrlPressed = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl); + + // 3️. Shift 키 상태 감지 + bool shiftPressed = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift); + + // 4️. 부모 TreeList에 클릭 정보 전달 + // TreeList는 이 정보를 받아서 선택 로직을 처리합니다. + // (단일 선택 / 다중 선택 / 범위 선택 등) + control.OnItemClicked(data, ctrlPressed, shiftPressed); + } + + #endregion + + #region 펼침/접힘 (Expand/Collapse) + + /// + /// 자식 아이템의 펼침/접힘을 토글합니다. (펼쳐있으면 접고, 접혀있으면 펼침) + /// + /// 호출 시점: + /// - childExpand 버튼을 클릭했을 때 + /// + /// 동작: + /// 1. 애니메이션이 진행 중이면 무시 (중복 클릭 방지) + /// 2. data.IsExpanded 토글 (true ↔ false) + /// 3. childContainer 활성화/비활성화 + /// 4. 0.3초에 걸쳐 화살표 회전 애니메이션 실행 + /// + /// UI 피드백: + /// 애니메이션이 있으면 사용자가 반응을 확인할 수 있습니다. + /// 즉시 완료되는 것보다 더 자연스럽고 좋은 경험입니다. + /// + public void ToggleChild() + { + // 1️. 애니메이션 진행 중이면 중복 호출 방지 + if (isAnimating) return; + + // 플래그 설정: 애니메이션 시작 isAnimating = true; - data.isExpanded = !data.isExpanded; + // 2️. 펼침/접힘 상태 토글 + // ! 연산자: 반대로 변경 (true → false, false → true) + data!.IsExpanded = !data.IsExpanded; - childContainer.SetActive(data.isExpanded); + // 3️. 자식 컨테이너 표시/숨김 + // IsExpanded가 true면 표시, false면 숨김 + childContainer.SetActive(data.IsExpanded); + // 4️. 0.3초에 걸쳐 펼침/접힘 애니메이션 실행 SetExpand(0.3f); } + /// + /// 펼침/접힘 화살표의 회전 애니메이션을 실행합니다. + /// + /// 파라미터: + /// - duration: 애니메이션 지속 시간 (초) + /// 기본값 0.0 = 즉시 완료 (애니메이션 없음) + /// 0.3 = 0.3초에 걸쳐 회전 + /// + /// 동작: + /// - 펼쳐짐 (IsExpanded=true): 화살표를 0도로 회전 (▼ 모양) + /// - 접혀짐 (IsExpanded=false): 화살표를 90도로 회전 (▶ 모양) + /// - 애니메이션 완료 후 isAnimating 플래그 리셋 + /// + /// DORotate란? (Tweening 라이브러리) + /// 부드럽게 회전하는 애니메이션을 쉽게 만들어줍니다. + /// duration 시간에 걸쳐 지정된 각도까지 회전합니다. + /// + /// OnComplete란? + /// 애니메이션이 완료되면 호출되는 콜백입니다. + /// 람다 식(=>)으로 익명 함수를 정의합니다. + /// private void SetExpand(float duration = 0.0f) { + // 1️. 자식이 있는지 확인해서 펼침 버튼 표시 여부 결정 childExpand.gameObject.SetActive(childRoot.childCount > 0); + + // 2️. 자식이 있는 경우에만 애니메이션 실행 if (childRoot.childCount > 0) { - if (data != null) data.isExpanded = childContainer.activeSelf == true; - // 애니메이션을 위해 현재 각도에서 목표 각도로 회전시킵니다. - childExpand.transform.DORotate(new Vector3(0, 0, data.isExpanded ? 0 : 90), duration) + // UI 상태와 데이터 동기화 + if (data != null) data.IsExpanded = childContainer.activeSelf == true; + // 3️. 화살표 회전 애니메이션 실행 + // DORotate(목표 각도, 지속 시간) + // IsExpanded가 true면 0도 (▼), false면 90도 (▶) + childExpand.transform.DORotate(new Vector3(0, 0, data.IsExpanded ? 0 : 90), duration) .OnComplete(() => { - // 애니메이션이 완료되면 플래그를 초기화합니다. + // 4️. 애니메이션이 완료되면 플래그 리셋 + // 이제 다시 ToggleChild() 호출 가능 isAnimating = false; }); } else { - if(data != null) data.isExpanded = false; + // 자식이 없으면 IsExpanded는 항상 false + if (data != null) data.IsExpanded = false; } } - public void AddChild() - { - TreeListItemData itemData = new TreeListItemData - { - generalName = data?.generalName + "." + (data?.children.Count + 1), - }; - AddChild(itemData); - } + #endregion + #region 자식 관리 (Child Management) + + /// + /// 새로운 자식 아이템을 추가합니다. + /// + /// 호출 시점: + /// - 프로그램이 런타임에 새 자식을 추가할 때 + /// - 예: 폴더에 파일을 추가했을 때 + /// + /// 동작: + /// 1. 새 자식 UI 생성 (CreateItem) + /// 2. 자식 컨테이너 활성화 + /// 3. 펼침 애니메이션 (자동으로 펼침) + /// + /// 매개변수: + /// - data: 추가할 자식 데이터 + /// + /// 사용 예: + /// treeItem.AddChild(newChildData); + /// // → 새 자식이 자동으로 UI에 추가되고 펼쳐짐 + /// public void AddChild(TreeListItemData data) { + // 1️. 새 자식 UI 생성 CreateItem(data); + // 2️. 자식 컨테이너 활성화 (표시) childContainer.SetActive(true); + + // 3️. 0.3초에 걸쳐 펼침 애니메이션 실행 + // 사용자가 새 자식이 추가되었음을 자연스럽게 인식 SetExpand(0.3f); } + /// + /// 자식 데이터를 받아 UI TreeListItem으로 생성합니다. + /// + /// 호출 시점: + /// - Init()에서 기존 자식들을 UI로 생성할 때 + /// - OnDataChanged()에서 추가된 자식을 UI로 생성할 때 + /// - AddChild()에서 새 자식을 추가할 때 + /// + /// 동작: + /// 1. control.ItemPrefab를 childRoot 아래에 인스턴스화 + /// 2. 생성된 item의 Init() 메서드 호출 + /// 3. 생성된 item 반환 + /// + /// Instantiate란? + /// 프리팹(템플릿)을 복제해서 새로운 객체를 만드는 함수입니다. + /// Instantiate(프리팹, 부모, 복제) + /// + /// 매개변수: + /// - data: 생성할 아이템의 데이터 + /// + /// 반환값: + /// - 생성된 TreeListItem 컴포넌트 + /// + /// 재귀 구조: + /// CreateItem은 계속 자식을 생성하므로 + /// 깊이 있는 트리 구조를 만들 수 있습니다. + /// protected TreeListItem CreateItem(TreeListItemData data) { - TreeListItem item = GameObject.Instantiate(control.ItemPrefab, childRoot); - item.Init(data, control); + // 1️. 프리팹을 복제해서 새로운 TreeListItem 생성 + // Instantiate(원본, 부모, 옵션) + // control.ItemPrefab: UI 아이템 템플릿 + // childRoot: 새 아이템의 부모 Transform + TreeListItem item = GameObject.Instantiate( + control.ItemPrefab, // 복제할 프리팹 + childRoot // 부모로 배치할 위치 + ); + + // 2️. 생성된 아이템 초기화 + item.Init(data, control, control.DragDropManager); + + // 3️. 생성된 아이템 반환 return item; } + #endregion + + #region 제거 (Deletion) + + /// + /// 이 TreeListItem과 관련된 모든 리소스를 정리하고 삭제합니다. + /// + /// 호출 시점: + /// - 트리에서 아이템을 제거하고 싶을 때 + /// - 프로그램이 명시적으로 아이템을 삭제할 때 + /// + /// 정리 작업: + /// 1. 데이터 변경 이벤트 구독 해제 + /// 2. 버튼 클릭 이벤트 구독 해제 + /// 3. GameObject 삭제 + /// + /// 왜 구독을 해제하나? + /// - 이벤트를 구독하는 것은 메모리 연결을 만듭니다. + /// - 해제하지 않으면 메모리 누수가 발생할 수 있습니다. + /// - 따라서 삭제 전에 반드시 해제해야 합니다. + /// + /// 메모리 누수란? + /// 불필요한 메모리가 해제되지 않고 계속 사용되는 문제입니다. + /// 게임이 계속 실행되면서 메모리 사용량이 증가해서 + /// 결국 게임이 느려지거나 충돌할 수 있습니다. + /// public void Delete() { + // 1️. 데이터 변경 이벤트 구독 해제 + if (data != null) + { + // -= 연산자: 이벤트에서 리스너 제거 + data.OnDataChanged -= OnDataChanged; + data.OnSelectionChanged -= OnSelectionChanged; + } + + // 2️. 버튼 클릭 이벤트 구독 해제 + if (itemButton != null) + { + itemButton.onClick.RemoveListener(OnItemClicked); + } + + // 3️. 이 GameObject 삭제 + // 게임 실행 중에 오브젝트를 제거합니다. GameObject.Destroy(gameObject); } + + /// + /// GameObject가 파괴될 때 자동으로 호출되는 Unity 메서드입니다. + /// + /// 호출 시점: + /// - Destroy(gameObject) 호출 후 실제 삭제되기 직전 + /// - 게임이 종료될 때 + /// - 씬이 언로드될 때 + /// + /// 용도: + /// - 정리 작업 (Clean-up) + /// - 리소스 해제 + /// - 이벤트 구독 해제 + /// + /// Delete()와의 차이: + /// - Delete(): 명시적으로 호출하는 메서드 + /// - OnDestroy(): Unity에서 자동으로 호출하는 메서드 + /// - 둘 다 같은 정리 작업을 합니다 (중복 방지) + /// + private void OnDestroy() + { + // 1️. 데이터 변경 이벤트 구독 해제 + if (data != null) + { + data.OnDataChanged -= OnDataChanged; + data.OnSelectionChanged -= OnSelectionChanged; + } + + // 2️. 버튼 클릭 이벤트 구독 해제 + if (itemButton != null) + { + itemButton.onClick.RemoveListener(OnItemClicked); + } + } + + #endregion } -} +} \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/List/Tree/TreeListItemData.cs b/Assets/Scripts/UVC/UI/List/Tree/TreeListItemData.cs index 8b5afaea..5f3ac1de 100644 --- a/Assets/Scripts/UVC/UI/List/Tree/TreeListItemData.cs +++ b/Assets/Scripts/UVC/UI/List/Tree/TreeListItemData.cs @@ -5,39 +5,542 @@ using System.Collections.Generic; namespace UVC.UI.List.Tree { - public class TreeListItemData : InfiniteScrollData + /// + /// 트리 구조 리스트에서 각 아이템이 갖는 데이터 클래스입니다. + /// + /// 트리 구조란? 폴더-파일처럼 상위(부모)와 하위(자식) 관계를 가진 계층 구조입니다. + /// 예: 📁 폴더 + /// ├─ 📄 파일1 + /// ├─ 📄 파일2 + /// └─ 📁 하위폴더 + /// └─ 📄 파일3 + /// + /// 이 클래스는 InfiniteScrollData를 상속하여 UI 스크롤 리스트와 연동됩니다. + /// + public class TreeListItemData { - /// - /// 일반 아이템 이름 - /// - public string generalName = string.Empty; + #region 이벤트 (Events) /// - /// 일반 아이템 옵션 + /// 데이터가 변경되었을 때 발생하는 이벤트입니다. + /// + /// 용도: 이 데이터의 속성(Name, Option 등)이 변경되면 + /// UI에 자동으로 반영되도록 통지합니다. + /// + /// 사용 예: + /// treeItem.OnDataChanged += (data) => Debug.Log("데이터 변경됨!"); /// - public string generalOption = string.Empty; + public Action? OnDataChanged; /// - /// 자식 확장 여부 + /// 선택 상태가 변경되었을 때 발생하는 이벤트입니다. + /// + /// 용도: 사용자가 이 아이템을 클릭해서 선택 또는 선택 해제했을 때 + /// 다른 시스템(예: 옵션 창)에 알려줍니다. + /// + /// 매개변수: + /// - TreeListItemData: 변경된 아이템 자신 + /// - bool: true면 선택됨, false면 선택 해제됨 + /// + /// 사용 예: + /// treeItem.OnSelectionChanged += (data, isSelected) => { + /// Debug.Log($"{data.Name}이 {(isSelected ? "선택됨" : "해제됨")}"); + /// }; /// - internal bool isExpanded = false; + public Action? OnSelectionChanged; + + #endregion + + #region 내부 필드 (Private Fields) /// - /// 카테고리 확장/축소 버튼 클릭 시 호출될 액션입니다. + /// 아이템의 이름을 저장하는 비공개 필드입니다. + /// + /// '_' 접두사를 붙인 이유: + /// 실제 데이터는 여기 저장하고, public 프로퍼티(Name)를 통해 + /// 접근을 제어합니다. (캡슐화) + /// + /// 예: _name = "폴더", Name 프로퍼티로 접근 + /// + private string _name = string.Empty; + + /// + /// 아이템의 추가 옵션 정보를 저장합니다. + /// + /// 용도: 읽기 전용, 숨김 속성 등의 추가 설정 정보 + /// 예: "readonly", "hidden", "locked" 등 + /// + private string _option = string.Empty; + + /// + /// 이 아이템의 자식들이 펼쳐져 있는지 여부를 나타냅니다. + /// + /// true: 자식들이 표시됨 (▼ 펼침 상태) + /// false: 자식들이 숨겨짐 (▶ 접혀있는 상태) + /// + /// 자식이 없으면 이 값은 의미가 없습니다. + /// + private bool _isExpanded = false; + + /// + /// 현재 아이템이 사용자에게 선택되어 있는지를 나타냅니다. + /// + /// true: 선택됨 (보통 배경색이 다르게 표시) + /// false: 선택 안 됨 (기본 상태) + /// + /// 예: 파일 탐색기에서 파일을 클릭했을 때 그 파일의 _isSelected = true + /// + private bool _isSelected = false; + + /// + /// 이 아이템의 하위 아이템들을 모두 저장하는 리스트입니다. + /// + /// 트리 구조 예: + /// 부모 (이 객체) + /// ├─ 자식1 + /// ├─ 자식2 + /// └─ 자식3 + /// + /// _children 리스트에 [자식1, 자식2, 자식3]이 저장됩니다. + /// 자식이 없으면 빈 리스트입니다. + /// + private List _children = new List(); + + #endregion + + #region 공개 프로퍼티 (Public Properties) + + /// + /// 아이템의 이름을 가져오거나 설정합니다. + /// + /// 동작: + /// - 가져올 때(get): _name의 값을 반환합니다. + /// - 설정할 때(set): + /// 1. 기존 값과 비교해서 정말 달라졌는지 확인 + /// 2. 다르면 새 값으로 변경 + /// 3. OnDataChanged 이벤트를 발생시켜 UI에 알림 + /// + /// 이렇게 하는 이유: 같은 값으로 변경되는 불필요한 갱신을 피합니다. + /// + /// 사용 예: + /// treeItem.Name = "새로운 이름"; // 자동으로 UI 업데이트 + /// string currentName = treeItem.Name; // 이름 읽기 + /// + public string Name + { + get => _name; + set + { + if (_name != value) + { + _name = value; + NotifyDataChanged(); // UI에 변경을 알림 + } + } + } + + /// + /// 아이템의 추가 옵션을 가져오거나 설정합니다. + /// + /// 동작: Name 프로퍼티와 동일하게 작동합니다. + /// 변경될 때마다 OnDataChanged 이벤트를 발생시킵니다. + /// + /// 사용 예: + /// treeItem.Option = "readonly"; // 읽기 전용으로 설정 + /// + public string Option + { + get => _option; + set + { + if (_option != value) + { + _option = value; + NotifyDataChanged(); + } + } + } + + /// + /// 이 아이템의 자식들이 펼쳐져 있는지 여부를 가져오거나 설정합니다. + /// + /// internal 접근제한자 이유: + /// 이것은 UI 시스템에서만 관리해야 하므로 외부에서 직접 접근할 수 없습니다. + /// (같은 어셈블리 내부에서만 접근 가능) + /// + /// true: 자식들이 표시됨 (트리 펼침) + /// false: 자식들이 숨겨짐 (트리 접힘) + /// + /// 사용 예: (UI 시스템에서만) + /// treeItem.IsExpanded = true; // 자식들을 표시 + /// + internal bool IsExpanded + { + get => _isExpanded; + set + { + if (_isExpanded != value) + { + _isExpanded = value; + NotifyDataChanged(); // 트리 구조 UI 갱신 + } + } + } + + /// + /// 이 아이템이 선택되어 있는지 여부를 가져오거나 설정합니다. + /// + /// 중요한 차이점: 다른 프로퍼티는 OnDataChanged를 호출하지만, + /// 이것은 OnSelectionChanged를 호출합니다. + /// 왜? 선택 상태는 UI 갱신이 아니라 + /// 선택 이벤트 처리가 필요하기 때문입니다. + /// + /// 사용 예: + /// treeItem.IsSelected = true; // 아이템 선택 + /// if (treeItem.IsSelected) { ... } // 선택 여부 확인 + /// + public bool IsSelected + { + get => _isSelected; + set + { + if (_isSelected != value) + { + _isSelected = value; + // OnSelectionChanged 이벤트 발생 + // 예: "폴더가 선택되었습니다" 같은 처리를 수행 + OnSelectionChanged?.Invoke(this, value); + } + } + } + + /// + /// 사용자가 확장/축소 버튼을 클릭했을 때 호출될 함수입니다. + /// + /// 용도: 트리의 화살표(▼/▶) 버튼을 클릭했을 때 + /// IsExpanded 상태를 변경하는 로직을 실행합니다. + /// + /// 누가 등록하나? UI 시스템 (TreeListItem 클래스) + /// + /// 사용 예: + /// treeItem.OnClickAction = (data) => { + /// data.IsExpanded = !data.IsExpanded; // 펼침/접힘 토글 + /// }; /// public Action? OnClickAction; - internal List children = new List(); - - public TreeListItemData() { } - - public TreeListItemData(string generalName, List? childrenItemData = null) + /// + /// 이 아이템의 모든 자식 아이템들을 가져오거나 설정합니다. + /// + /// internal 접근제한자 이유: + /// 자식 리스트는 AddChild, RemoveChild, ClearChildren 메서드로만 + /// 수정되어야 데이터 일관성이 보장됩니다. + /// + /// 동작: + /// - 가져올 때(get): _children 리스트 반환 + /// - 설정할 때(set): + /// 1. null이면 빈 리스트로 설정 (null 방지) + /// 2. OnDataChanged 이벤트 발생 + /// + /// ?? 연산자 설명: + /// childrenItemData ?? new List<>() + /// = childrenItemData가 null이면 빈 리스트 사용 + /// null이 아니면 childrenItemData 사용 + /// + internal List Children { - this.generalName = generalName; - if (childrenItemData != null) + get => _children; + set { - this.children = childrenItemData; + _children = value ?? new List(); + NotifyDataChanged(); } } + + #endregion + + #region 생성자 (Constructors) + + /// + /// 빈 TreeListItemData를 생성합니다. + /// + /// 초기값: + /// - Name: 빈 문자열 + /// - Option: 빈 문자열 + /// - IsExpanded: false (접혀있음) + /// - Children: 빈 리스트 (자식 없음) + /// + /// 사용 예: + /// var item = new TreeListItemData(); + /// item.Name = "새 폴더"; + /// + public TreeListItemData() + { + _name = string.Empty; + _option = string.Empty; + _isExpanded = false; + _children = new List(); + } + + /// + /// 이름과 선택적으로 자식 목록을 지정하여 TreeListItemData를 생성합니다. + /// + /// 매개변수: + /// - generalName: 아이템의 이름 (필수) + /// - childrenItemData: 초기 자식 목록 (선택사항, null 가능) + /// + /// 초기값: + /// - Name: generalName + /// - Option: 빈 문자열 + /// - IsExpanded: false + /// - Children: childrenItemData (null이면 빈 리스트) + /// + /// 사용 예: + /// // 간단한 아이템 생성 + /// var item1 = new TreeListItemData("폴더"); + /// + /// // 자식을 포함해서 생성 + /// var children = new List { item1 }; + /// var parent = new TreeListItemData("부모 폴더", children); + /// + public TreeListItemData(string generalName, List? childrenItemData = null) + { + _name = generalName; + _option = string.Empty; + _isExpanded = false; + _children = childrenItemData ?? new List(); + } + + #endregion + + #region 자식 관리 메서드 (Child Management Methods) + + /// + /// 이 아이템에 자식 아이템을 추가합니다. + /// + /// 동작: + /// 1. 자식을 _children 리스트에 추가 + /// 2. OnDataChanged 이벤트 발생 (UI 트리 구조 갱신) + /// + /// 사용 예: + /// parent.AddChild(child); // 부모에 자식 추가 + /// + /// 트리 구조 변화: + /// 변경 전: 변경 후: + /// 부모 부모 + /// ├─ 자식1 ├─ 자식1 + /// └─ 자식2 ├─ 자식2 + /// └─ 새자식 + /// + public void AddChild(TreeListItemData child) + { + _children.Add(child); + NotifyDataChanged(); // UI에 트리 구조 변경 알림 + } + + /// + /// 이 아이템에서 지정된 자식 아이템을 제거합니다. + /// + /// 동작: + /// 1. 자식을 _children 리스트에서 제거 + /// 2. OnDataChanged 이벤트 발생 + /// + /// 주의: 같은 이름의 첫 번째 자식만 제거됩니다. + /// (TreeListItemData의 == 연산자가 Name으로 비교하기 때문) + /// + /// 사용 예: + /// parent.RemoveChild(child); // 부모에서 자식 제거 + /// + /// 트리 구조 변화: + /// 변경 전: 변경 후: + /// 부모 부모 + /// ├─ 자식1 ├─ 자식1 + /// ├─ 자식2 + /// └─ 자식3 └─ 자식3 + /// (자식2 제거) + /// + public void RemoveChild(TreeListItemData child) + { + _children.Remove(child); + NotifyDataChanged(); // UI에 트리 구조 변경 알림 + } + + /// + /// 이 아이템의 모든 자식을 한 번에 제거합니다. + /// + /// 동작: + /// 1. _children 리스트를 완전히 비움 + /// 2. OnDataChanged 이벤트 발생 + /// + /// 주의: 자식들이 메모리에서 삭제되는 것은 아니고, + /// 이 아이템과의 연결만 끊어집니다. + /// (C#의 가비지 컬렉션이 필요 없으면 나중에 정리) + /// + /// 사용 예: + /// parent.ClearChildren(); // 모든 자식 제거 + /// + /// 트리 구조 변화: + /// 변경 전: 변경 후: + /// 부모 부모 + /// ├─ 자식1 (모든 자식 제거됨) + /// ├─ 자식2 + /// └─ 자식3 + /// + public void ClearChildren() + { + _children.Clear(); + NotifyDataChanged(); // UI에 트리 구조 변경 알림 + } + + #endregion + + #region 내부 메서드 (Internal Methods) + + /// + /// 데이터가 변경되었음을 UI 시스템에 알립니다. + /// + /// 동작: OnDataChanged 이벤트를 발생시킵니다. + /// 이를 통해 UI는 자동으로 이 아이템의 정보를 갱신합니다. + /// + /// 호출되는 시점: + /// - Name이나 Option이 변경될 때 + /// - 자식이 추가/제거될 때 + /// - IsExpanded 상태가 변경될 때 + /// + /// 왜 protected인가? + /// 이 클래스를 상속받은 자식 클래스에서도 호출할 수 있도록 하기 위함입니다. + /// + internal void NotifyDataChanged() + { + // OnDataChanged가 등록되어 있으면 실행 + // ?. 연산자: null이면 실행하지 않음 (null reference exception 방지) + OnDataChanged?.Invoke(this); + } + + #endregion + + #region 비교 연산자 (Comparison Operators) + + /// + /// 두 TreeListItemData 객체가 같은지 비교합니다. (== 연산자) + /// + /// 비교 기준: Name (아이템의 이름) + /// 즉, 이름이 같으면 같은 아이템으로 간주합니다. + /// + /// 비교 로직: + /// 1. 같은 객체인가? (메모리 주소가 같음) → true + /// 2. 둘 다 null이거나 하나가 null? → false (둘 다 null이면 true인데, 1번에서 처리) + /// 3. Name이 같은가? → true/false + /// + /// 사용 예: + /// var item1 = new TreeListItemData("파일"); + /// var item2 = new TreeListItemData("파일"); + /// var item3 = new TreeListItemData("폴더"); + /// + /// item1 == item2 // true (Name이 "파일"로 같음) + /// item1 == item3 // false (Name이 다름) + /// item1 == null // false + /// + /// 주의: 같은 이름이면 같은 아이템으로 취급되므로, + /// 실제로 다른 객체임에도 true가 될 수 있습니다. + /// 이는 의도된 설계입니다. + /// + public static bool operator ==(TreeListItemData? left, TreeListItemData? right) + { + // 같은 객체인지 확인 (메모리 주소 비교) + if (ReferenceEquals(left, right)) + { + return true; + } + + // 하나 이상이 null이면 false (ReferenceEquals에서 둘 다 null인 경우는 true 반환) + if (left is null || right is null) + { + return false; + } + + // 이름으로 비교 + return left.Name == right.Name; + } + + /// + /// 두 TreeListItemData 객체가 다른지 비교합니다. (!= 연산자) + /// + /// 동작: == 연산자의 결과를 반대(!)로 반환합니다. + /// + /// 사용 예: + /// if (item1 != item2) { ... } // 다른 아이템이면 실행 + /// + public static bool operator !=(TreeListItemData? left, TreeListItemData? right) + { + return !(left == right); + } + + #endregion + + #region 객체 메서드 (Object Methods) + + /// + /// 이 객체의 고유한 해시 코드를 반환합니다. + /// + /// 용도: 이 객체를 Dictionary나 HashSet 같은 컬렉션에 저장할 때 사용합니다. + /// + /// 해시 코드란? 객체를 빠르게 비교/검색하기 위한 고유 숫자입니다. + /// 같은 내용이면 같은 해시 코드를 반환해야 합니다. + /// + /// 우리의 기준: + /// Name의 해시 코드 = 이 객체의 해시 코드 + /// 왜? == 연산자에서 Name으로 비교하기 때문입니다. + /// + /// 사용 예: + /// int hash = item.GetHashCode(); + /// + /// // Dictionary에 저장 + /// Dictionary dict = new(); + /// dict[item] = "값"; // 내부적으로 GetHashCode() 사용 + /// + public override int GetHashCode() + { + return _name.GetHashCode(); + } + + /// + /// 이 객체가 다른 객체와 같은지 비교합니다. (Equals 메서드) + /// + /// 용도: 모든 C# 객체는 Equals 메서드를 가집니다. + /// 이 메서드를 오버라이드하여 우리의 비교 로직을 정의합니다. + /// + /// 동작: + /// 1. 다른 객체가 TreeListItemData 타입인지 확인 + /// 2. 맞으면 == 연산자로 비교 (Name으로 비교) + /// 3. 아니면 false 반환 + /// + /// 사용 예: + /// var item1 = new TreeListItemData("파일"); + /// var item2 = new TreeListItemData("파일"); + /// + /// item1.Equals(item2) // true + /// item1.Equals("파일") // false (문자열은 다른 타입) + /// item1 == item2 // true (== 연산자와 동일) + /// + /// GetHashCode()와 Equals의 관계: + /// - 같은 객체면 같은 해시 코드를 가져야 함 + /// - 우리의 경우: Name이 같으면 Equals는 true, 해시 코드도 같음 + /// - 이는 일관성 있게 설계되어 있습니다. + /// + public override bool Equals(object? obj) + { + // obj가 TreeListItemData 타입인지 확인 + if (obj is TreeListItemData other) + { + // TreeListItemData면 == 연산자로 비교 + return this == other; + } + + // 다른 타입이면 false + return false; + } + + #endregion } -} +} \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/List/Tree/TreeListItemDragHandler.cs b/Assets/Scripts/UVC/UI/List/Tree/TreeListItemDragHandler.cs new file mode 100644 index 00000000..cc53f983 --- /dev/null +++ b/Assets/Scripts/UVC/UI/List/Tree/TreeListItemDragHandler.cs @@ -0,0 +1,700 @@ +#nullable enable +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; + +namespace UVC.UI.List.Tree +{ + /// + /// TreeListItem의 드래그 & 드롭 입력을 처리하는 컴포넌트입니다. + /// + /// 역할: + /// 1. 마우스 입력 감지 (클릭, 드래그) + /// 2. 드래그 시각 피드백 (알파값 변경, 오프셋 이동) + /// 3. 드롭 대상 판단 (마우스 위치 기반) + /// 4. 드래그 매니저에 이벤트 전달 + /// + /// 구조: + /// - PointerDown: 마우스 클릭 감지 → 드래그 시작 준비 + /// - Drag: 마우스 이동 중 → 드래그 진행, 시각 피드백 + /// - PointerUp: 마우스 해제 → 드롭 처리 + /// + public class TreeListItemDragHandler : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler + { + /// + /// 부모 TreeListItem입니다. + /// + private TreeListItem? treeListItem; + + /// + /// 부모 TreeList입니다. + /// + private TreeList? treeList; + + /// + /// 드래그 & 드롭 매니저입니다. + /// + private TreeListDragDropManager? dragDropManager; + + private RectTransform? rectTransform; + + /// + /// 드래그 중 시각 피드백을 위한 CanvasGroup입니다. + /// + private CanvasGroup? canvasGroup; + + /// + /// 드래그 시작 시 원본 알파값입니다. + /// + private float originalAlpha = 1f; + + private Vector2 dragOffset = Vector2.zero; + + /// + /// 드래그 중 적용할 알파값입니다. + /// + [SerializeField] + private float dragAlpha = 0.5f; + + /// + /// 드래그 활성화 여부입니다. + /// + [SerializeField] + private bool enableDragDrop = true; + + /// + /// 드롭 위치 표시 막대 프리팹입니다. + /// + private Image? dropIndicator; + private RectTransform? dropIndicatorRect; + + /// + /// 드롭 표시기의 부모 (Content 또는 EntryRoot) + /// + private RectTransform? dropIndicatorParent; + + /// + /// 드래그 시작 시 아이템의 원본 부모입니다. + /// 드래그 후 원래 위치로 복구할 때 사용합니다. + /// + private Transform? originalParent; + private int originalSiblingIndex; + + + private void Awake() + { + rectTransform = GetComponent(); + // CanvasGroup 가져오기 (없으면 생성) + canvasGroup = GetComponent(); + if (canvasGroup == null) + { + canvasGroup = gameObject.AddComponent(); + } + + originalAlpha = canvasGroup.alpha; + } + + public void SetDragDropManager(TreeListItem item, TreeList list, TreeListDragDropManager manager) + { + treeListItem = item; + treeList = list; + dragDropManager = manager; + Debug.Log($"[TreeListItemDragHandler] 드래그 핸들러 설정: {item.Data?.Name ?? "Unknown"}"); + } + + /// + /// 드래그가 시작될 때 호출됩니다. (IBeginDragHandler) + /// OnPointerDown 이후 마우스가 약간 움직이면 자동으로 호출됩니다. + /// + public void OnBeginDrag(PointerEventData eventData) + { + Debug.Log($"[OnPointerDown]"); + if (!enableDragDrop || treeListItem?.Data == null || dragDropManager == null) + { + return; + } + + // 마우스 버튼이 왼쪽이 아니면 무시 + if (eventData.button != PointerEventData.InputButton.Left) + { + return; + } + + // 드래그 시작 준비: 마우스 위치와 아이템 위치의 오프셋 계산 + RectTransformUtility.ScreenPointToLocalPointInRectangle( + rectTransform, + eventData.position, + null, + out var localPoint); + + dragOffset = localPoint; + + Debug.Log($"[OnPointerDown] {treeListItem.Data.Name}에 포인터 다운, offset: {dragOffset}"); + } + + /// + /// 드래그 중에 마우스가 이동할 때 호출됩니다. (IDragHandler) + /// + public void OnDrag(PointerEventData eventData) + { + if (!enableDragDrop || dragDropManager == null || treeListItem?.Data == null || treeList == null || rectTransform == null) + { + Debug.LogWarning("[OnDrag] 필수 컴포넌트 누락 또는 비활성화됨"); + return; + } + + if (eventData.button != PointerEventData.InputButton.Left) + { + return; + } + + Debug.Log($"[OnDrag] {treeListItem.Data.Name} 드래그 중"); + + // 드래그 시작 처리 (첫 드래그 프레임) + if (!dragDropManager.IsDragging) + { + dragDropManager.StartDrag(treeListItem.Data); + + // 원본 부모와 위치 저장 + originalParent = rectTransform.parent; + originalSiblingIndex = rectTransform.GetSiblingIndex(); + + // 드래그 중 시각 피드백 + if (canvasGroup != null) + { + canvasGroup.alpha = dragAlpha; + canvasGroup.blocksRaycasts = false; + } + + // 드롭 위치 표시 막대 생성 + CreateDropIndicator(); + + Debug.Log($"[OnDrag] {treeListItem.Data.Name} 드래그 시작"); + } + + // 아이템이 마우스를 따라다니도록 위치 업데이트 + UpdateDragPosition(eventData); + + // 마우스 위의 드롭 대상 찾기 + var targetItem = GetItemAtMousePosition(eventData.position); + dragDropManager.OnDragOver(targetItem?.Data); + + // 드롭 위치 표시 업데이트 + UpdateDropIndicator(targetItem); + + if (targetItem != null) + { + Debug.Log($"[OnDrag] 드래그 중: {targetItem.Data?.Name ?? "Unknown"} 위에 있음"); + } + } + + /// + /// 드래그가 종료될 때 호출됩니다. (IEndDragHandler) + /// 마우스 버튼을 놓으면 자동으로 호출됩니다. + /// + public void OnEndDrag(PointerEventData eventData) + { + Debug.Log($"[OnPointerUp]"); + if (!enableDragDrop || dragDropManager == null) + { + return; + } + + Debug.Log($"[OnPointerUp] 드래그 완료"); + + // 원본 알파값으로 복구 + if (canvasGroup != null) + { + canvasGroup.alpha = originalAlpha; + canvasGroup.blocksRaycasts = true; + } + + // 원본 부모로 복구 (드래그 중 이동했던 위치 복구) + if (originalParent != null) + { + rectTransform?.SetParent(originalParent); + if (rectTransform != null) + { + rectTransform.SetSiblingIndex(originalSiblingIndex); + } + } + + // 드롭 위치 표시 막대 숨김 + HideDropIndicator(); + + // 드래그가 시작되지 않았으면 무시 + if (!dragDropManager.IsDragging) + { + dragDropManager.EndDrag(); + return; + } + + // 마우스 위치의 대상 아이템 찾기 + var targetItem = GetItemAtMousePosition(eventData.position); + + + // 드롭 시도 + if (treeListItem?.Data != null) + { + var result = dragDropManager.TryDrop(targetItem?.Data); + Debug.Log($"[OnPointerUp] 드롭 결과: {(result ? "성공" : "실패")}"); + + if (result) + { + // 드롭 성공 → 데이터 동기화 + HandleDropSuccess(treeListItem.Data, targetItem); + } + } + + dragDropManager.EndDrag(); + } + + /// + /// 드래그 중 아이템이 마우스를 따라다니도록 위치를 업데이트합니다. + /// Y축만 이동 (X축은 고정) + /// + private void UpdateDragPosition(PointerEventData eventData) + { + if (rectTransform == null || treeList == null) + { + return; + } + + var canvasRect = treeList.GetComponent(); + if (canvasRect == null) + { + return; + } + + // 스크린 좌표를 캔버스 로컬 좌표로 변환 + if (RectTransformUtility.ScreenPointToLocalPointInRectangle( + canvasRect, + eventData.position, + null, + out var canvasLocalPoint)) + { + // ✅ Y축만 업데이트 (X는 고정) + // 드래그 오프셋을 고려한 Y 위치만 계산 + var currentPosition = rectTransform.anchoredPosition; + rectTransform.anchoredPosition = new Vector2( + currentPosition.x, // ✅ X는 그대로 유지 + canvasLocalPoint.y - dragOffset.y // ✅ Y만 마우스를 따라 이동 + ); + } + } + + /// + /// 드롭 위치 표시 막대를 생성합니다. + /// + private void CreateDropIndicator() + { + if (dropIndicator != null) + { + return; + } + + // ✅ Root(root)를 부모로 설정 + // 계층 구조: TreeList > ScrollView > Viewport > Content > Root > TreeListItem + dropIndicatorParent = treeList!.Root.parent as RectTransform;// rectTransform?.parent as RectTransform; + + if (dropIndicatorParent == null) + { + Debug.LogError("[CreateDropIndicator] EntryRoot/Content를 찾을 수 없습니다"); + return; + } + + // 새로운 GameObject 생성 + var indicatorGo = new GameObject("DropIndicator"); + indicatorGo.transform.SetParent(dropIndicatorParent, false); + + // Image 컴포넌트 추가 + dropIndicator = indicatorGo.AddComponent(); + dropIndicator.color = new Color(0.2f, 0.8f, 1f, 0.5f); // 반투명 파란색 + + // RectTransform 설정 + dropIndicatorRect = indicatorGo.GetComponent(); + if (dropIndicatorRect != null) + { + dropIndicatorRect.anchorMin = new Vector2(0, 0.5f); + dropIndicatorRect.anchorMax = new Vector2(1, 0.5f); + dropIndicatorRect.pivot = new Vector2(0, 0.5f); + dropIndicatorRect.sizeDelta = new Vector2(0, 3f); // 높이 3 + } + + dropIndicator.raycastTarget = false; + indicatorGo.SetActive(false); + + Debug.Log("[CreateDropIndicator] 드롭 위치 표시 막대 생성됨"); + } + + /// + /// 드롭 위치 표시 막대를 업데이트합니다. + /// VerticalLayoutGroup 환경에서도 정확하게 위치를 계산합니다. + /// + private void UpdateDropIndicator(TreeListItem? targetItem) + { + if (dropIndicator == null || dropIndicatorRect == null || dropIndicatorParent == null) + { + return; + } + + if (targetItem == null) + { + dropIndicator.gameObject.SetActive(false); + return; + } + + // 드롭 위치 판단 + var targetRect = targetItem.GetComponent(); + if (targetRect == null) + { + dropIndicator.gameObject.SetActive(false); + return; + } + + var dropPosition = GetDropPosition(targetRect); + + dropIndicator.gameObject.SetActive(true); + + // ✅ 핵심: 월드 좌표로 변환해서 부모 기준 로컬 좌표 계산 + Vector3[] targetCorners = new Vector3[4]; + targetRect.GetWorldCorners(targetCorners); + + // 아이템의 월드 Y 좌표 + float targetWorldY = targetCorners[0].y; + + // 부모(EntryRoot)의 월드 좌표 + Vector3[] parentCorners = new Vector3[4]; + dropIndicatorParent.GetWorldCorners(parentCorners); + float parentWorldY = parentCorners[0].y; + float parentHeight = parentCorners[1].y - parentCorners[0].y; + + // 월드 Y를 부모 기준 로컬 Y로 변환 + float relativeY = targetWorldY - parentWorldY; + + // 부모의 pivot을 고려한 로컬 Y 계산 + float pivotAdjustedY = relativeY - (parentHeight * dropIndicatorParent.pivot.y); + + float indicatorY = 0; + + switch (dropPosition) + { + case DropPosition.Above: + // 대상 아이템 위 (아이템 높이의 절반) + indicatorY = pivotAdjustedY + (targetRect.rect.height / 2); + break; + + case DropPosition.Below: + // 대상 아이템 아래 + indicatorY = pivotAdjustedY - (targetRect.rect.height / 2); + break; + + case DropPosition.InsideAsChild: + // 대상 아이템 중앙 + indicatorY = pivotAdjustedY; + break; + } + + // ✅ DropIndicator 위치 설정 + dropIndicatorRect.anchoredPosition = new Vector2(0, indicatorY); + + Debug.Log($"[UpdateDropIndicator] 위치: {dropPosition}, targetY: {targetWorldY}, parentY: {parentWorldY}, indicatorY: {indicatorY}"); + } + + /// + /// 드롭 위치 표시 막대를 숨깁니다. + /// + private void HideDropIndicator() + { + if (dropIndicator != null) + { + dropIndicator.gameObject.SetActive(false); + } + } + + /// + /// 주어진 스크린 좌표에 있는 TreeListItem을 찾습니다. + /// + /// 스크린 좌표 + /// 찾은 TreeListItem (없으면 null) + private TreeListItem? GetItemAtMousePosition(Vector2 screenPosition) + { + if (treeList == null) + { + return null; + } + + // Raycast로 UI 요소 찾기 + var results = new System.Collections.Generic.List(); + var eventData = new PointerEventData(EventSystem.current) + { + position = screenPosition + }; + + EventSystem.current.RaycastAll(eventData, results); + + Debug.Log($"[GetItemAtMousePosition] Raycast 결과: {results.Count}개"); + + foreach (var result in results) + { + var item = result.gameObject.GetComponentInParent(); + if (item != null && item != treeListItem) + { + Debug.Log($"[GetItemAtMousePosition] 찾은 아이템: {item.Data?.Name ?? "Unknown"}"); + return item; + } + } + + return null; + } + + + /// + /// 드롭 성공 후 데이터를 동기화합니다. + /// + /// 드래그된 아이템 + /// 드롭 대상 UI 아이템 + private void HandleDropSuccess(TreeListItemData? draggedData, TreeListItem? targetItem) + { + if (draggedData == null || treeList == null) + { + return; + } + + // 드롭 대상이 없으면 (빈 공간에 드롭) 루트로 이동 + if (targetItem == null) + { + Debug.Log($"[HandleDropSuccess] 루트로 이동"); + MoveToRoot(draggedData); + treeList.UpdateFlattenedItemList(); + return; + } + + var targetData = targetItem.Data; + if (targetData == null) + { + return; + } + + // 드롭 위치 판단: 대상의 위/아래 또는 자식으로 + var dropPosition = GetDropPosition(targetItem.GetComponent()); + + Debug.Log($"[HandleDropSuccess] 드롭 위치: {dropPosition}"); + + switch (dropPosition) + { + case DropPosition.InsideAsChild: + // 대상의 자식으로 이동 + MoveAsChild(draggedData, targetData); + break; + + case DropPosition.Above: + // 대상 위에 위치 (같은 부모 내에서) + MoveBefore(draggedData, targetData); + break; + + case DropPosition.Below: + // 대상 아래에 위치 (같은 부모 내에서) + MoveAfter(draggedData, targetData); + break; + } + + treeList.UpdateFlattenedItemList(); + } + + /// + /// 드롭 위치를 판단합니다. + /// + /// 대상 아이템의 RectTransform + /// 드롭 위치 + private DropPosition GetDropPosition(RectTransform targetRect) + { + // 목표: 마우스 Y 위치를 기반으로 위/아래/안쪽 판단 + // 상위 1/3: 위 + // 중간 1/3: 자식 + // 하위 1/3: 아래 + + var height = targetRect.rect.height; + var thresholdUpper = height * 0.33f; + var thresholdLower = height * 0.67f; + + // 월드 좌표에서 로컬 좌표로 변환 + RectTransformUtility.ScreenPointToLocalPointInRectangle( + targetRect, + Input.mousePosition, + null, + out var localMousePos); + + // 로컬 Y 좌표로 판단 (RectTransform의 피벗이 중앙이면 -height/2 ~ height/2) + float relativeY = localMousePos.y; + + if (relativeY > thresholdUpper) + { + return DropPosition.Above; + } + else if (relativeY < -thresholdLower) + { + return DropPosition.Below; + } + else + { + return DropPosition.InsideAsChild; + } + } + + /// + /// 아이템을 루트 레벨로 이동합니다. + /// + private void MoveToRoot(TreeListItemData draggedData) + { + // 기존 부모에서 제거 + RemoveFromParent(draggedData); + Debug.Log($"[MoveToRoot] {draggedData.Name}을(를) 루트로 이동"); + // 루트 레벨에 추가 (트리 구조 데이터에만 적용) + // UI 업데이트는 이벤트 구독자가 처리 + } + + /// + /// 아이템을 다른 아이템의 자식으로 이동합니다. + /// + private void MoveAsChild(TreeListItemData draggedData, TreeListItemData targetData) + { + RemoveFromParent(draggedData); + targetData.AddChild(draggedData); + Debug.Log($"[MoveAsChild] {draggedData.Name}을(를) {targetData.Name}의 자식으로 이동"); + } + + /// + /// 아이템을 다른 아이템 앞으로 이동합니다. + /// + private void MoveBefore(TreeListItemData draggedData, TreeListItemData targetData) + { + var parentData = FindParentOfItem(draggedData); + RemoveFromParent(draggedData); + + if (parentData != null) + { + var targetIndex = parentData.Children.IndexOf(targetData); + if (targetIndex >= 0) + { + parentData.Children.Insert(targetIndex, draggedData); + parentData.NotifyDataChanged(); + Debug.Log($"[MoveBefore] {draggedData.Name}을(를) {targetData.Name} 앞으로 이동"); + } + } + } + + /// + /// 아이템을 다른 아이템 뒤로 이동합니다. + /// + private void MoveAfter(TreeListItemData draggedData, TreeListItemData targetData) + { + var parentData = FindParentOfItem(draggedData); + RemoveFromParent(draggedData); + + if (parentData != null) + { + var targetIndex = parentData.Children.IndexOf(targetData); + if (targetIndex >= 0) + { + parentData.Children.Insert(targetIndex + 1, draggedData); + parentData.NotifyDataChanged(); + Debug.Log($"[MoveAfter] {draggedData.Name}을(를) {targetData.Name} 뒤로 이동"); + } + } + } + + /// + /// 아이템을 현재 부모에서 제거합니다. + /// + private void RemoveFromParent(TreeListItemData item) + { + var parent = FindParentOfItem(item); + if (parent != null) + { + parent.RemoveChild(item); + } + } + + /// + /// 주어진 아이템의 부모를 찾습니다. + /// + private TreeListItemData? FindParentOfItem(TreeListItemData item) + { + if (treeList == null) + { + return null; + } + + // 루트 아이템들 확인 + foreach (Transform child in treeList.GetComponent()) + { + var childItem = child.GetComponent(); + if (childItem?.Data == item) + { + return null; // 루트 레벨 + } + } + + // 전체 트리 순회해서 부모 찾기 + return FindParentRecursive(item, null); + } + + private TreeListItemData? FindParentRecursive(TreeListItemData target, TreeListItemData? currentParent) + { + if (currentParent != null) + { + foreach (var child in currentParent.Children) + { + if (child == target) + { + return currentParent; + } + + var found = FindParentRecursive(target, child); + if (found != null) + { + return found; + } + } + } + else + { + // 루트 레벨의 모든 아이템 확인 + if (treeList != null) + { + var root = treeList.GetComponent(); + foreach (Transform child in root) + { + var childItem = child.GetComponent(); + if (childItem?.Data != null) + { + var found = FindParentRecursive(target, childItem.Data); + if (found != null) + { + return found; + } + } + } + } + } + + return null; + } + + /// + /// 드롭 위치를 나타내는 열거형입니다. + /// + private enum DropPosition + { + /// 위 (형제 아이템으로 앞쪽) + Above, + + /// 안쪽 (자식으로) + InsideAsChild, + + /// 아래 (형제 아이템으로 뒤쪽) + Below + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/List/Tree/TreeListItemDragHandler.cs.meta b/Assets/Scripts/UVC/UI/List/Tree/TreeListItemDragHandler.cs.meta new file mode 100644 index 00000000..493994f0 --- /dev/null +++ b/Assets/Scripts/UVC/UI/List/Tree/TreeListItemDragHandler.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 43beb810567ff7e489a0a9059cd3c4b0 \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/Modal/ColorPicker/ColorPicker.cs b/Assets/Scripts/UVC/UI/Modal/ColorPicker/ColorPicker.cs index bcbee629..351ee5c3 100644 --- a/Assets/Scripts/UVC/UI/Modal/ColorPicker/ColorPicker.cs +++ b/Assets/Scripts/UVC/UI/Modal/ColorPicker/ColorPicker.cs @@ -48,7 +48,7 @@ namespace UVC.UI.Modal.ColorPicker private bool interact; - // these can only work with the prefab and its children + // these can only work with the prefab and its Children public RectTransform positionIndicator; public Slider mainComponent; public Slider rComponent; diff --git a/Assets/Scripts/UVC/UI/ToolBar/Model/ToolbarModel.cs b/Assets/Scripts/UVC/UI/ToolBar/Model/ToolbarModel.cs index 90daf6e4..3fa9e544 100644 --- a/Assets/Scripts/UVC/UI/ToolBar/Model/ToolbarModel.cs +++ b/Assets/Scripts/UVC/UI/ToolBar/Model/ToolbarModel.cs @@ -1,4 +1,4 @@ -using UVC.UI.Commands; +using UVC.UI.Commands; namespace UVC.UI.Toolbar.Model { @@ -39,7 +39,7 @@ namespace UVC.UI.Toolbar.Model /// false, // 초기 상태 (꺼짐) /// "icons/grid_on", // 켜짐 상태 아이콘 /// "icons/grid_off", // 꺼짐 상태 아이콘 - /// (isSelected) => Debug.Log($"그리드 표시: {isSelected}"), // OnToggle 콜백 + /// (IsSelected) => Debug.Log($"그리드 표시: {IsSelected}"), // OnToggle 콜백 /// null, // ClickCommand (필요시 설정) /// "tooltip_toggle_grid_visibility" /// ); @@ -47,10 +47,10 @@ namespace UVC.UI.Toolbar.Model /// // 5. 라디오 버튼 그룹 추가 /// string viewModeGroup = "ViewModeSelection"; /// myToolbar.AddRadioButton(viewModeGroup, "button_2d_view", true, "icons/2d_view", null, - /// (isSelected) => { if(isSelected) Debug.Log("2D 뷰 선택됨"); }, + /// (IsSelected) => { if(IsSelected) Debug.Log("2D 뷰 선택됨"); }, /// null, "tooltip_2d_view"); /// myToolbar.AddRadioButton(viewModeGroup, "button_3d_view", false, "icons/3d_view", null, - /// (isSelected) => { if(isSelected) Debug.Log("3D 뷰 선택됨"); }, + /// (IsSelected) => { if(IsSelected) Debug.Log("3D 뷰 선택됨"); }, /// null, "tooltip_3d_view"); /// /// // 6. 설정된 모델을 ToolbarView에 전달 diff --git a/Assets/Scripts/UVC/UI/ToolBar/Model/ToolbarRadioButton.cs b/Assets/Scripts/UVC/UI/ToolBar/Model/ToolbarRadioButton.cs index 61823f5e..dcd7c127 100644 --- a/Assets/Scripts/UVC/UI/ToolBar/Model/ToolbarRadioButton.cs +++ b/Assets/Scripts/UVC/UI/ToolBar/Model/ToolbarRadioButton.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace UVC.UI.Toolbar.Model { @@ -27,9 +27,9 @@ namespace UVC.UI.Toolbar.Model /// OffIconSpritePath = "icons/toolbar/view_top_off", /// Tooltip = "tooltip_top_view", /// IsSelected = true, // 초기 선택 상태 - /// OnToggle = (isSelected) => + /// OnToggle = (IsSelected) => /// { - /// if (isSelected) UnityEngine.Debug.Log("탑뷰 선택됨 (OnToggle)"); + /// if (IsSelected) UnityEngine.Debug.Log("탑뷰 선택됨 (OnToggle)"); /// }, /// ClickCommand = new ActionCommand(() => /// { @@ -46,9 +46,9 @@ namespace UVC.UI.Toolbar.Model /// OffIconSpritePath = "icons/toolbar/view_front_off", /// Tooltip = "tooltip_front_view", /// IsSelected = false, // 초기 선택 안됨 - /// OnToggle = (isSelected) => + /// OnToggle = (IsSelected) => /// { - /// if (isSelected) UnityEngine.Debug.Log("프론트뷰 선택됨 (OnToggle)"); + /// if (IsSelected) UnityEngine.Debug.Log("프론트뷰 선택됨 (OnToggle)"); /// }, /// ClickCommand = new ActionCommand(() => /// { diff --git a/Assets/Scripts/UVC/UI/ToolBar/Model/ToolbarToggleButton.cs b/Assets/Scripts/UVC/UI/ToolBar/Model/ToolbarToggleButton.cs index bf9c2622..ad8aae0b 100644 --- a/Assets/Scripts/UVC/UI/ToolBar/Model/ToolbarToggleButton.cs +++ b/Assets/Scripts/UVC/UI/ToolBar/Model/ToolbarToggleButton.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace UVC.UI.Toolbar.Model { @@ -27,9 +27,9 @@ namespace UVC.UI.Toolbar.Model /// // 2. 토글 버튼 클릭 시 실행될 커맨드 정의 (선택 사항, 주로 상태 변경 후 추가 작업) /// // 이 커맨드는 IsSelected 상태가 변경된 *후에* ExecuteClick 내부에서 호출됩니다. /// // 커맨드 파라미터로 현재 IsSelected 상태를 받고 싶다면 ActionCommand 사용 가능. - /// ICommand muteCommand = new ActionCommand((isSelected) => + /// ICommand muteCommand = new ActionCommand((IsSelected) => /// { - /// UnityEngine.Debug.Log($"음소거 버튼 커맨드 실행됨. 현재 선택 상태: {isSelected}"); + /// UnityEngine.Debug.Log($"음소거 버튼 커맨드 실행됨. 현재 선택 상태: {IsSelected}"); /// }); /// /// // 3. ToolbarToggleButton 인스턴스 생성 및 속성 설정 diff --git a/Assets/Scripts/UVC/UI/ToolBar/View/ToolbarToggleButtonViewProcessor.cs b/Assets/Scripts/UVC/UI/ToolBar/View/ToolbarToggleButtonViewProcessor.cs index c1d0c871..c65a6ff9 100644 --- a/Assets/Scripts/UVC/UI/ToolBar/View/ToolbarToggleButtonViewProcessor.cs +++ b/Assets/Scripts/UVC/UI/ToolBar/View/ToolbarToggleButtonViewProcessor.cs @@ -1,4 +1,4 @@ -using UnityEngine; +using UnityEngine; using UnityEngine.UI; using UVC.UI.Toolbar.Model; @@ -89,9 +89,9 @@ namespace UVC.UI.Toolbar.View // ExecuteClick 내부에서 IsSelected 상태가 변경되고, 관련 커맨드가 실행되며, OnToggleStateChanged 이벤트가 발생합니다. toggleComponent.onValueChanged.AddListener((isSelected) => { - // isSelected 파라미터는 UI에서 변경된 새 상태이지만, + // IsSelected 파라미터는 UI에서 변경된 새 상태이지만, // ToolbarToggleButton의 ExecuteClick() 내부에서 IsSelected가 토글되므로 - // isSelected 파라미터를 직접 사용하지 않고 ExecuteClick()만 호출합니다. + // IsSelected 파라미터를 직접 사용하지 않고 ExecuteClick()만 호출합니다. // ExecuteClick()이 모델의 상태를 올바르게 변경하고 이벤트를 발생시킬 책임이 있습니다. toggleModel.ExecuteClick(); }); diff --git a/Assets/Scripts/UVC/UI/ToolBar/View/ToolbarView.cs b/Assets/Scripts/UVC/UI/ToolBar/View/ToolbarView.cs index eb52c0f0..844d5b0a 100644 --- a/Assets/Scripts/UVC/UI/ToolBar/View/ToolbarView.cs +++ b/Assets/Scripts/UVC/UI/ToolBar/View/ToolbarView.cs @@ -60,7 +60,7 @@ namespace UVC.UI.Toolbar.View /// /// // 토글 버튼 추가 /// myModel.AddToggleButton("그리드 보기", false, "icons/grid_on", "icons/grid_off", - /// (isSelected) => Debug.Log($"그리드 표시: {isSelected}")); + /// (IsSelected) => Debug.Log($"그리드 표시: {IsSelected}")); /// /// // 라디오 버튼 그룹 추가 /// string viewModeGroup = "ViewMode"; diff --git a/Assets/Scripts/UVC/UI/Window/ComponentListWindow.cs b/Assets/Scripts/UVC/UI/Window/ComponentListWindow.cs index 47570c39..9f856896 100644 --- a/Assets/Scripts/UVC/UI/Window/ComponentListWindow.cs +++ b/Assets/Scripts/UVC/UI/Window/ComponentListWindow.cs @@ -43,7 +43,7 @@ namespace UVC.UI.Window if (componentList == null) { - Debug.LogError("InfiniteScroll component is not assigned or found in children."); + Debug.LogError("InfiniteScroll component is not assigned or found in Children."); return; } diff --git a/Assets/Shapes2D/Editor/ShapeEditor.cs b/Assets/Shapes2D/Editor/ShapeEditor.cs index a270ccc1..4026b89c 100644 --- a/Assets/Shapes2D/Editor/ShapeEditor.cs +++ b/Assets/Shapes2D/Editor/ShapeEditor.cs @@ -1,4 +1,4 @@ -namespace Shapes2D { +namespace Shapes2D { using UnityEngine; using UnityEditor; @@ -146,7 +146,7 @@ Quaternion oldRotation = shape.transform.rotation; shape.transform.rotation = Quaternion.identity; - // get the desired pixel size of our shape and all its children, which will be the size of our texture + // get the desired pixel size of our shape and all its Children, which will be the size of our texture Vector2 size = shape.GetShapePixelSize(pixelsPerUnit: pixelsPerUnit); int w = (int) size.x; int h = (int) size.y; @@ -255,7 +255,7 @@ RenderTexture.active = oldRT; // grab the sprite's pivot point based on the top object's location - // relative to its children + // relative to its Children Vector2 pivot = shape.GetPivot(); // restore the shape's rotation diff --git a/Assets/Shapes2D/Scripts/Shape.cs b/Assets/Shapes2D/Scripts/Shape.cs index 7cf15dea..5fc158c1 100644 --- a/Assets/Shapes2D/Scripts/Shape.cs +++ b/Assets/Shapes2D/Scripts/Shape.cs @@ -1,4 +1,4 @@ -namespace Shapes2D { +namespace Shapes2D { using UnityEngine; using UnityEngine.UI; @@ -1647,7 +1647,7 @@ /// /// Used internally. - /// Returns the shape's world corners, not including any children, as 5 Vector3s (bottom left, + /// Returns the shape's world corners, not including any Children, as 5 Vector3s (bottom left, /// top left, top right, bottom right, and bottom left again). /// public Vector3[] GetWorldCorners(Vector3[] corners) {