AlicizaX/Client/Packages/com.alicizax.uxtool/Editor/UXGUI/SceneView/Hierachy/UXHierachyWindow.cs

1306 lines
44 KiB
C#
Raw Normal View History

2025-12-01 16:46:28 +08:00
using System;
using System.Collections.Generic;
using System.Linq;
using AlicizaX.UXTool;
using AlicizaX.UI;
2025-12-01 16:46:28 +08:00
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.CreateUXSlider(command); }, a => DropdownMenuAction.Status.Normal);
2025-12-01 16:46:28 +08:00
}
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);
}
}