AlicizaX/Client/Packages/com.alicizax.uxtool/Editor/UXGUI/SceneView/Project/UXProjectWindow.cs

1245 lines
42 KiB
C#
Raw Normal View History

2025-12-01 16:46:28 +08:00
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AlicizaX.UXTool;
2025-12-01 16:46:28 +08:00
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
public class PrefabTreeViewWindow : EditorWindow
{
private const string EditorPrefKeyPrefix = "PrefabTreeView_Expanded_"; // key + Root
private ScrollView treeScroll;
private TextField searchField;
private Texture2D folderIcon;
private Texture2D folderEmptyIcon;
private Texture2D defaultPrefabIcon;
private Image previewIcon;
private Label previewLabel;
private HashSet<string> expandedPaths = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
private HashSet<string> expandedSnapshot = null;
private HashSet<string> selectedPaths = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
private string searchText = string.Empty;
private string currentDragTargetPath = null;
private VisualElement currentDragTargetElement = null;
[MenuItem("Window/Prefab Tree View (Fixed Root)")]
public static void ShowWindow()
{
var wnd = GetWindow<PrefabTreeViewWindow>();
wnd.titleContent = new GUIContent("Prefab Tree View");
wnd.minSize = new Vector2(420, 300);
}
private void OnEnable()
{
folderIcon = EditorGUIUtility.IconContent("Folder Icon").image as Texture2D;
if (folderIcon == null) folderIcon = EditorGUIUtility.IconContent("Folder").image as Texture2D;
folderEmptyIcon = EditorGUIUtility.IconContent("FolderEmpty Icon").image as Texture2D;
if (folderEmptyIcon == null) folderEmptyIcon = EditorGUIUtility.IconContent("FolderEmpty").image as Texture2D;
if (folderEmptyIcon == null) folderEmptyIcon = folderIcon; // fallback
defaultPrefabIcon = EditorGUIUtility.IconContent("Prefab Icon").image as Texture2D;
if (defaultPrefabIcon == null) defaultPrefabIcon = EditorGUIUtility.IconContent("PrefabNormal Icon").image as Texture2D;
rootVisualElement.Clear();
BuildUI();
LoadExpandedState();
RefreshTree();
rootVisualElement.RegisterCallback<KeyDownEvent>(OnKeyDown);
rootVisualElement.focusable = true;
rootVisualElement.RegisterCallback<MouseDownEvent>(evt => { rootVisualElement.Focus(); });
}
private void OnDisable()
{
SaveExpandedState();
}
private VisualElement previewContainer;
private void BuildUI()
{
var root = rootVisualElement;
var split = new UnityEngine.UIElements.TwoPaneSplitView(1, 50, TwoPaneSplitViewOrientation.Vertical);
split.style.flexGrow = 1;
var topContainer = new VisualElement();
topContainer.name = "TopContainer";
topContainer.style.flexDirection = FlexDirection.Column;
topContainer.style.flexGrow = 1;
var searchRow = new VisualElement();
searchRow.style.flexDirection = FlexDirection.Row;
searchRow.style.alignItems = Align.Center;
searchRow.style.marginBottom = 4;
searchField = new TextField();
searchField.style.flexGrow = 1;
searchField.RegisterCallback<ChangeEvent<string>>(evt => OnSearchChanged(evt.newValue));
var clearBtn = new Button(() =>
{
searchField.value = string.Empty;
OnSearchChanged(string.Empty);
}) { text = "Clear" };
var refreshBtn = new Button(() => RefreshTree());
refreshBtn.style.backgroundImage = new StyleBackground(EditorGUIUtility.IconContent("d_Refresh").image as Texture2D);
refreshBtn.style.height = 20;
refreshBtn.style.width = 20;
refreshBtn.style.backgroundSize = new BackgroundSize(new Length(80, LengthUnit.Percent), new Length(80, LengthUnit.Percent));
var createFolder = new Button(() => { CreateFolderUnder(Def_UXGUIPath.UIResRootPath); });
createFolder.style.backgroundImage = new StyleBackground(EditorGUIUtility.IconContent("Folder Icon").image as Texture2D);
createFolder.style.height = 20;
createFolder.style.width = 20;
createFolder.style.backgroundSize = new BackgroundSize(new Length(80, LengthUnit.Percent), new Length(80, LengthUnit.Percent));
searchRow.Add(searchField);
searchRow.Add(refreshBtn);
searchRow.Add(createFolder);
topContainer.Add(searchRow);
treeScroll = new ScrollView();
treeScroll.style.flexGrow = 1;
// treeScroll.verticalScrollerVisibility = ScrollerVisibility.Hidden;
treeScroll.horizontalScrollerVisibility = ScrollerVisibility.Hidden;
treeScroll.pickingMode = PickingMode.Position;
treeScroll.RegisterCallback<DragEnterEvent>(evt =>
{
OnDragEnterOrUpdate(evt.target as VisualElement);
evt.StopPropagation();
});
treeScroll.RegisterCallback<DragUpdatedEvent>(evt =>
{
OnDragEnterOrUpdate(evt.target as VisualElement);
evt.StopPropagation();
});
treeScroll.RegisterCallback<DragPerformEvent>(evt =>
{
OnDragPerform(evt);
evt.StopPropagation();
});
treeScroll.RegisterCallback<DragLeaveEvent>(evt =>
{
ClearDragTarget();
evt.StopPropagation();
});
treeScroll.RegisterCallback<DragExitedEvent>(evt => { ClearDragTarget(); });
treeScroll.AddManipulator(new ContextualMenuManipulator(evt =>
{
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 => CreateFolderUnder(Def_UXGUIPath.UIResRootPath), a => DropdownMenuAction.Status.Normal);
evt.menu.AppendAction("新建组件", a => CreatePrefabUnder(Def_UXGUIPath.UIResRootPath), a => DropdownMenuAction.Status.Normal);
if (selectedPaths.Count > 0)
{
evt.menu.AppendSeparator();
evt.menu.AppendAction($"删除选中 ({selectedPaths.Count})", a => DeleteSelectionWithConfirm(), a => DropdownMenuAction.Status.Normal);
}
}
}));
topContainer.Add(treeScroll);
previewContainer = new VisualElement();
previewContainer.name = "PreviewContainer";
previewContainer.style.flexGrow = 1;
previewContainer.style.minHeight = 180;
previewContainer.style.backgroundColor = new StyleColor(new Color(0.2f, 0.2f, 0.2f));
previewLabel = new Label("Preview");
previewLabel.name = "previewLabel";
previewLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
previewLabel.style.height = 24;
previewLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
previewContainer.Add(previewLabel);
previewIcon = new Image();
previewIcon.style.height = Length.Percent(100);
previewIcon.style.width = Length.Percent(100);
previewContainer.Add(previewIcon);
split.Add(topContainer);
split.Add(previewContainer);
root.Add(split);
root.RegisterCallback<MouseDownEvent>(evt =>
{
if (evt.button != 0) return;
var target = evt.target as VisualElement;
bool hasPathAncestor = false;
while (target != null)
{
if (target.userData is string)
{
hasPathAncestor = true;
break;
}
target = target.parent as VisualElement;
}
if (!hasPathAncestor) ClearSelection();
}, TrickleDown.TrickleDown);
}
private void OnSearchChanged(string newValue)
{
var trimmed = (newValue ?? string.Empty).Trim();
bool wasEmpty = string.IsNullOrEmpty(searchText);
bool nowEmpty = string.IsNullOrEmpty(trimmed);
if (wasEmpty && !nowEmpty)
{
expandedSnapshot = new HashSet<string>(expandedPaths, StringComparer.InvariantCultureIgnoreCase);
}
if (!wasEmpty && nowEmpty)
{
if (expandedSnapshot != null)
{
expandedPaths = new HashSet<string>(expandedSnapshot, StringComparer.InvariantCultureIgnoreCase);
expandedSnapshot = null;
}
}
searchText = trimmed;
RefreshTree();
}
private void RefreshTree()
{
treeScroll.Clear();
selectedPaths.Clear();
Selection.objects = new UnityEngine.Object[0];
ClearDragTarget();
if (!AssetDatabase.IsValidFolder(Def_UXGUIPath.UIResRootPath))
{
treeScroll.Add(new Label($"Invalid root path: {Def_UXGUIPath.UIResRootPath}. Please create it in Project window."));
return;
}
var subfolders = AssetDatabase.GetSubFolders(Def_UXGUIPath.UIResRootPath);
Array.Sort(subfolders, StringComparer.InvariantCultureIgnoreCase);
foreach (var sf in subfolders)
{
if (string.IsNullOrEmpty(searchText) || FolderMatchesSearchRecursive(sf))
{
var item = CreateFolderItem(sf, 0, inSearch: !string.IsNullOrEmpty(searchText));
if (item != null) treeScroll.Add(item);
}
}
}
private bool FolderMatchesSearchRecursive(string folderPath)
{
if (string.IsNullOrEmpty(searchText)) return true;
var key = searchText.ToLowerInvariant();
if (Path.GetFileName(folderPath).ToLowerInvariant().Contains(key)) return true;
var prefabGuids = AssetDatabase.FindAssets("t:GameObject", new[] { folderPath });
foreach (var g in prefabGuids)
{
var p = AssetDatabase.GUIDToAssetPath(g);
if (Path.GetFileName(p).ToLowerInvariant().Contains(key)) return true;
}
var subs = AssetDatabase.GetSubFolders(folderPath);
foreach (var s in subs)
{
if (FolderMatchesSearchRecursive(s)) return true;
}
return false;
}
private VisualElement CreateFolderItem(string folderAssetPath, int indentLevel, bool inSearch = false)
{
var container = new VisualElement();
container.style.flexDirection = FlexDirection.Column;
container.style.alignSelf = Align.Stretch;
var header = new VisualElement();
header.style.flexDirection = FlexDirection.Row;
header.style.alignItems = Align.Center;
header.style.paddingLeft = 2 + indentLevel * 12;
header.style.paddingTop = 2;
header.style.paddingBottom = 2;
header.style.marginLeft = indentLevel > 0 ? -18 : 0;
header.pickingMode = PickingMode.Position;
bool hasChildren = FolderHasAnyDirectChild(folderAssetPath);
bool isExpanded = expandedPaths.Contains(folderAssetPath);
var chevron = new Label(hasChildren ? (isExpanded ? "▼" : "▶") : "");
chevron.style.unityTextAlign = TextAnchor.MiddleLeft;
chevron.style.width = 12;
chevron.style.marginRight = 6;
chevron.style.fontSize = 8;
var iconImg = new UnityEngine.UIElements.Image();
iconImg.image = hasChildren ? folderIcon : folderEmptyIcon;
iconImg.style.width = 16;
iconImg.style.height = 16;
iconImg.style.marginRight = 6;
var nameLabel = new Label(Path.GetFileName(folderAssetPath));
nameLabel.style.unityTextAlign = TextAnchor.MiddleLeft;
header.Add(chevron);
header.Add(iconImg);
header.Add(nameLabel);
header.userData = folderAssetPath;
var childContainer = new VisualElement();
childContainer.style.flexDirection = FlexDirection.Column;
childContainer.style.marginLeft = 12;
bool displayExpanded = isExpanded;
if (inSearch && !string.IsNullOrEmpty(searchText))
{
if (FolderMatchesSearchRecursive(folderAssetPath))
{
var selfMatches = Path.GetFileName(folderAssetPath).ToLowerInvariant().Contains(searchText.ToLowerInvariant());
if (!selfMatches) displayExpanded = true;
}
else
{
return null;
}
}
childContainer.style.display = displayExpanded ? DisplayStyle.Flex : DisplayStyle.None;
if (displayExpanded && childContainer.childCount == 0)
{
PopulateFolderChildren(folderAssetPath, childContainer, indentLevel + 1, inSearch);
}
chevron.RegisterCallback<MouseDownEvent>(evt =>
{
if (hasChildren && string.IsNullOrEmpty(searchText))
{
bool willExpand = childContainer.style.display == DisplayStyle.None;
SetExpanded(chevron, childContainer, willExpand);
if (willExpand && childContainer.childCount == 0)
{
PopulateFolderChildren(folderAssetPath, childContainer, indentLevel + 1, inSearch);
}
if (willExpand) expandedPaths.Add(folderAssetPath);
else expandedPaths.Remove(folderAssetPath);
SaveExpandedState();
}
HandleSelectionClick(folderAssetPath, evt);
evt.StopImmediatePropagation();
});
header.RegisterCallback<MouseDownEvent>(evt =>
{
if (evt.button != 0) return;
HandleSelectionClick(folderAssetPath, evt);
var mouseDownPos = evt.mousePosition;
EventCallback<MouseMoveEvent> moveCb = null;
EventCallback<MouseUpEvent> upCb = null;
List<string> dragPaths = new List<string>();
if (selectedPaths.Contains(folderAssetPath)) dragPaths = selectedPaths.ToList();
else dragPaths.Add(folderAssetPath);
moveCb = (MouseMoveEvent me) =>
{
if ((me.mousePosition - mouseDownPos).sqrMagnitude > 16f)
{
StartDragAssets(dragPaths);
header.UnregisterCallback(moveCb);
header.UnregisterCallback(upCb);
}
};
upCb = (MouseUpEvent mu) =>
{
header.UnregisterCallback(moveCb);
header.UnregisterCallback(upCb);
};
header.RegisterCallback(moveCb);
header.RegisterCallback(upCb);
evt.StopImmediatePropagation();
});
header.AddManipulator(new ContextualMenuManipulator(evt =>
{
evt.menu.ClearItems();
if (selectedPaths.Count == 1 && selectedPaths.Contains(folderAssetPath))
{
evt.menu.AppendAction("新建文件夹", a => CreateFolderUnder(folderAssetPath), a => DropdownMenuAction.Status.Normal);
evt.menu.AppendAction("新建组件", a => CreatePrefabUnder(folderAssetPath), a => DropdownMenuAction.Status.Normal);
evt.menu.AppendSeparator();
evt.menu.AppendAction("删除文件夹", a => DeleteSingleFolderWithConfirm(folderAssetPath), a => DropdownMenuAction.Status.Normal);
}
else if (selectedPaths.Count > 0)
{
evt.menu.AppendAction($"删除选中 ({selectedPaths.Count})", a => DeleteSelectionWithConfirm(), a => DropdownMenuAction.Status.Normal);
}
else
{
evt.menu.AppendAction("新建文件夹", a => CreateFolderUnder(folderAssetPath), a => DropdownMenuAction.Status.Normal);
evt.menu.AppendAction("新建组件", a => CreatePrefabUnder(folderAssetPath), a => DropdownMenuAction.Status.Normal);
evt.menu.AppendSeparator();
evt.menu.AppendAction("删除文件夹", a => DeleteSingleFolderWithConfirm(folderAssetPath), a => DropdownMenuAction.Status.Normal);
}
}));
header.RegisterCallback<DragEnterEvent>(evt =>
{
OnDragEnterOrUpdate(evt.target as VisualElement);
evt.StopPropagation();
});
header.RegisterCallback<DragUpdatedEvent>(evt =>
{
OnDragEnterOrUpdate(evt.target as VisualElement);
evt.StopPropagation();
});
header.RegisterCallback<DragPerformEvent>(evt =>
{
OnDragPerform(evt);
evt.StopPropagation();
});
header.RegisterCallback<DragLeaveEvent>(evt =>
{
ClearDragTarget();
evt.StopPropagation();
});
container.Add(header);
container.Add(childContainer);
return container;
}
private void PopulateFolderChildren(string folderAssetPath, VisualElement childContainer, int indentLevel, bool inSearch)
{
var subfolders = AssetDatabase.GetSubFolders(folderAssetPath);
Array.Sort(subfolders, StringComparer.InvariantCultureIgnoreCase);
foreach (var sf in subfolders)
{
if (string.IsNullOrEmpty(searchText) || FolderMatchesSearchRecursive(sf))
{
var el = CreateFolderItem(sf, indentLevel, inSearch);
if (el != null) childContainer.Add(el);
}
}
var prefabs = GetPrefabsInFolder(folderAssetPath);
foreach (var p in prefabs)
{
if (!string.IsNullOrEmpty(searchText))
{
if (!Path.GetFileName(p).ToLowerInvariant().Contains(searchText.ToLowerInvariant())) continue;
}
childContainer.Add(CreatePrefabRow(p, indentLevel));
}
}
private void SetExpanded(Label chevron, VisualElement childContainer, bool expand)
{
chevron.text = expand ? "▼" : "▶";
childContainer.style.display = expand ? DisplayStyle.Flex : DisplayStyle.None;
}
private bool FolderHasAnyDirectChild(string folderAssetPath)
{
var subs = AssetDatabase.GetSubFolders(folderAssetPath);
if (subs != null && subs.Length > 0) return true;
var guids = AssetDatabase.FindAssets("", new[] { folderAssetPath });
foreach (var g in guids)
{
var p = AssetDatabase.GUIDToAssetPath(g);
if (string.IsNullOrEmpty(p)) continue;
var parent = Path.GetDirectoryName(p).Replace("\\", "/");
if (string.Equals(parent, folderAssetPath, StringComparison.InvariantCultureIgnoreCase))
{
if (p.EndsWith(".meta", StringComparison.InvariantCultureIgnoreCase)) continue;
return true;
}
}
return false;
}
private VisualElement CreatePrefabRow(string prefabAssetPath, int indentLevel)
{
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.alignItems = Align.Center;
row.style.paddingLeft = 2 + indentLevel * 12;
row.style.paddingTop = 2;
row.style.paddingBottom = 2;
row.pickingMode = PickingMode.Position;
Texture2D icon = AssetDatabase.GetCachedIcon(prefabAssetPath) as Texture2D;
var mainObj = AssetDatabase.LoadMainAssetAtPath(prefabAssetPath);
if (icon == null && mainObj != null)
{
var objContent = EditorGUIUtility.ObjectContent(mainObj, mainObj.GetType());
icon = objContent != null ? objContent.image as Texture2D : null;
}
if (icon == null) icon = defaultPrefabIcon;
var iconImg = new UnityEngine.UIElements.Image();
iconImg.image = icon;
iconImg.style.width = 16;
iconImg.style.height = 16;
iconImg.style.marginRight = 6;
var lbl = new Label(Path.GetFileNameWithoutExtension(prefabAssetPath));
lbl.style.unityTextAlign = TextAnchor.MiddleLeft;
row.Add(iconImg);
row.Add(lbl);
row.userData = prefabAssetPath;
row.RegisterCallback<MouseDownEvent>(evt =>
{
if (evt.button != 0) return;
if (evt.clickCount == 2)
{
if (mainObj != null)
{
PrefabStageUtils.SwitchStage(prefabAssetPath);
}
evt.StopImmediatePropagation();
return;
}
HandleSelectionClick(prefabAssetPath, evt);
var mouseDownPos = evt.mousePosition;
EventCallback<MouseMoveEvent> moveCb = null;
EventCallback<MouseUpEvent> upCb = null;
List<string> dragPaths = new List<string>();
if (selectedPaths.Contains(prefabAssetPath)) dragPaths = selectedPaths.ToList();
else dragPaths.Add(prefabAssetPath);
moveCb = (MouseMoveEvent me) =>
{
if ((me.mousePosition - mouseDownPos).sqrMagnitude > 16f)
{
StartDragAssets(dragPaths);
row.UnregisterCallback(moveCb);
row.UnregisterCallback(upCb);
}
};
upCb = (MouseUpEvent mu) =>
{
row.UnregisterCallback(moveCb);
row.UnregisterCallback(upCb);
};
row.RegisterCallback(moveCb);
row.RegisterCallback(upCb);
evt.StopImmediatePropagation();
});
row.AddManipulator(new ContextualMenuManipulator(evt =>
{
evt.menu.ClearItems();
if (selectedPaths.Count > 0)
{
evt.menu.AppendAction($"删除选中 ({selectedPaths.Count})", a => DeleteSelectionWithConfirm(), a => DropdownMenuAction.Status.Normal);
}
else
{
evt.menu.AppendAction("删除预制体", a => DeleteSinglePrefabWithConfirm(prefabAssetPath), a => DropdownMenuAction.Status.Normal);
}
}));
row.RegisterCallback<DragEnterEvent>(evt =>
{
OnDragEnterOrUpdate(evt.target as VisualElement);
evt.StopPropagation();
});
row.RegisterCallback<DragUpdatedEvent>(evt =>
{
OnDragEnterOrUpdate(evt.target as VisualElement);
evt.StopPropagation();
});
row.RegisterCallback<DragPerformEvent>(evt =>
{
OnDragPerform(evt);
evt.StopPropagation();
});
row.RegisterCallback<DragLeaveEvent>(evt =>
{
ClearDragTarget();
evt.StopPropagation();
});
return row;
}
private string[] GetPrefabsInFolder(string folderAssetPath)
{
var guids = AssetDatabase.FindAssets("t:GameObject", new[] { folderAssetPath });
List<string> result = new List<string>();
foreach (var g in guids)
{
var p = AssetDatabase.GUIDToAssetPath(g);
var parent = Path.GetDirectoryName(p).Replace("\\", "/");
if (string.Equals(parent, folderAssetPath, StringComparison.InvariantCultureIgnoreCase))
result.Add(p);
}
result.Sort(StringComparer.InvariantCultureIgnoreCase);
return result.ToArray();
}
private void HandleSelectionClick(string assetPath, MouseDownEvent evt)
{
bool isCtrl = evt.ctrlKey || evt.commandKey;
if (isCtrl)
{
if (selectedPaths.Contains(assetPath)) selectedPaths.Remove(assetPath);
else selectedPaths.Add(assetPath);
}
else
{
selectedPaths.Clear();
selectedPaths.Add(assetPath);
}
UpdateEditorSelection();
UpdateVisualSelectionMarkers();
}
private void ClearSelection()
{
if (selectedPaths.Count == 0) return;
selectedPaths.Clear();
UpdateEditorSelection();
UpdateVisualSelectionMarkers();
}
private void UpdateEditorSelection()
{
var list = new List<UnityEngine.Object>();
foreach (var p in selectedPaths)
{
var o = AssetDatabase.LoadMainAssetAtPath(p);
if (o != null) list.Add(o);
}
Selection.objects = list.ToArray();
}
private void UpdateVisualSelectionMarkers()
{
void Mark(VisualElement elem)
{
if (elem.userData is string path)
{
if (selectedPaths.Contains(path))
elem.style.backgroundColor = new StyleColor(new Color(0.24f, 0.48f, 0.90f, 0.25f));
else
elem.style.backgroundColor = StyleKeyword.Null;
}
foreach (var c in elem.Children()) Mark(c);
}
foreach (var child in treeScroll.Children()) Mark(child);
UpdatePreviewTexture();
}
private void DeleteSelectionWithConfirm()
{
if (selectedPaths.Count == 0) return;
string message = "Are you sure you want to delete the following assets?\n\n" + string.Join("\n", selectedPaths);
if (!EditorUtility.DisplayDialog("Delete Selected", message, "Delete", "Cancel")) return;
var prefabList = new List<string>();
var folderList = new List<string>();
foreach (var p in selectedPaths)
{
if (AssetDatabase.IsValidFolder(p)) folderList.Add(p);
else prefabList.Add(p);
}
foreach (var p in prefabList) AssetDatabase.DeleteAsset(p);
folderList.Sort((a, b) => b.Length.CompareTo(a.Length));
foreach (var f in folderList) AssetDatabase.DeleteAsset(f);
AssetDatabase.Refresh();
selectedPaths.Clear();
RefreshTree();
}
private void DeleteSinglePrefabWithConfirm(string prefabAssetPath)
{
if (!EditorUtility.DisplayDialog("Delete Prefab", $"Delete prefab:\n{prefabAssetPath}?", "Delete", "Cancel")) return;
AssetDatabase.DeleteAsset(prefabAssetPath);
AssetDatabase.Refresh();
RefreshTree();
}
private void DeleteSingleFolderWithConfirm(string folderAssetPath)
{
if (!EditorUtility.DisplayDialog("Delete Folder", $"Delete folder and all contents:\n{folderAssetPath}?", "Delete", "Cancel")) return;
AssetDatabase.DeleteAsset(folderAssetPath);
AssetDatabase.Refresh();
RefreshTree();
}
private void CreateFolderUnder(string parentAssetFolder)
{
if (!AssetDatabase.IsValidFolder(parentAssetFolder)) parentAssetFolder = Def_UXGUIPath.UIResRootPath;
string baseName = "NewFolder";
string newName = baseName;
int i = 1;
while (AssetDatabase.IsValidFolder(PathCombine(parentAssetFolder, newName)))
{
newName = baseName + " " + i;
i++;
}
AssetDatabase.CreateFolder(parentAssetFolder, newName);
AssetDatabase.Refresh();
RefreshTree();
}
private void CreatePrefabUnder(string parentAssetFolder)
{
if (!AssetDatabase.IsValidFolder(parentAssetFolder)) parentAssetFolder = Def_UXGUIPath.UIResRootPath;
UXComponentCreateWindowHelper.ShowWindow(parentAssetFolder, RefreshTree);
}
private static string PathCombine(string a, string b)
{
if (a.EndsWith("/")) a = a.Substring(0, a.Length - 1);
return a + "/" + b;
}
private string GetEditorPrefKey()
{
return EditorPrefKeyPrefix + Def_UXGUIPath.UIResRootPath;
}
private void SaveExpandedState()
{
try
{
var list = new List<string>(expandedPaths);
var joined = string.Join("|", list);
EditorPrefs.SetString(GetEditorPrefKey(), joined);
}
catch (Exception e)
{
Debug.LogWarning("Failed to save expanded state: " + e.Message);
}
}
private void LoadExpandedState()
{
expandedPaths.Clear();
try
{
var joined = EditorPrefs.GetString(GetEditorPrefKey(), "");
if (!string.IsNullOrEmpty(joined))
{
var parts = joined.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var p in parts) expandedPaths.Add(p);
}
}
catch (Exception e)
{
Debug.LogWarning("Failed to load expanded state: " + e.Message);
}
}
private void OnKeyDown(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.Delete || evt.keyCode == KeyCode.Backspace)
{
DeleteSelectionWithConfirm();
evt.StopPropagation();
return;
}
if (evt.keyCode == KeyCode.F2)
{
if (selectedPaths.Count == 1)
{
var path = selectedPaths.First();
StartRename(path);
evt.StopPropagation();
}
}
}
private Texture texture;
private void UpdatePreviewTexture()
{
if (selectedPaths.Count == 0 || selectedPaths.Count > 1 || (selectedPaths.Count > 0 && !selectedPaths.First().Contains(".prefab")))
{
this.texture = null;
previewIcon.image = null;
previewLabel.text = string.Empty;
return;
}
string previewPath = selectedPaths.First();
string guid = AssetDatabase.AssetPathToGUID(previewPath);
texture = Utils.GetAssetsPreviewTexture(guid, 1024);
previewLabel.text = Path.GetFileNameWithoutExtension(previewPath) + " (preview)";
previewIcon.image = texture;
}
private void StartDragAssets(List<string> assetPaths)
{
var objs = new List<UnityEngine.Object>();
foreach (var p in assetPaths)
{
var o = AssetDatabase.LoadMainAssetAtPath(p);
if (o != null) objs.Add(o);
}
DragAndDrop.PrepareStartDrag();
DragAndDrop.paths = assetPaths.ToArray();
DragAndDrop.objectReferences = objs.ToArray();
DragAndDrop.StartDrag("Move Assets");
}
/// <summary>
/// Called by DragEnter / DragUpdated - determine destination and update visuals.
/// </summary>
private void OnDragEnterOrUpdate(VisualElement evtTarget)
{
var destFolder = GetDropDestinationFromVisualTarget(evtTarget);
bool valid = IsDropValid(DragAndDrop.paths, destFolder);
DragAndDrop.visualMode = valid ? DragAndDropVisualMode.Move : DragAndDropVisualMode.Rejected;
UpdateDragTargetVisual(destFolder, valid);
}
private void OnDragPerform(DragPerformEvent evt)
{
var destFolder = GetDropDestinationFromVisualTarget(evt.target as VisualElement);
bool valid = IsDropValid(DragAndDrop.paths, destFolder);
if (valid && DragAndDrop.paths != null && DragAndDrop.paths.Length > 0)
{
MoveAssetsToFolder(DragAndDrop.paths, destFolder);
DragAndDrop.AcceptDrag();
}
else
{
// do nothing (either invalid or all no-op)
// Optional: 显示提示
// Debug.Log("Drop rejected or no-op.");
}
ClearDragTarget();
}
private string GetDropDestinationFromVisualTarget(VisualElement t)
{
while (t != null)
{
if (t.userData is string dataPath)
{
// if it's a folder path, return directly
if (AssetDatabase.IsValidFolder(dataPath))
{
return dataPath;
}
else
{
// userData is an asset (prefab) -> return its parent folder
var parent = Path.GetDirectoryName(dataPath);
if (string.IsNullOrEmpty(parent)) return Def_UXGUIPath.UIResRootPath;
return parent.Replace("\\", "/");
}
}
t = t.parent as VisualElement;
}
// no path ancestor -> drop to root
return Def_UXGUIPath.UIResRootPath;
}
/// <summary>
/// Validate drop: prevent moving folder into itself or into its child.
/// Also reject if **all** source items would be no-ops (i.e. already in destFolder).
/// </summary>
private bool IsDropValid(string[] srcPaths, string destFolder)
{
if (srcPaths == null || srcPaths.Length == 0) return false;
if (!AssetDatabase.IsValidFolder(destFolder)) destFolder = Def_UXGUIPath.UIResRootPath;
bool anyValidMove = false;
foreach (var src in srcPaths)
{
if (string.IsNullOrEmpty(src)) continue;
if (AssetDatabase.IsValidFolder(src))
{
// cannot move folder into itself or its descendants
if (destFolder.Equals(src, StringComparison.InvariantCultureIgnoreCase)) continue;
if (destFolder.StartsWith(src + "/", StringComparison.InvariantCultureIgnoreCase)) continue; // into its child -> invalid
// if the folder's parent already equals destFolder => that item would be no-op
var parent = Path.GetDirectoryName(src);
if (!string.IsNullOrEmpty(parent)) parent = parent.Replace("\\", "/");
if (string.Equals(parent, destFolder, StringComparison.InvariantCultureIgnoreCase))
{
// this src would be no-op, continue checking others
continue;
}
// otherwise it's a valid move
anyValidMove = true;
}
else
{
// file/prefab: parent folder
var parent = Path.GetDirectoryName(src);
if (!string.IsNullOrEmpty(parent)) parent = parent.Replace("\\", "/");
if (string.Equals(parent, destFolder, StringComparison.InvariantCultureIgnoreCase))
{
// would be no-op
continue;
}
anyValidMove = true;
}
}
return anyValidMove;
}
/// <summary>
/// Highlight target element and display drop hint in preview label.
/// </summary>
private void UpdateDragTargetVisual(string destFolder, bool valid)
{
// if same as current, only update visual text
if (!string.IsNullOrEmpty(currentDragTargetPath) && string.Equals(currentDragTargetPath, destFolder, StringComparison.InvariantCultureIgnoreCase))
{
previewLabel.text = valid ? $"Drop target: {destFolder}" : $"Invalid drop: {destFolder}";
return;
}
// clear previous
ClearDragTargetHighlight();
currentDragTargetPath = destFolder;
previewLabel.text = valid ? $"Drop target: {destFolder}" : $"Invalid drop or no-op: {destFolder}";
var el = FindElementByPathInTree(destFolder);
if (el != null)
{
currentDragTargetElement = el;
el.style.backgroundColor = new StyleColor(new Color(0.1f, 0.8f, 0.1f, valid ? 0.2f : 0.12f));
}
else
{
currentDragTargetElement = treeScroll;
treeScroll.style.backgroundColor = new StyleColor(new Color(0.1f, 0.8f, 0.1f, valid ? 0.08f : 0.05f));
}
}
private void ClearDragTarget()
{
currentDragTargetPath = null;
previewLabel.text = string.Empty;
ClearDragTargetHighlight();
}
private void ClearDragTargetHighlight()
{
if (currentDragTargetElement != null)
{
currentDragTargetElement.style.backgroundColor = StyleKeyword.Null;
currentDragTargetElement = null;
}
treeScroll.style.backgroundColor = StyleKeyword.Null;
}
/// <summary>
/// Recursively search treeScroll children for VisualElement whose userData equals path.
/// Returns that element (the header element we set userData on), or null.
/// </summary>
private VisualElement FindElementByPathInTree(string path)
{
if (string.IsNullOrEmpty(path)) return null;
VisualElement found = null;
void Search(VisualElement parent)
{
if (found != null) return;
foreach (var c in parent.Children())
{
if (c.userData is string s && string.Equals(s, path, StringComparison.InvariantCultureIgnoreCase))
{
found = c;
return;
}
Search(c);
if (found != null) return;
}
}
Search(treeScroll);
return found;
}
private void MoveAssetsToFolder(string[] paths, string destFolder)
{
if (paths == null || paths.Length == 0) return;
if (!AssetDatabase.IsValidFolder(destFolder)) destFolder = Def_UXGUIPath.UIResRootPath;
var newSelection = new List<string>();
foreach (var src in paths)
{
if (string.IsNullOrEmpty(src)) continue;
if (AssetDatabase.IsValidFolder(src))
{
if (destFolder.Equals(src, StringComparison.InvariantCultureIgnoreCase)) continue;
if (destFolder.StartsWith(src + "/", StringComparison.InvariantCultureIgnoreCase)) continue; // cannot move into its own child
// if parent already equals destFolder -> skip (no-op)
var parent = Path.GetDirectoryName(src);
if (!string.IsNullOrEmpty(parent)) parent = parent.Replace("\\", "/");
if (string.Equals(parent, destFolder, StringComparison.InvariantCultureIgnoreCase)) continue;
}
else
{
// file/prefab: skip if already in destFolder
var parent = Path.GetDirectoryName(src);
if (!string.IsNullOrEmpty(parent)) parent = parent.Replace("\\", "/");
if (string.Equals(parent, destFolder, StringComparison.InvariantCultureIgnoreCase)) continue;
}
var fileName = Path.GetFileName(src);
var destPath = PathCombine(destFolder, fileName);
destPath = AssetDatabase.GenerateUniqueAssetPath(destPath);
var err = AssetDatabase.MoveAsset(src, destPath);
if (!string.IsNullOrEmpty(err))
{
Debug.LogWarning($"Failed to move {src} -> {destPath}: {err}");
}
else
{
newSelection.Add(destPath);
}
}
AssetDatabase.Refresh();
if (newSelection.Count > 0)
{
selectedPaths.Clear();
foreach (var s in newSelection) selectedPaths.Add(s);
}
RefreshTree();
}
// ----- Renaming support (F2) -----
private void StartRename(string assetPath)
{
if (string.IsNullOrEmpty(assetPath)) return;
var el = FindElementByPathInTree(assetPath);
if (el == null) return;
// find first Label child to replace
Label nameLabel = null;
foreach (var c in el.Children())
{
if (c is Label l)
{
nameLabel = l;
break;
}
}
if (nameLabel == null) return;
// current base name and extension for files
string currentBase;
string extension = "";
if (AssetDatabase.IsValidFolder(assetPath))
{
currentBase = Path.GetFileName(assetPath);
}
else
{
extension = Path.GetExtension(assetPath);
currentBase = Path.GetFileNameWithoutExtension(assetPath);
}
// create TextField and insert at same position
int index = 0;
foreach (var c in el.Children())
{
if (c == nameLabel) break;
index++;
}
var tf = new TextField();
tf.value = currentBase;
tf.style.flexGrow = 1;
tf.RegisterCallback<KeyDownEvent>(ke =>
{
if (ke.keyCode == KeyCode.Return || ke.keyCode == KeyCode.KeypadEnter)
{
CommitRename(assetPath, tf.value, extension);
ke.StopPropagation();
}
else if (ke.keyCode == KeyCode.Escape)
{
CancelRename();
ke.StopPropagation();
}
});
tf.RegisterCallback<FocusOutEvent>(fe =>
{
// commit on lose focus
CommitRename(assetPath, tf.value, extension);
});
el.Insert(index, tf);
nameLabel.style.display = DisplayStyle.None;
// focus and select text
tf.Focus();
tf.SelectAll();
}
private void CancelRename()
{
// easiest: just refresh tree to restore UI
RefreshTree();
}
private void CommitRename(string originalPath, string newBaseName, string extension)
{
if (string.IsNullOrWhiteSpace(newBaseName))
{
// cancel rename if empty
RefreshTree();
return;
}
string parent = Path.GetDirectoryName(originalPath);
if (!string.IsNullOrEmpty(parent)) parent = parent.Replace("\\", "/");
if (AssetDatabase.IsValidFolder(originalPath))
{
// rename folder (newBaseName is folder name)
var err = AssetDatabase.RenameAsset(originalPath, newBaseName);
if (!string.IsNullOrEmpty(err))
{
Debug.LogWarning("Rename failed: " + err);
}
else
{
AssetDatabase.Refresh();
var newFolderPath = PathCombine(parent, newBaseName);
selectedPaths.Clear();
selectedPaths.Add(newFolderPath);
RefreshTree();
}
}
else
{
// file/prefab: append extension if missing
string ext = extension;
if (string.IsNullOrEmpty(ext)) ext = Path.GetExtension(originalPath);
var newName = newBaseName + ext;
var err = AssetDatabase.RenameAsset(originalPath, newName);
if (!string.IsNullOrEmpty(err))
{
Debug.LogWarning("Rename failed: " + err);
}
else
{
AssetDatabase.Refresh();
var newAssetPath = PathCombine(parent, newName);
selectedPaths.Clear();
selectedPaths.Add(newAssetPath);
RefreshTree();
}
}
}
}