1245 lines
42 KiB
C#
1245 lines
42 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Aliciza.UXTool;
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|