using System; using System.Collections.Generic; using System.Linq; using Aliciza.UXTool; using AlicizaX.UI.Runtime; 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 foldStates = new Dictionary(); // flat ordered list of GameObjects displayed (for shift-range selection) List flatList = new List(); // selection state in the window (keeps synced with UnityEditor.Selection) List selectedObjects = new List(); 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(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(OnKeyDown); root.RegisterCallback(OnKeyUp); root.RegisterCallback(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(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(evt => { if (PrefabStageUtils.InEmptyStage) return; rootDropZone.style.backgroundColor = new StyleColor(new Color(0.2f, 0.2f, 0.2f, 0.06f)); evt.StopImmediatePropagation(); }); rootDropZone.RegisterCallback(evt => { if (PrefabStageUtils.InEmptyStage) return; rootDropZone.style.backgroundColor = new StyleColor(Color.clear); if (currentDragTargetRow != null) { ClearDragHighlight(currentDragTargetRow); } evt.StopImmediatePropagation(); }); rootDropZone.RegisterCallback(evt => { if (PrefabStageUtils.InEmptyStage) return; if (DragAndDrop.objectReferences != null && DragAndDrop.objectReferences.Length > 0) DragAndDrop.visualMode = DragAndDropVisualMode.Move; evt.StopImmediatePropagation(); }); rootDropZone.RegisterCallback(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(evt => { if (currentDragTargetRow != null) { ClearDragHighlight(currentDragTargetRow); } evt.StopImmediatePropagation(); }); root.RegisterCallback(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() != 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 childNodes = new List(); 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((evt) => { if (evt.button == 0) { expanded = !expanded; foldStates[id] = expanded; foldLabel.text = expanded ? "▼" : "▶"; SaveFoldStates(); var childrenContainer = node.Q("children"); if (childrenContainer != null) childrenContainer.style.display = expanded ? DisplayStyle.Flex : DisplayStyle.None; evt.StopImmediatePropagation(); } }); // hover color foldLabel.RegisterCallback((evt) => foldLabel.style.color = new StyleColor(Color.white)); foldLabel.RegisterCallback((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(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(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(evt => { /* highlight handled in DragUpdated */ evt.StopImmediatePropagation(); }); row.RegisterCallback(evt => { ClearDragHighlight(row); evt.StopImmediatePropagation(); }); row.RegisterCallback(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(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(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 { 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 newSelections = new List(); 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().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().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(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(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); } 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); } }