1305 lines
44 KiB
C#
1305 lines
44 KiB
C#
|
|
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<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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
}
|