1306 lines
44 KiB
C#
1306 lines
44 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using AlicizaX.UXTool;
|
||
using AlicizaX.UI;
|
||
using UnityEditor;
|
||
using UnityEditor.SceneManagement;
|
||
using UnityEditor.Experimental.SceneManagement;
|
||
using UnityEngine;
|
||
using UnityEngine.UIElements;
|
||
using Object = UnityEngine.Object;
|
||
|
||
public class UXHierarchyWindow : EditorWindow
|
||
{
|
||
const string FoldoutPrefsKeyBase = "UXHierarchy_Foldouts_v1"; // base key
|
||
const string WindowTitle = "UXHierarchy";
|
||
|
||
VisualElement root;
|
||
ScrollView scroll;
|
||
VisualElement treeContainer;
|
||
TextField searchField;
|
||
VisualElement rootDropZone; // accepts drops to root
|
||
|
||
// foldout state: instanceID -> bool (true = expanded)
|
||
Dictionary<int, bool> foldStates = new Dictionary<int, bool>();
|
||
|
||
// flat ordered list of GameObjects displayed (for shift-range selection)
|
||
List<GameObject> flatList = new List<GameObject>();
|
||
|
||
// selection state in the window (keeps synced with UnityEditor.Selection)
|
||
List<GameObject> selectedObjects = new List<GameObject>();
|
||
int lastClickedIndex = -1;
|
||
|
||
// delete-key auto-repeat
|
||
bool deleteKeyHeld = false;
|
||
double nextAutoDeleteTime = 0;
|
||
const double DeleteInitialDelay = 0.35;
|
||
const double DeleteRepeatInterval = 0.08;
|
||
|
||
// rename state
|
||
TextField activeRenameField = null;
|
||
|
||
// drag feedback tracking
|
||
VisualElement currentDragTargetRow = null;
|
||
|
||
enum DragDropMode
|
||
{
|
||
None,
|
||
InsertBefore,
|
||
InsertAfter,
|
||
MakeChild
|
||
}
|
||
|
||
DragDropMode currentDragMode = DragDropMode.None;
|
||
|
||
const float DragBorderWidth = 3f; // thickness of the insert line
|
||
|
||
public static void ShowWindow()
|
||
{
|
||
var w = GetWindow<UXHierarchyWindow>(false, WindowTitle);
|
||
w.minSize = new Vector2(240, 200);
|
||
w.Show();
|
||
}
|
||
|
||
void OnEnable()
|
||
{
|
||
LoadFoldStates();
|
||
|
||
root = rootVisualElement;
|
||
root.styleSheets.Clear();
|
||
root.style.paddingTop = 4;
|
||
root.style.paddingLeft = 4;
|
||
root.style.paddingRight = 4;
|
||
|
||
// header row: search + refresh icon (removed title text)
|
||
var headerRow = new VisualElement();
|
||
headerRow.style.flexDirection = FlexDirection.Row;
|
||
headerRow.style.alignItems = Align.Center;
|
||
headerRow.style.marginBottom = 6;
|
||
|
||
// search icon
|
||
var searchIcon = new Image();
|
||
Texture si = EditorGUIUtility.IconContent("Search Icon").image ?? EditorGUIUtility.IconContent("d_Search Icon").image ?? EditorGUIUtility.IconContent("SearchField").image;
|
||
if (si != null)
|
||
{
|
||
searchIcon.image = si;
|
||
searchIcon.style.width = 16;
|
||
searchIcon.style.height = 16;
|
||
searchIcon.style.marginRight = 4;
|
||
headerRow.Add(searchIcon);
|
||
}
|
||
|
||
searchField = new TextField();
|
||
searchField.name = "uxh-search";
|
||
searchField.style.flexGrow = 1;
|
||
searchField.style.minWidth = 80;
|
||
searchField.tooltip = "Search by name (case-insensitive)";
|
||
searchField.RegisterValueChangedCallback(evt => RebuildTree());
|
||
headerRow.Add(searchField);
|
||
|
||
// refresh icon-only button
|
||
var refreshBtn = new Button(() => RebuildTree());
|
||
Texture rtex = EditorGUIUtility.IconContent("Refresh").image ?? EditorGUIUtility.IconContent("d_Refresh").image;
|
||
if (rtex != null)
|
||
{
|
||
var img = new Image() { image = rtex };
|
||
img.style.width = 16;
|
||
img.style.height = 16;
|
||
refreshBtn.Add(img);
|
||
refreshBtn.tooltip = "Refresh";
|
||
}
|
||
else
|
||
{
|
||
refreshBtn.text = "Refresh";
|
||
}
|
||
|
||
refreshBtn.style.marginLeft = 6;
|
||
headerRow.Add(refreshBtn);
|
||
|
||
root.Add(headerRow);
|
||
|
||
// scroll area
|
||
scroll = new ScrollView(ScrollViewMode.Vertical);
|
||
scroll.style.flexGrow = 1;
|
||
root.Add(scroll);
|
||
|
||
// root drop zone: small area at top to drop to root
|
||
rootDropZone = new VisualElement();
|
||
rootDropZone.style.height = 18;
|
||
rootDropZone.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||
rootDropZone.style.marginBottom = 4;
|
||
rootDropZone.style.alignItems = Align.Center;
|
||
rootDropZone.style.justifyContent = Justify.Center;
|
||
rootDropZone.style.backgroundColor = new StyleColor(new Color(0, 0, 0, 0));
|
||
rootDropZone.Add(new Label("Drop here to make root"));
|
||
scroll.contentContainer.Add(rootDropZone);
|
||
|
||
treeContainer = new VisualElement();
|
||
treeContainer.name = "treeContainer";
|
||
treeContainer.style.flexDirection = FlexDirection.Column;
|
||
scroll.contentContainer.Add(treeContainer);
|
||
|
||
// event handlers
|
||
root.RegisterCallback<KeyDownEvent>(OnKeyDown);
|
||
root.RegisterCallback<KeyUpEvent>(OnKeyUp);
|
||
root.RegisterCallback<FocusOutEvent>(e => StopRename());
|
||
|
||
scroll.AddManipulator(new ContextualMenuManipulator(evt =>
|
||
{
|
||
if (PrefabStageUtils.InEmptyStage) return;
|
||
|
||
var t = evt.target as VisualElement;
|
||
bool hasPathAncestor = false;
|
||
while (t != null)
|
||
{
|
||
if (t.userData is string)
|
||
{
|
||
hasPathAncestor = true;
|
||
break;
|
||
}
|
||
|
||
t = t.parent as VisualElement;
|
||
}
|
||
|
||
if (!hasPathAncestor)
|
||
{
|
||
evt.menu.AppendAction("创建空物体", a => CreateEmptyUI(null), a => DropdownMenuAction.Status.Normal);
|
||
CreateCommonUIMenu(evt, null);
|
||
}
|
||
}));
|
||
|
||
scroll.RegisterCallback<ClickEvent>(evt =>
|
||
{
|
||
if (evt.button != 0) return;
|
||
|
||
VisualElement t = evt.target as VisualElement;
|
||
bool clickedRow = false;
|
||
|
||
while (t != null)
|
||
{
|
||
if (t.ClassListContains("uxh-row") || t.userData is GameObject)
|
||
{
|
||
clickedRow = true;
|
||
break;
|
||
}
|
||
|
||
t = t.parent as VisualElement;
|
||
}
|
||
|
||
if (!clickedRow)
|
||
{
|
||
selectedObjects.Clear();
|
||
Selection.objects = new Object[0];
|
||
UpdateSelectionHighlighting();
|
||
|
||
evt.StopImmediatePropagation();
|
||
}
|
||
});
|
||
|
||
|
||
rootDropZone.RegisterCallback<DragEnterEvent>(evt =>
|
||
{
|
||
if (PrefabStageUtils.InEmptyStage) return;
|
||
rootDropZone.style.backgroundColor = new StyleColor(new Color(0.2f, 0.2f, 0.2f, 0.06f));
|
||
evt.StopImmediatePropagation();
|
||
});
|
||
rootDropZone.RegisterCallback<DragLeaveEvent>(evt =>
|
||
{
|
||
if (PrefabStageUtils.InEmptyStage) return;
|
||
rootDropZone.style.backgroundColor = new StyleColor(Color.clear);
|
||
if (currentDragTargetRow != null)
|
||
{
|
||
ClearDragHighlight(currentDragTargetRow);
|
||
}
|
||
|
||
evt.StopImmediatePropagation();
|
||
});
|
||
rootDropZone.RegisterCallback<DragUpdatedEvent>(evt =>
|
||
{
|
||
if (PrefabStageUtils.InEmptyStage) return;
|
||
if (DragAndDrop.objectReferences != null && DragAndDrop.objectReferences.Length > 0) DragAndDrop.visualMode = DragAndDropVisualMode.Move;
|
||
evt.StopImmediatePropagation();
|
||
});
|
||
rootDropZone.RegisterCallback<DragPerformEvent>(evt =>
|
||
{
|
||
if (DragAndDrop.objectReferences != null && DragAndDrop.objectReferences.Length > 0 && !PrefabStageUtils.InEmptyStage)
|
||
{
|
||
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
||
Transform rootParent = null;
|
||
if (prefabStage != null && prefabStage.prefabContentsRoot != null)
|
||
rootParent = prefabStage.prefabContentsRoot.transform;
|
||
else
|
||
rootParent = null; // null means scene root
|
||
|
||
// perform reparent to root (or to prefabContentsRoot)
|
||
foreach (var o in DragAndDrop.objectReferences)
|
||
{
|
||
var draggedGO = o as GameObject;
|
||
if (draggedGO == null) continue;
|
||
// if drag source is same prefab, allow
|
||
if (rootParent != null)
|
||
{
|
||
Undo.SetTransformParent(draggedGO.transform, rootParent, "Reparent to root (UXHierarchy)");
|
||
draggedGO.transform.SetSiblingIndex(rootParent.childCount - 1);
|
||
}
|
||
else
|
||
{
|
||
// move to scene root
|
||
Undo.SetTransformParent(draggedGO.transform, null, "Reparent to root (UXHierarchy)");
|
||
}
|
||
}
|
||
|
||
if (prefabStage != null)
|
||
{
|
||
try
|
||
{
|
||
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
|
||
}
|
||
catch
|
||
{
|
||
}
|
||
}
|
||
else
|
||
{
|
||
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
|
||
}
|
||
|
||
RebuildTree();
|
||
DragAndDrop.AcceptDrag();
|
||
}
|
||
|
||
rootDropZone.style.backgroundColor = new StyleColor(Color.clear);
|
||
evt.StopImmediatePropagation();
|
||
});
|
||
|
||
// clear drag highlight when leaving the whole tree or root
|
||
treeContainer.RegisterCallback<DragLeaveEvent>(evt =>
|
||
{
|
||
if (currentDragTargetRow != null)
|
||
{
|
||
ClearDragHighlight(currentDragTargetRow);
|
||
}
|
||
|
||
evt.StopImmediatePropagation();
|
||
});
|
||
root.RegisterCallback<DragLeaveEvent>(evt =>
|
||
{
|
||
if (currentDragTargetRow != null)
|
||
{
|
||
ClearDragHighlight(currentDragTargetRow);
|
||
}
|
||
|
||
rootDropZone.style.backgroundColor = new StyleColor(Color.clear);
|
||
evt.StopImmediatePropagation();
|
||
});
|
||
|
||
// keep selection in sync if external selection changes
|
||
Selection.selectionChanged += OnUnitySelectionChanged;
|
||
|
||
// hierarchy changes (scene/prefab content changes)
|
||
EditorApplication.hierarchyChanged += OnHierarchyChanged;
|
||
EditorApplication.update += EditorUpdate;
|
||
|
||
RebuildTree();
|
||
}
|
||
|
||
void OnDisable()
|
||
{
|
||
Selection.selectionChanged -= OnUnitySelectionChanged;
|
||
EditorApplication.hierarchyChanged -= OnHierarchyChanged;
|
||
EditorApplication.update -= EditorUpdate;
|
||
|
||
SaveFoldStates();
|
||
}
|
||
|
||
void OnHierarchyChanged()
|
||
{
|
||
RebuildTree();
|
||
}
|
||
|
||
void EditorUpdate()
|
||
{
|
||
// handle delete auto-repeat while held
|
||
if (deleteKeyHeld && EditorApplication.timeSinceStartup >= nextAutoDeleteTime)
|
||
{
|
||
DoDeleteSelected();
|
||
nextAutoDeleteTime = EditorApplication.timeSinceStartup + DeleteRepeatInterval;
|
||
}
|
||
}
|
||
|
||
void OnUnitySelectionChanged()
|
||
{
|
||
var sel = Selection.gameObjects.ToList();
|
||
selectedObjects = sel;
|
||
UpdateSelectionHighlighting();
|
||
Repaint();
|
||
}
|
||
|
||
void RebuildTree()
|
||
{
|
||
// clear any drag highlight to avoid residual visuals
|
||
if (currentDragTargetRow != null)
|
||
{
|
||
ClearDragHighlight(currentDragTargetRow);
|
||
currentDragTargetRow = null;
|
||
currentDragMode = DragDropMode.None;
|
||
}
|
||
|
||
treeContainer.Clear();
|
||
flatList.Clear();
|
||
|
||
string search = (searchField != null) ? searchField.value?.Trim() ?? "" : "";
|
||
|
||
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
||
if (prefabStage != null && prefabStage.prefabContentsRoot != null)
|
||
{
|
||
var root = prefabStage.prefabContentsRoot;
|
||
var prefabRoot = prefabStage.prefabContentsRoot.GetComponent<Canvas>() != null ? root.transform : root.transform.parent;
|
||
for (int i = 0; i < prefabRoot.transform.childCount; ++i)
|
||
{
|
||
var child = prefabRoot.transform.GetChild(i).gameObject;
|
||
var node = CreateNodeRecursive(child, 0, search);
|
||
if (node != null) treeContainer.Add(node);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
var roots = EditorSceneManager.GetActiveScene().GetRootGameObjects();
|
||
foreach (var go in roots)
|
||
{
|
||
var node = CreateNodeRecursive(go, 0, search);
|
||
if (node != null) treeContainer.Add(node);
|
||
}
|
||
}
|
||
|
||
UpdateSelectionHighlighting();
|
||
}
|
||
|
||
VisualElement CreateNodeRecursive(GameObject go, int indent, string search)
|
||
{
|
||
// decide whether this node or any descendant matches the search
|
||
bool matches = string.IsNullOrEmpty(search) || go.name.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0;
|
||
bool isPrefab = PrefabUtility.IsPartOfAnyPrefab(go);
|
||
// process children
|
||
List<VisualElement> childNodes = new List<VisualElement>();
|
||
if (!isPrefab)
|
||
{
|
||
for (int i = 0; i < go.transform.childCount; ++i)
|
||
{
|
||
var child = go.transform.GetChild(i).gameObject;
|
||
var childNode = CreateNodeRecursive(child, indent + 1, search);
|
||
if (childNode != null) childNodes.Add(childNode);
|
||
}
|
||
}
|
||
|
||
// if nothing matches and no children match, skip
|
||
if (!matches && childNodes.Count == 0) return null;
|
||
|
||
// otherwise build node
|
||
flatList.Add(go);
|
||
int indexInFlat = flatList.Count - 1;
|
||
|
||
var node = new VisualElement();
|
||
node.style.flexDirection = FlexDirection.Column;
|
||
node.userData = go;
|
||
|
||
// row
|
||
var row = new VisualElement();
|
||
row.style.flexDirection = FlexDirection.Row;
|
||
row.style.alignItems = Align.Center;
|
||
row.style.paddingLeft = indent * 12;
|
||
row.style.paddingTop = 2;
|
||
row.style.paddingBottom = 2;
|
||
row.style.flexGrow = 0;
|
||
row.AddToClassList("uxh-row");
|
||
row.style.paddingRight = 6;
|
||
|
||
// only show fold icon if we have visible children
|
||
bool hasVisibleChildren = childNodes.Count > 0;
|
||
Label foldLabel = null;
|
||
|
||
if (hasVisibleChildren && !isPrefab)
|
||
{
|
||
foldLabel = new Label();
|
||
foldLabel.style.width = 16;
|
||
foldLabel.style.height = 16;
|
||
foldLabel.style.alignSelf = Align.Center;
|
||
foldLabel.style.marginRight = 4;
|
||
foldLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||
foldLabel.tooltip = "Expand / Collapse";
|
||
|
||
int id = go.GetInstanceID();
|
||
bool expanded = foldStates.ContainsKey(id) ? foldStates[id] : true;
|
||
foldLabel.text = expanded ? "▼" : "▶";
|
||
foldLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||
foldLabel.style.fontSize = 10;
|
||
foldLabel.style.color = new StyleColor(new Color(0.55f, 0.55f, 0.55f));
|
||
|
||
// toggle on click
|
||
foldLabel.RegisterCallback<MouseDownEvent>((evt) =>
|
||
{
|
||
if (evt.button == 0)
|
||
{
|
||
expanded = !expanded;
|
||
foldStates[id] = expanded;
|
||
foldLabel.text = expanded ? "▼" : "▶";
|
||
SaveFoldStates();
|
||
var childrenContainer = node.Q<VisualElement>("children");
|
||
if (childrenContainer != null)
|
||
childrenContainer.style.display = expanded ? DisplayStyle.Flex : DisplayStyle.None;
|
||
evt.StopImmediatePropagation();
|
||
}
|
||
});
|
||
|
||
// hover color
|
||
foldLabel.RegisterCallback<MouseEnterEvent>((evt) => foldLabel.style.color = new StyleColor(Color.white));
|
||
foldLabel.RegisterCallback<MouseLeaveEvent>((evt) => foldLabel.style.color = new StyleColor(new Color(0.55f, 0.55f, 0.55f)));
|
||
|
||
row.Add(foldLabel);
|
||
}
|
||
else
|
||
{
|
||
// add spacer so icons align
|
||
var spacer = new VisualElement();
|
||
spacer.style.width = 16;
|
||
spacer.style.height = 16;
|
||
spacer.style.marginRight = 4;
|
||
row.Add(spacer);
|
||
}
|
||
|
||
// icon
|
||
var icon = new Image();
|
||
icon.image = GetObjectIcon(go);
|
||
icon.style.width = 16;
|
||
icon.style.height = 16;
|
||
icon.style.alignSelf = Align.Center;
|
||
icon.style.marginRight = 4;
|
||
row.Add(icon);
|
||
|
||
// label
|
||
var label = new Label(go.name);
|
||
label.style.flexGrow = 1;
|
||
label.style.unityTextAlign = TextAnchor.MiddleLeft;
|
||
label.userData = indexInFlat;
|
||
row.Add(label);
|
||
|
||
row.AddManipulator(new ContextualMenuManipulator(evt =>
|
||
{
|
||
// right click: node menu
|
||
if (evt.button == 1 && !PrefabStageUtils.InEmptyStage)
|
||
{
|
||
SelectSingle(go);
|
||
evt.menu.AppendAction("创建空物体", a => CreateEmptyUI(go), a => DropdownMenuAction.Status.Normal);
|
||
evt.menu.AppendAction("删除", a =>
|
||
{
|
||
SelectSingle(go);
|
||
DoDeleteSelected();
|
||
}, a => DropdownMenuAction.Status.Normal);
|
||
CreateCommonUIMenu(evt, go);
|
||
CreateControlerUIMenu(evt, go);
|
||
evt.StopImmediatePropagation();
|
||
}
|
||
}));
|
||
// mouse handling
|
||
row.RegisterCallback<MouseDownEvent>(evt =>
|
||
{
|
||
// left click selection
|
||
if (evt.button == 0)
|
||
{
|
||
bool ctrl = evt.ctrlKey || evt.commandKey;
|
||
bool shift = evt.shiftKey;
|
||
|
||
int clickedIndex = (int)label.userData;
|
||
|
||
if (shift && lastClickedIndex >= 0)
|
||
{
|
||
int a = Math.Min(lastClickedIndex, clickedIndex);
|
||
int b = Math.Max(lastClickedIndex, clickedIndex);
|
||
var range = flatList.GetRange(a, b - a + 1);
|
||
selectedObjects = range.Where(x => x != null).ToList();
|
||
Selection.objects = selectedObjects.ToArray();
|
||
}
|
||
else if (ctrl)
|
||
{
|
||
if (selectedObjects.Contains(go)) selectedObjects.Remove(go);
|
||
else selectedObjects.Add(go);
|
||
Selection.objects = selectedObjects.ToArray();
|
||
lastClickedIndex = clickedIndex;
|
||
}
|
||
else
|
||
{
|
||
SelectSingle(go);
|
||
lastClickedIndex = clickedIndex;
|
||
}
|
||
|
||
evt.StopImmediatePropagation();
|
||
}
|
||
|
||
// double-click print name
|
||
if (evt.clickCount == 2 && evt.button == 0 && isPrefab)
|
||
{
|
||
var prefabAsset = PrefabUtility.GetCorrespondingObjectFromSource(go);
|
||
var prefabPath = AssetDatabase.GetAssetPath(prefabAsset);
|
||
if (prefabPath.StartsWith(Def_UXGUIPath.UIResRootPath))
|
||
{
|
||
PrefabStageUtils.SwitchStage(prefabPath);
|
||
}
|
||
|
||
evt.StopImmediatePropagation();
|
||
}
|
||
});
|
||
|
||
// drag start: when moving mouse while left pressed
|
||
row.RegisterCallback<MouseMoveEvent>(evt =>
|
||
{
|
||
if (evt.pressedButtons == 1)
|
||
{
|
||
if (!selectedObjects.Contains(go)) SelectSingle(go);
|
||
DragAndDrop.PrepareStartDrag();
|
||
DragAndDrop.objectReferences = selectedObjects.ToArray();
|
||
DragAndDrop.StartDrag("UXHierarchyDrag");
|
||
evt.StopImmediatePropagation();
|
||
}
|
||
});
|
||
|
||
// drag feedback & drop handling on row
|
||
row.RegisterCallback<DragEnterEvent>(evt =>
|
||
{
|
||
/* highlight handled in DragUpdated */
|
||
evt.StopImmediatePropagation();
|
||
});
|
||
row.RegisterCallback<DragLeaveEvent>(evt =>
|
||
{
|
||
ClearDragHighlight(row);
|
||
evt.StopImmediatePropagation();
|
||
});
|
||
row.RegisterCallback<DragUpdatedEvent>(evt =>
|
||
{
|
||
if (DragAndDrop.objectReferences != null && DragAndDrop.objectReferences.Length > 0)
|
||
{
|
||
// compute position ratio
|
||
float h = Math.Max(1.0f, row.layout.height);
|
||
float y = evt.localMousePosition.y;
|
||
float ratio = y / h;
|
||
|
||
DragAndDrop.visualMode = DragAndDropVisualMode.Move;
|
||
|
||
DragDropMode mode;
|
||
if (ratio < 0.25f) mode = DragDropMode.InsertBefore;
|
||
else if (ratio > 0.75f) mode = DragDropMode.InsertAfter;
|
||
else mode = DragDropMode.MakeChild;
|
||
|
||
// only update highlight if changed to reduce style churn
|
||
if (currentDragTargetRow != row || currentDragMode != mode)
|
||
ApplyDragHighlight(row, mode);
|
||
}
|
||
|
||
evt.StopImmediatePropagation();
|
||
});
|
||
|
||
row.RegisterCallback<DragPerformEvent>(evt =>
|
||
{
|
||
if (DragAndDrop.objectReferences != null && DragAndDrop.objectReferences.Length > 0)
|
||
{
|
||
float h = Math.Max(1.0f, row.layout.height);
|
||
float y = evt.localMousePosition.y;
|
||
float ratio = y / h;
|
||
|
||
DragDropMode mode;
|
||
if (ratio < 0.25f) mode = DragDropMode.InsertBefore;
|
||
else if (ratio > 0.75f) mode = DragDropMode.InsertAfter;
|
||
else mode = DragDropMode.MakeChild;
|
||
|
||
PerformDropOnRow(go, mode);
|
||
DragAndDrop.AcceptDrag();
|
||
}
|
||
|
||
ClearDragHighlight(row);
|
||
evt.StopImmediatePropagation();
|
||
});
|
||
|
||
// add row to node
|
||
node.Add(row);
|
||
|
||
// children container
|
||
var childrenContainer = new VisualElement();
|
||
childrenContainer.name = "children";
|
||
childrenContainer.style.flexDirection = FlexDirection.Column;
|
||
childrenContainer.style.marginLeft = 0;
|
||
childrenContainer.style.display = hasVisibleChildren ? DisplayStyle.Flex : DisplayStyle.None;
|
||
node.Add(childrenContainer);
|
||
|
||
// populate visible children
|
||
foreach (var cn in childNodes) childrenContainer.Add(cn);
|
||
|
||
return node;
|
||
}
|
||
|
||
void ApplyDragHighlight(VisualElement row, DragDropMode mode)
|
||
{
|
||
// clear previous if different
|
||
if (currentDragTargetRow != null && currentDragTargetRow != row)
|
||
{
|
||
ClearDragHighlight(currentDragTargetRow);
|
||
currentDragTargetRow = null;
|
||
currentDragMode = DragDropMode.None;
|
||
}
|
||
|
||
// store
|
||
currentDragTargetRow = row;
|
||
currentDragMode = mode;
|
||
|
||
// reset style first
|
||
row.style.borderTopWidth = new StyleFloat(0f);
|
||
row.style.borderBottomWidth = new StyleFloat(0f);
|
||
row.style.borderLeftWidth = new StyleFloat(0f);
|
||
row.style.borderRightWidth = new StyleFloat(0f);
|
||
row.style.borderTopColor = new StyleColor(Color.clear);
|
||
row.style.borderBottomColor = new StyleColor(Color.clear);
|
||
row.style.backgroundColor = new StyleColor(Color.clear);
|
||
|
||
if (mode == DragDropMode.InsertBefore)
|
||
{
|
||
row.style.borderTopWidth = new StyleFloat(DragBorderWidth);
|
||
row.style.borderTopColor = new StyleColor(Color.cyan);
|
||
}
|
||
else if (mode == DragDropMode.InsertAfter)
|
||
{
|
||
row.style.borderBottomWidth = new StyleFloat(DragBorderWidth);
|
||
row.style.borderBottomColor = new StyleColor(Color.cyan);
|
||
}
|
||
else if (mode == DragDropMode.MakeChild)
|
||
{
|
||
row.style.backgroundColor = new StyleColor(new Color(0.2f, 0.2f, 0.2f, 0.06f));
|
||
}
|
||
}
|
||
|
||
void ClearDragHighlight(VisualElement row)
|
||
{
|
||
if (row == null) return;
|
||
row.style.borderTopWidth = new StyleFloat(0f);
|
||
row.style.borderBottomWidth = new StyleFloat(0f);
|
||
row.style.borderLeftWidth = new StyleFloat(0f);
|
||
row.style.borderRightWidth = new StyleFloat(0f);
|
||
row.style.borderTopColor = new StyleColor(Color.clear);
|
||
row.style.borderBottomColor = new StyleColor(Color.clear);
|
||
row.style.backgroundColor = new StyleColor(Color.clear);
|
||
if (currentDragTargetRow == row)
|
||
{
|
||
currentDragTargetRow = null;
|
||
currentDragMode = DragDropMode.None;
|
||
}
|
||
}
|
||
|
||
void PerformDropOnRow(GameObject targetGO, DragDropMode mode)
|
||
{
|
||
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
||
Transform targetParent = null;
|
||
|
||
if (mode == DragDropMode.MakeChild)
|
||
{
|
||
targetParent = targetGO.transform;
|
||
}
|
||
else
|
||
{
|
||
// insert before/after among targetGO's siblings
|
||
targetParent = targetGO.transform.parent;
|
||
}
|
||
|
||
foreach (var o in DragAndDrop.objectReferences)
|
||
{
|
||
var draggedGO = o as GameObject;
|
||
if (draggedGO == null) continue;
|
||
// prevent making parent its own child
|
||
if (IsAncestor(draggedGO.transform, targetGO.transform)) continue;
|
||
|
||
if (mode == DragDropMode.MakeChild)
|
||
{
|
||
Undo.SetTransformParent(draggedGO.transform, targetParent, "Reparent GameObject (UXHierarchy)");
|
||
// place at end
|
||
draggedGO.transform.SetSiblingIndex(targetParent.childCount - 1);
|
||
}
|
||
else
|
||
{
|
||
// when targetParent == null -> scene root
|
||
Undo.SetTransformParent(draggedGO.transform, targetParent, "Reparent GameObject (UXHierarchy)");
|
||
|
||
int targetIndex;
|
||
if (targetParent != null)
|
||
{
|
||
var siblings = GetSiblings(targetParent);
|
||
targetIndex = Array.IndexOf(siblings, targetGO);
|
||
}
|
||
else
|
||
{
|
||
var roots = GetSceneRoots();
|
||
targetIndex = Array.IndexOf(roots, targetGO);
|
||
}
|
||
|
||
if (targetIndex < 0) targetIndex = 0;
|
||
int finalIndex = (mode == DragDropMode.InsertBefore) ? targetIndex : targetIndex + 1;
|
||
|
||
// clamp finalIndex
|
||
int maxIndex = (targetParent != null) ? targetParent.childCount : GetSceneRoots().Length;
|
||
finalIndex = Mathf.Clamp(finalIndex, 0, maxIndex);
|
||
|
||
draggedGO.transform.SetSiblingIndex(finalIndex);
|
||
}
|
||
}
|
||
|
||
if (prefabStage != null)
|
||
{
|
||
try
|
||
{
|
||
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
|
||
}
|
||
catch
|
||
{
|
||
}
|
||
}
|
||
else
|
||
{
|
||
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
|
||
}
|
||
|
||
RebuildTree();
|
||
}
|
||
|
||
GameObject[] GetSiblings(Transform parent)
|
||
{
|
||
if (parent == null) return new GameObject[0];
|
||
var arr = new GameObject[parent.childCount];
|
||
for (int i = 0; i < parent.childCount; ++i) arr[i] = parent.GetChild(i).gameObject;
|
||
return arr;
|
||
}
|
||
|
||
GameObject[] GetSceneRoots()
|
||
{
|
||
var roots = EditorSceneManager.GetActiveScene().GetRootGameObjects();
|
||
return roots;
|
||
}
|
||
|
||
bool IsAncestor(Transform potentialChild, Transform potentialAncestor)
|
||
{
|
||
if (potentialChild == null || potentialAncestor == null) return false;
|
||
var t = potentialAncestor;
|
||
while (t != null)
|
||
{
|
||
if (t == potentialChild) return true;
|
||
t = t.parent;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
Texture GetObjectIcon(GameObject go)
|
||
{
|
||
try
|
||
{
|
||
if (PrefabUtility.IsPartOfAnyPrefab(go))
|
||
{
|
||
var prefabAsset = PrefabUtility.GetCorrespondingObjectFromSource(go);
|
||
if (prefabAsset != null) return EditorGUIUtility.ObjectContent(prefabAsset, typeof(GameObject)).image;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
}
|
||
|
||
return EditorGUIUtility.ObjectContent(go, typeof(GameObject)).image;
|
||
}
|
||
|
||
void UpdateSelectionHighlighting()
|
||
{
|
||
var rows = treeContainer.Query<VisualElement>(className: "uxh-row").ToList();
|
||
foreach (var r in rows)
|
||
{
|
||
var go = r.parent.userData as GameObject;
|
||
if (go != null && selectedObjects.Contains(go))
|
||
{
|
||
r.style.backgroundColor = new StyleColor(new Color(0.24f, 0.48f, 0.78f, 0.18f));
|
||
}
|
||
else
|
||
{
|
||
r.style.backgroundColor = new StyleColor(Color.clear);
|
||
}
|
||
}
|
||
}
|
||
|
||
void SelectSingle(GameObject go)
|
||
{
|
||
selectedObjects = new List<GameObject> { go };
|
||
Selection.objects = selectedObjects.ToArray();
|
||
UpdateSelectionHighlighting();
|
||
}
|
||
|
||
void OnKeyDown(KeyDownEvent evt)
|
||
{
|
||
if (evt.keyCode == KeyCode.Delete || evt.keyCode == KeyCode.Backspace)
|
||
{
|
||
deleteKeyHeld = true;
|
||
nextAutoDeleteTime = EditorApplication.timeSinceStartup + DeleteInitialDelay;
|
||
DoDeleteSelected();
|
||
evt.StopImmediatePropagation();
|
||
return;
|
||
}
|
||
|
||
if (evt.keyCode == KeyCode.F2)
|
||
{
|
||
// ensure F2 triggers rename even if UI focus is elsewhere
|
||
if (Selection.activeGameObject != null)
|
||
StartRenameFor(Selection.activeGameObject);
|
||
evt.StopImmediatePropagation();
|
||
return;
|
||
}
|
||
|
||
if ((evt.ctrlKey || evt.commandKey) && evt.keyCode == KeyCode.A)
|
||
{
|
||
selectedObjects = flatList.Where(x => x != null).ToList();
|
||
Selection.objects = selectedObjects.ToArray();
|
||
UpdateSelectionHighlighting();
|
||
evt.StopImmediatePropagation();
|
||
return;
|
||
}
|
||
|
||
if ((evt.ctrlKey || evt.commandKey) && evt.keyCode == KeyCode.D)
|
||
{
|
||
DuplicateSelected();
|
||
evt.StopImmediatePropagation();
|
||
return;
|
||
}
|
||
}
|
||
|
||
void OnKeyUp(KeyUpEvent evt)
|
||
{
|
||
if (evt.keyCode == KeyCode.Delete || evt.keyCode == KeyCode.Backspace)
|
||
{
|
||
deleteKeyHeld = false;
|
||
evt.StopImmediatePropagation();
|
||
return;
|
||
}
|
||
}
|
||
|
||
// also catch F2 and Delete in IMGUI OnGUI (helps when UI Toolkit root doesn't have keyboard focus)
|
||
void OnGUI()
|
||
{
|
||
var e = Event.current;
|
||
if (e.type == EventType.KeyDown)
|
||
{
|
||
if (e.keyCode == KeyCode.F2)
|
||
{
|
||
if (Selection.activeGameObject != null)
|
||
{
|
||
StartRenameFor(Selection.activeGameObject);
|
||
e.Use();
|
||
}
|
||
}
|
||
else if (e.keyCode == KeyCode.Delete || e.keyCode == KeyCode.Backspace)
|
||
{
|
||
DoDeleteSelected();
|
||
e.Use();
|
||
}
|
||
else if ((e.control || e.command) && e.keyCode == KeyCode.D)
|
||
{
|
||
DuplicateSelected();
|
||
e.Use();
|
||
}
|
||
}
|
||
}
|
||
|
||
void DuplicateSelected()
|
||
{
|
||
if (selectedObjects == null || selectedObjects.Count == 0) return;
|
||
|
||
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
||
Transform rootPrefab = (prefabStage != null && prefabStage.prefabContentsRoot != null)
|
||
? prefabStage.prefabContentsRoot.transform
|
||
: null;
|
||
|
||
// 收集有效的原对象(排除 null)
|
||
var originals = selectedObjects.Where(x => x != null).ToList();
|
||
if (originals.Count == 0) return;
|
||
|
||
// 按 parent 分组,这样可以为每个 parent 保持插入索引逻辑
|
||
var grouped = originals.GroupBy(go => go.transform.parent);
|
||
|
||
List<GameObject> newSelections = new List<GameObject>();
|
||
|
||
foreach (var grp in grouped)
|
||
{
|
||
Transform parent = grp.Key; // 可能为 null(场景根)
|
||
// 按 sibling index 升序处理,避免插入时索引错乱
|
||
var items = grp.OrderBy(g => g.transform.GetSiblingIndex()).ToList();
|
||
int insertedCount = 0;
|
||
|
||
foreach (var original in items)
|
||
{
|
||
if (original == null) continue;
|
||
// 不要复制 prefab contents root 本身
|
||
if (rootPrefab != null && original == rootPrefab.gameObject) continue;
|
||
|
||
// 实例化(会复制子物体和所有组件的当前值)
|
||
GameObject dup = (GameObject)Object.Instantiate(original);
|
||
|
||
// 让名字在同父级下唯一(简单实现),避免大量 "(Clone)" 风格名字
|
||
dup.name = GetUniqueNameInParent(original.name, parent);
|
||
|
||
// 注册 Undo(便于撤销)
|
||
Undo.RegisterCreatedObjectUndo(dup, "Duplicate GameObject (UXHierarchy)");
|
||
|
||
// 设置 parent & sibling index (使用 SetParent(false) 保持本地变换)
|
||
if (parent != null)
|
||
{
|
||
dup.transform.SetParent(parent, false);
|
||
int origIndex = original.transform.GetSiblingIndex();
|
||
int finalIndex = Mathf.Clamp(origIndex + 1 + insertedCount, 0, parent.childCount - 1);
|
||
dup.transform.SetSiblingIndex(finalIndex);
|
||
}
|
||
else
|
||
{
|
||
// 场景根
|
||
dup.transform.SetParent(null);
|
||
int origIndex = original.transform.GetSiblingIndex();
|
||
int finalIndex = Mathf.Clamp(origIndex + 1 + insertedCount, 0, GetSceneRoots().Length - 1);
|
||
dup.transform.SetSiblingIndex(finalIndex);
|
||
}
|
||
|
||
newSelections.Add(dup);
|
||
insertedCount++;
|
||
}
|
||
}
|
||
|
||
// 把 Selection 设置为新复制的对象
|
||
Selection.objects = newSelections.Cast<Object>().ToArray();
|
||
selectedObjects = newSelections;
|
||
|
||
// 标记脏并刷新树视图
|
||
if (prefabStage != null)
|
||
{
|
||
try
|
||
{
|
||
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
|
||
}
|
||
catch
|
||
{
|
||
}
|
||
}
|
||
else
|
||
{
|
||
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
|
||
}
|
||
|
||
RebuildTree();
|
||
UpdateSelectionHighlighting();
|
||
Repaint();
|
||
}
|
||
|
||
// 简单的在父级中创建不同名的方法(避免与同名 sibling 冲突)
|
||
string GetUniqueNameInParent(string baseName, Transform parent)
|
||
{
|
||
GameObject[] siblings = parent != null ? GetSiblings(parent) : GetSceneRoots();
|
||
string name = baseName;
|
||
int suffix = 1;
|
||
// 如果已存在相同名字,则追加 " 1", " 2" ...(你可以改成其它命名规则)
|
||
while (siblings.Any(g => g != null && g.name == name))
|
||
{
|
||
name = baseName + " " + suffix;
|
||
suffix++;
|
||
}
|
||
|
||
return name;
|
||
}
|
||
|
||
void DoDeleteSelected()
|
||
{
|
||
if (selectedObjects == null || selectedObjects.Count == 0) return;
|
||
|
||
if (selectedObjects.Count > 10)
|
||
{
|
||
if (!EditorUtility.DisplayDialog("Delete objects", $"Delete {selectedObjects.Count} objects?", "Delete", "Cancel"))
|
||
return;
|
||
}
|
||
|
||
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
||
var rootPrefab = prefabStage.prefabContentsRoot;
|
||
|
||
foreach (var go in selectedObjects.ToArray())
|
||
{
|
||
if (go == null) continue;
|
||
if (rootPrefab == go)
|
||
{
|
||
Debug.Log("Component Root Node Can Not Delete");
|
||
continue;
|
||
}
|
||
|
||
Undo.DestroyObjectImmediate(go);
|
||
}
|
||
|
||
selectedObjects.Clear();
|
||
Selection.objects = new Object[0];
|
||
|
||
if (prefabStage != null)
|
||
{
|
||
try
|
||
{
|
||
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
|
||
}
|
||
catch
|
||
{
|
||
}
|
||
}
|
||
else
|
||
{
|
||
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
|
||
}
|
||
|
||
RebuildTree();
|
||
}
|
||
|
||
void StartRenameFor(GameObject go)
|
||
{
|
||
if (go == null) return;
|
||
var nodes = treeContainer.Query<VisualElement>().Where(n => n.userData == go).ToList();
|
||
if (nodes.Count > 0)
|
||
{
|
||
var node = nodes[0];
|
||
var row = node.ElementAt(0);
|
||
// find the label child (not other labels)
|
||
Label label = null;
|
||
foreach (var child in row.Children())
|
||
{
|
||
if (child is Label l && l.userData is int)
|
||
{
|
||
label = l;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (label != null) StartRename(row, go, label);
|
||
else Debug.LogWarning("UXHierarchy: cannot find label to rename.");
|
||
}
|
||
}
|
||
|
||
void StartRename(VisualElement row, GameObject go, Label label)
|
||
{
|
||
StopRename();
|
||
|
||
var tf = new TextField();
|
||
tf.value = go.name;
|
||
tf.style.flexGrow = 1;
|
||
tf.style.marginLeft = 0;
|
||
tf.style.marginRight = 0;
|
||
|
||
int idx = row.IndexOf(label);
|
||
if (idx >= 0)
|
||
{
|
||
row.Remove(label);
|
||
row.Insert(idx, tf);
|
||
tf.Focus();
|
||
tf.SelectAll();
|
||
activeRenameField = tf;
|
||
|
||
tf.RegisterCallback<KeyDownEvent>(evt =>
|
||
{
|
||
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
|
||
{
|
||
CommitRename(go, tf.value);
|
||
evt.StopImmediatePropagation();
|
||
}
|
||
else if (evt.keyCode == KeyCode.Escape)
|
||
{
|
||
CancelRename(row, tf, label);
|
||
evt.StopImmediatePropagation();
|
||
}
|
||
});
|
||
|
||
tf.RegisterCallback<FocusOutEvent>(evt => { CommitRename(go, tf.value); });
|
||
}
|
||
}
|
||
|
||
void CommitRename(GameObject go, string newName)
|
||
{
|
||
if (go == null) return;
|
||
if (newName == go.name)
|
||
{
|
||
StopRename();
|
||
return;
|
||
}
|
||
|
||
Undo.RecordObject(go, "Rename GameObject (UXHierarchy)");
|
||
go.name = newName;
|
||
EditorUtility.SetDirty(go);
|
||
StopRename();
|
||
|
||
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
||
if (prefabStage != null)
|
||
{
|
||
try
|
||
{
|
||
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
|
||
}
|
||
catch
|
||
{
|
||
}
|
||
}
|
||
|
||
RebuildTree();
|
||
}
|
||
|
||
void CancelRename(VisualElement row, TextField tf, Label label)
|
||
{
|
||
int idx = row.IndexOf(tf);
|
||
if (idx >= 0)
|
||
{
|
||
row.Remove(tf);
|
||
row.Insert(idx, label);
|
||
}
|
||
|
||
activeRenameField = null;
|
||
}
|
||
|
||
void StopRename()
|
||
{
|
||
if (activeRenameField == null) return;
|
||
var tf = activeRenameField;
|
||
var parent = tf.parent;
|
||
if (parent != null)
|
||
{
|
||
var label = new Label(tf.value);
|
||
label.style.flexGrow = 1;
|
||
int idx = parent.IndexOf(tf);
|
||
parent.Remove(tf);
|
||
parent.Insert(idx, label);
|
||
}
|
||
|
||
activeRenameField = null;
|
||
}
|
||
|
||
// New: create empty UI GameObject with RectTransform. If parentGO is null, create at root or prefab root
|
||
void CreateEmptyUI(GameObject parentGO)
|
||
{
|
||
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
||
Transform parentTransform = null;
|
||
if (parentGO != null)
|
||
parentTransform = parentGO.transform;
|
||
else if (prefabStage != null && prefabStage.prefabContentsRoot != null)
|
||
parentTransform = prefabStage.prefabContentsRoot.transform;
|
||
else
|
||
parentTransform = null; // scene root
|
||
|
||
var go = new GameObject("New UI Empty", typeof(RectTransform));
|
||
Undo.RegisterCreatedObjectUndo(go, "Create New UI Empty (UXHierarchy)");
|
||
if (parentTransform != null)
|
||
{
|
||
go.transform.SetParent(parentTransform, false);
|
||
}
|
||
// else leave at scene root (transform.parent == null)
|
||
|
||
Selection.activeGameObject = go;
|
||
if (prefabStage != null)
|
||
{
|
||
try
|
||
{
|
||
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
|
||
}
|
||
catch
|
||
{
|
||
}
|
||
}
|
||
else
|
||
{
|
||
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
|
||
}
|
||
|
||
RebuildTree();
|
||
}
|
||
|
||
// fold state persistence keyed by context (prefab asset path or scene path)
|
||
string GetFoldPrefsKey()
|
||
{
|
||
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
||
if (prefabStage != null)
|
||
{
|
||
var path = prefabStage.assetPath;
|
||
if (string.IsNullOrEmpty(path)) path = "prefab_unknown";
|
||
return $"{FoldoutPrefsKeyBase}::prefab::{path}";
|
||
}
|
||
else
|
||
{
|
||
var scene = EditorSceneManager.GetActiveScene();
|
||
var sp = string.IsNullOrEmpty(scene.path) ? "scene_unsaved" : scene.path;
|
||
return $"{FoldoutPrefsKeyBase}::scene::{sp}";
|
||
}
|
||
}
|
||
|
||
void SaveFoldStates()
|
||
{
|
||
try
|
||
{
|
||
var entries = foldStates.Select(kv => $"{kv.Key}:{(kv.Value ? 1 : 0)}");
|
||
var joined = string.Join(",", entries);
|
||
EditorPrefs.SetString(GetFoldPrefsKey(), joined);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.LogWarning("UXHierarchy could not save fold states: " + ex.Message);
|
||
}
|
||
}
|
||
|
||
void LoadFoldStates()
|
||
{
|
||
foldStates.Clear();
|
||
var key = GetFoldPrefsKey();
|
||
if (EditorPrefs.HasKey(key))
|
||
{
|
||
var joined = EditorPrefs.GetString(key);
|
||
if (!string.IsNullOrEmpty(joined))
|
||
{
|
||
try
|
||
{
|
||
var parts = joined.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||
foreach (var p in parts)
|
||
{
|
||
var kv = p.Split(':');
|
||
if (kv.Length == 2)
|
||
{
|
||
if (int.TryParse(kv[0], out int id) && int.TryParse(kv[1], out int v))
|
||
{
|
||
foldStates[id] = (v == 1);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
foldStates.Clear();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private void CreateCommonUIMenu(ContextualMenuPopulateEvent evt, GameObject target)
|
||
{
|
||
MenuCommand command = new MenuCommand(target);
|
||
evt.menu.AppendSeparator();
|
||
evt.menu.AppendAction("UXImage", a => { UXCreateHelper.CreateUXImage(command); }, a => DropdownMenuAction.Status.Normal);
|
||
evt.menu.AppendAction("UXTextMeshPro", a => { UXCreateHelper.CreateUXTextMeshPro(command); }, a => DropdownMenuAction.Status.Normal);
|
||
evt.menu.AppendAction("UXButton", a => { UXCreateHelper.CreateUXButton(command); }, a => DropdownMenuAction.Status.Normal);
|
||
evt.menu.AppendAction("UXInput Field", a => { UXCreateHelper.CreateUXInputField(command); }, a => DropdownMenuAction.Status.Normal);
|
||
evt.menu.AppendAction("UXScrollView", a => { UXCreateHelper.CreateUxRecyclerView(); }, a => DropdownMenuAction.Status.Normal);
|
||
evt.menu.AppendAction("UXSlider", a => { UXCreateHelper.CreateUXToggle(command); }, a => DropdownMenuAction.Status.Normal);
|
||
}
|
||
|
||
private void CreateControlerUIMenu(ContextualMenuPopulateEvent evt, GameObject target)
|
||
{
|
||
evt.menu.AppendSeparator();
|
||
evt.menu.AppendAction("添加控制器", a =>
|
||
{
|
||
if (!target.TryGetComponent(typeof(UXControllerStateRecorder), out Component controller))
|
||
{
|
||
target.AddComponent(typeof(UXControllerStateRecorder));
|
||
}
|
||
}, a => DropdownMenuAction.Status.Normal);
|
||
}
|
||
}
|