AlicizaX/Client/Packages/com.alicizax.uxtool/Editor/UXGUI/SceneView/Hierachy/UXHierachyWindow.cs
2025-12-19 20:53:25 +08:00

1306 lines
44 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}