598 lines
20 KiB
C#
598 lines
20 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using UnityEditor;
|
|
using UnityEditor.SceneManagement;
|
|
using UnityEngine;
|
|
|
|
namespace AibisDream.EditorTools
|
|
{
|
|
public class HandlerNameCollectorWindow : EditorWindow
|
|
{
|
|
private const string WindowTitle = "Handler Name Collector";
|
|
|
|
private HandlerNameCache _cache;
|
|
|
|
private Vector2 _scrollPos;
|
|
private string _searchText = string.Empty;
|
|
private bool _showScenes = true;
|
|
private bool _showPrefabs = true;
|
|
private bool _showConflictsOnly;
|
|
|
|
private HandlerEntryKind _viewKind = HandlerEntryKind.DirectorHandler;
|
|
|
|
private readonly List<HandlerNameCacheEntry> _sortedEntries = new List<HandlerNameCacheEntry>();
|
|
private readonly Dictionary<string, List<HandlerNameCacheEntry>> _entriesByName =
|
|
new Dictionary<string, List<HandlerNameCacheEntry>>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
private readonly HashSet<string> _conflictNames =
|
|
new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
private HandlerNameCacheEntry _selectedEntry;
|
|
|
|
[MenuItem("Tools/Handler Name Collector")]
|
|
public static void ShowWindow()
|
|
{
|
|
var window = GetWindow<HandlerNameCollectorWindow>(WindowTitle);
|
|
window.minSize = new Vector2(700, 400);
|
|
window.Show();
|
|
}
|
|
|
|
[MenuItem("Tools/Timeline Name Collector", false, 101)]
|
|
public static void ShowWindowLegacyMenu()
|
|
{
|
|
ShowWindow();
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
LoadCacheIfNeeded();
|
|
RebuildIndexes();
|
|
}
|
|
|
|
private void LoadCacheIfNeeded()
|
|
{
|
|
_cache ??= HandlerNameCache.Load();
|
|
_cache.entries ??= new List<HandlerNameCacheEntry>();
|
|
}
|
|
|
|
private void RebuildIndexes()
|
|
{
|
|
_sortedEntries.Clear();
|
|
_entriesByName.Clear();
|
|
_conflictNames.Clear();
|
|
|
|
if (_cache == null || _cache.entries == null)
|
|
return;
|
|
|
|
foreach (var entry in _cache.entries)
|
|
{
|
|
if (entry == null || entry.kind != _viewKind)
|
|
continue;
|
|
|
|
_sortedEntries.Add(entry);
|
|
|
|
if (!_entriesByName.TryGetValue(entry.registeredName, out var list))
|
|
{
|
|
list = new List<HandlerNameCacheEntry>();
|
|
_entriesByName[entry.registeredName] = list;
|
|
}
|
|
|
|
list.Add(entry);
|
|
}
|
|
|
|
_sortedEntries.Sort((a, b) =>
|
|
{
|
|
var nameCompare =
|
|
string.Compare(a.registeredName, b.registeredName, StringComparison.OrdinalIgnoreCase);
|
|
if (nameCompare != 0)
|
|
return nameCompare;
|
|
var typeCompare = a.isInScene.CompareTo(b.isInScene);
|
|
if (typeCompare != 0)
|
|
return -typeCompare;
|
|
return string.Compare(a.assetPath, b.assetPath, StringComparison.OrdinalIgnoreCase);
|
|
});
|
|
|
|
foreach (var kv in _entriesByName)
|
|
{
|
|
if (kv.Value.Count > 1)
|
|
_conflictNames.Add(kv.Key);
|
|
}
|
|
|
|
if (_selectedEntry != null && _selectedEntry.kind != _viewKind)
|
|
_selectedEntry = null;
|
|
}
|
|
|
|
private void OnGUI()
|
|
{
|
|
LoadCacheIfNeeded();
|
|
|
|
DrawToolbar();
|
|
EditorGUILayout.Space();
|
|
DrawViewKindBar();
|
|
EditorGUILayout.Space();
|
|
DrawFilterBar();
|
|
EditorGUILayout.Space();
|
|
DrawSummary();
|
|
EditorGUILayout.Space();
|
|
DrawListArea();
|
|
EditorGUILayout.Space();
|
|
DrawDetailArea();
|
|
}
|
|
|
|
private void DrawToolbar()
|
|
{
|
|
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
|
|
|
|
if (GUILayout.Button("刷新选中", EditorStyles.toolbarButton, GUILayout.Width(90)))
|
|
RefreshSelected();
|
|
|
|
if (GUILayout.Button("刷新全部", EditorStyles.toolbarButton, GUILayout.Width(90)))
|
|
RefreshAll(forceFull: false);
|
|
|
|
if (GUILayout.Button("强制全量", EditorStyles.toolbarButton, GUILayout.Width(90)))
|
|
RefreshAll(forceFull: true);
|
|
|
|
GUILayout.FlexibleSpace();
|
|
|
|
var lastFullScanLabel = string.IsNullOrEmpty(_cache.lastFullScanTime)
|
|
? "未进行全量扫描"
|
|
: $"上次全量: {FormatTime(_cache.lastFullScanTime)}";
|
|
GUILayout.Label(lastFullScanLabel, EditorStyles.miniLabel);
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
}
|
|
|
|
private void DrawViewKindBar()
|
|
{
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
GUILayout.Label("视图:", GUILayout.Width(40));
|
|
var labels = new[] { "DirectorHandler", "AnimatorHandler" };
|
|
var newIndex = GUILayout.Toolbar(
|
|
_viewKind == HandlerEntryKind.DirectorHandler ? 0 : 1,
|
|
labels,
|
|
GUILayout.Height(22));
|
|
|
|
var newKind = newIndex == 0 ? HandlerEntryKind.DirectorHandler : HandlerEntryKind.AnimatorHandler;
|
|
if (newKind != _viewKind)
|
|
{
|
|
_viewKind = newKind;
|
|
_selectedEntry = null;
|
|
RebuildIndexes();
|
|
}
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
}
|
|
|
|
private void DrawFilterBar()
|
|
{
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
GUILayout.Label("搜索:", GUILayout.Width(40));
|
|
_searchText = EditorGUILayout.TextField(_searchText);
|
|
|
|
_showScenes = GUILayout.Toggle(_showScenes, "场景", GUILayout.Width(60));
|
|
_showPrefabs = GUILayout.Toggle(_showPrefabs, "预制体", GUILayout.Width(70));
|
|
_showConflictsOnly = GUILayout.Toggle(_showConflictsOnly, "只看冲突", GUILayout.Width(80));
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
}
|
|
|
|
private void DrawSummary()
|
|
{
|
|
var total = CountVisibleEntries();
|
|
var conflictCount = _conflictNames.Count;
|
|
var modeLabel = _viewKind == HandlerEntryKind.DirectorHandler ? "DirectorHandler" : "AnimatorHandler";
|
|
EditorGUILayout.LabelField(
|
|
$"[{modeLabel}] 找到 {total} 个注册名条目({conflictCount} 个重名)",
|
|
EditorStyles.boldLabel);
|
|
}
|
|
|
|
private int CountVisibleEntries()
|
|
{
|
|
var n = 0;
|
|
foreach (var entry in _sortedEntries)
|
|
{
|
|
if (PassFilter(entry))
|
|
n++;
|
|
}
|
|
|
|
return n;
|
|
}
|
|
|
|
private void DrawListArea()
|
|
{
|
|
EditorGUILayout.LabelField("列表", EditorStyles.boldLabel);
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
GUILayout.Label("注册名", GUILayout.Width(220));
|
|
GUILayout.Label("类型", GUILayout.Width(50));
|
|
GUILayout.Label("位置", GUILayout.ExpandWidth(true));
|
|
GUILayout.Label("", GUILayout.Width(90));
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
var visibleCount = CountVisibleEntries();
|
|
var rect = GUILayoutUtility.GetRect(0, 100000, 0, 100000);
|
|
_scrollPos = GUI.BeginScrollView(rect, _scrollPos,
|
|
new Rect(0, 0, rect.width - 20, visibleCount * 20 + 10));
|
|
|
|
var y = 0f;
|
|
var rowHeight = 20f;
|
|
var viewWidth = rect.width - 20;
|
|
|
|
foreach (var entry in _sortedEntries)
|
|
{
|
|
if (!PassFilter(entry))
|
|
continue;
|
|
|
|
var isConflict = _conflictNames.Contains(entry.registeredName);
|
|
var rowRect = new Rect(0, y, viewWidth, rowHeight);
|
|
DrawRow(rowRect, entry, isConflict);
|
|
y += rowHeight;
|
|
}
|
|
|
|
GUI.EndScrollView();
|
|
}
|
|
|
|
private void DrawRow(Rect rect, HandlerNameCacheEntry entry, bool isConflict)
|
|
{
|
|
var typeLabel = entry.isInScene ? "场景" : "预制体";
|
|
var location = GetShortLocation(entry);
|
|
|
|
var bgColor = GUI.backgroundColor;
|
|
if (isConflict)
|
|
GUI.backgroundColor = new Color(1f, 0.9f, 0.9f);
|
|
|
|
if (Event.current.type == EventType.MouseDown && rect.Contains(Event.current.mousePosition))
|
|
{
|
|
if (Event.current.button == 0)
|
|
{
|
|
_selectedEntry = entry;
|
|
Repaint();
|
|
Event.current.Use();
|
|
}
|
|
}
|
|
|
|
GUI.Box(rect, GUIContent.none, EditorStyles.helpBox);
|
|
|
|
GUI.backgroundColor = bgColor;
|
|
|
|
var colRect = rect;
|
|
colRect.x += 4;
|
|
colRect.width = 220;
|
|
GUI.Label(colRect, entry.registeredName);
|
|
|
|
colRect.x += colRect.width + 4;
|
|
colRect.width = 50;
|
|
GUI.Label(colRect, typeLabel);
|
|
|
|
colRect.x += colRect.width + 4;
|
|
colRect.width = rect.width - colRect.x - 100;
|
|
GUI.Label(colRect, location);
|
|
|
|
colRect.x += colRect.width + 4;
|
|
colRect.width = 90;
|
|
if (GUI.Button(colRect, "项目中显示"))
|
|
PingEntry(entry);
|
|
}
|
|
|
|
private string RegisteredNameLabel()
|
|
{
|
|
return _viewKind == HandlerEntryKind.DirectorHandler ? "Timeline 注册名" : "Animator 注册名";
|
|
}
|
|
|
|
private void DrawDetailArea()
|
|
{
|
|
EditorGUILayout.LabelField("选中项详情", EditorStyles.boldLabel);
|
|
|
|
if (_selectedEntry == null)
|
|
{
|
|
EditorGUILayout.HelpBox("在上方列表中点击某一行查看详情。", MessageType.Info);
|
|
return;
|
|
}
|
|
|
|
GUILayout.Label($"{RegisteredNameLabel()}: {_selectedEntry.registeredName}", EditorStyles.boldLabel);
|
|
|
|
if (_entriesByName.TryGetValue(_selectedEntry.registeredName, out var group))
|
|
{
|
|
foreach (var e in group)
|
|
{
|
|
EditorGUILayout.BeginVertical("box");
|
|
|
|
EditorGUILayout.LabelField("资源路径", e.assetPath);
|
|
EditorGUILayout.LabelField("GameObject",
|
|
string.IsNullOrEmpty(e.gameObjectPath) ? "(未知)" : e.gameObjectPath);
|
|
EditorGUILayout.LabelField("类型", e.isInScene ? "场景" : "预制体");
|
|
EditorGUILayout.LabelField("行号", e.lineNumber.ToString());
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
if (GUILayout.Button("在Project窗口显示", GUILayout.Width(140)))
|
|
PingEntry(e);
|
|
|
|
if (e.isInScene && GUILayout.Button("打开场景并选中", GUILayout.Width(140)))
|
|
OpenSceneAndSelect(e);
|
|
|
|
if (GUILayout.Button("复制名称", GUILayout.Width(100)))
|
|
EditorGUIUtility.systemCopyBuffer = e.registeredName;
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
EditorGUILayout.EndVertical();
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool PassFilter(HandlerNameCacheEntry entry)
|
|
{
|
|
if (!_showScenes && entry.isInScene)
|
|
return false;
|
|
if (!_showPrefabs && !entry.isInScene)
|
|
return false;
|
|
|
|
if (_showConflictsOnly && !_conflictNames.Contains(entry.registeredName))
|
|
return false;
|
|
|
|
if (string.IsNullOrEmpty(_searchText))
|
|
return true;
|
|
|
|
var s = _searchText.Trim();
|
|
if (s.Length == 0)
|
|
return true;
|
|
|
|
var cmp = StringComparison.OrdinalIgnoreCase;
|
|
|
|
if (!string.IsNullOrEmpty(entry.registeredName) &&
|
|
entry.registeredName.Contains(s, cmp))
|
|
return true;
|
|
|
|
if (!string.IsNullOrEmpty(entry.assetPath) &&
|
|
entry.assetPath.Contains(s, cmp))
|
|
return true;
|
|
|
|
if (!string.IsNullOrEmpty(entry.gameObjectPath) &&
|
|
entry.gameObjectPath.Contains(s, cmp))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
private static string GetShortLocation(HandlerNameCacheEntry entry)
|
|
{
|
|
if (string.IsNullOrEmpty(entry.assetPath))
|
|
return string.Empty;
|
|
|
|
var fileName = Path.GetFileNameWithoutExtension(entry.assetPath);
|
|
|
|
if (string.IsNullOrEmpty(entry.gameObjectPath))
|
|
return fileName;
|
|
|
|
return $"{fileName} / {entry.gameObjectPath}";
|
|
}
|
|
|
|
private static void PingEntry(HandlerNameCacheEntry entry)
|
|
{
|
|
if (string.IsNullOrEmpty(entry.assetPath))
|
|
return;
|
|
|
|
var obj = AssetDatabase.LoadMainAssetAtPath(entry.assetPath);
|
|
if (obj != null)
|
|
{
|
|
EditorGUIUtility.PingObject(obj);
|
|
Selection.activeObject = obj;
|
|
}
|
|
}
|
|
|
|
private static void OpenSceneAndSelect(HandlerNameCacheEntry entry)
|
|
{
|
|
if (string.IsNullOrEmpty(entry.assetPath))
|
|
return;
|
|
|
|
if (!entry.isInScene)
|
|
return;
|
|
|
|
if (!File.Exists(entry.assetPath))
|
|
{
|
|
Debug.LogWarning($"场景文件不存在: {entry.assetPath}");
|
|
return;
|
|
}
|
|
|
|
var scene = EditorSceneManager.OpenScene(entry.assetPath, OpenSceneMode.Single);
|
|
if (!scene.IsValid())
|
|
{
|
|
Debug.LogWarning($"无法打开场景: {entry.assetPath}");
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(entry.gameObjectPath))
|
|
return;
|
|
|
|
var parts = entry.gameObjectPath.Split('/');
|
|
GameObject current = null;
|
|
|
|
foreach (var part in parts)
|
|
{
|
|
if (current == null)
|
|
{
|
|
var roots = scene.GetRootGameObjects();
|
|
current = Array.Find(roots, go => go.name == part);
|
|
}
|
|
else
|
|
{
|
|
var child = current.transform.Find(part);
|
|
current = child != null ? child.gameObject : null;
|
|
}
|
|
|
|
if (current == null)
|
|
break;
|
|
}
|
|
|
|
if (current != null)
|
|
{
|
|
Selection.activeGameObject = current;
|
|
EditorGUIUtility.PingObject(current);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"在场景 {scene.name} 中未找到路径: {entry.gameObjectPath}");
|
|
}
|
|
}
|
|
|
|
private void RefreshSelected()
|
|
{
|
|
LoadCacheIfNeeded();
|
|
|
|
var guids = Selection.assetGUIDs;
|
|
if (guids == null || guids.Length == 0)
|
|
{
|
|
EditorUtility.DisplayDialog(WindowTitle, "请在 Project 窗口中先选中场景或预制体资源。", "确定");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
EditorUtility.DisplayProgressBar(WindowTitle, "正在刷新选中资源...", 0f);
|
|
|
|
var processed = 0;
|
|
foreach (var guid in guids)
|
|
{
|
|
var assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
|
if (!IsSupportedAsset(assetPath))
|
|
continue;
|
|
|
|
ProcessSingleAsset(guid, assetPath);
|
|
|
|
processed++;
|
|
EditorUtility.DisplayProgressBar(WindowTitle, $"正在解析: {assetPath}",
|
|
processed / (float)guids.Length);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
EditorUtility.ClearProgressBar();
|
|
}
|
|
|
|
_cache.CleanupDeletedAssets();
|
|
_cache.Save();
|
|
RebuildIndexes();
|
|
Repaint();
|
|
}
|
|
|
|
private void RefreshAll(bool forceFull)
|
|
{
|
|
LoadCacheIfNeeded();
|
|
|
|
var sceneGuids = AssetDatabase.FindAssets("t:Scene");
|
|
var prefabGuids = AssetDatabase.FindAssets("t:Prefab");
|
|
|
|
var allGuids = new HashSet<string>(sceneGuids);
|
|
foreach (var g in prefabGuids)
|
|
allGuids.Add(g);
|
|
|
|
var guidList = new List<string>(allGuids);
|
|
guidList.Sort();
|
|
|
|
var existingHashes = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var e in _cache.entries)
|
|
{
|
|
if (e == null || string.IsNullOrEmpty(e.assetPath) || string.IsNullOrEmpty(e.fileHash))
|
|
continue;
|
|
|
|
if (!existingHashes.ContainsKey(e.assetPath))
|
|
existingHashes[e.assetPath] = e.fileHash;
|
|
}
|
|
|
|
try
|
|
{
|
|
EditorUtility.DisplayProgressBar(WindowTitle,
|
|
forceFull ? "正在强制全量扫描..." : "正在增量扫描...", 0f);
|
|
|
|
for (var i = 0; i < guidList.Count; i++)
|
|
{
|
|
var guid = guidList[i];
|
|
var assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
|
if (!IsSupportedAsset(assetPath))
|
|
continue;
|
|
|
|
var fullPath = HandlerNameCache.GetAssetFullPath(assetPath);
|
|
if (string.IsNullOrEmpty(fullPath) || !File.Exists(fullPath))
|
|
continue;
|
|
|
|
var newHash = HandlerNameCache.ComputeFileHash(fullPath);
|
|
|
|
var needScan = forceFull;
|
|
if (!needScan)
|
|
{
|
|
if (!existingHashes.TryGetValue(assetPath, out var oldHash) ||
|
|
string.IsNullOrEmpty(newHash) ||
|
|
!string.Equals(oldHash, newHash, StringComparison.OrdinalIgnoreCase))
|
|
needScan = true;
|
|
}
|
|
|
|
if (!needScan)
|
|
continue;
|
|
|
|
ProcessSingleAsset(guid, assetPath, newHash);
|
|
|
|
var progress = (i + 1) / (float)guidList.Count;
|
|
EditorUtility.DisplayProgressBar(WindowTitle, $"正在解析: {assetPath}", progress);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
EditorUtility.ClearProgressBar();
|
|
}
|
|
|
|
_cache.CleanupDeletedAssets();
|
|
_cache.SetLastFullScanNow();
|
|
_cache.Save();
|
|
RebuildIndexes();
|
|
Repaint();
|
|
}
|
|
|
|
private void ProcessSingleAsset(string guid, string assetPath, string precomputedHash = null)
|
|
{
|
|
var fullPath = HandlerNameCache.GetAssetFullPath(assetPath);
|
|
if (string.IsNullOrEmpty(fullPath) || !File.Exists(fullPath))
|
|
return;
|
|
|
|
var fileHash = precomputedHash ?? HandlerNameCache.ComputeFileHash(fullPath);
|
|
var lastModifiedUtc = File.GetLastWriteTimeUtc(fullPath).ToString("o");
|
|
|
|
_cache.RemoveEntriesForAsset(assetPath);
|
|
|
|
var entries = YamlHandlerNameParser.ParseAsset(
|
|
assetPath,
|
|
guid,
|
|
fileHash,
|
|
lastModifiedUtc);
|
|
|
|
if (entries != null && entries.Count > 0)
|
|
_cache.entries.AddRange(entries);
|
|
}
|
|
|
|
private static bool IsSupportedAsset(string assetPath)
|
|
{
|
|
if (string.IsNullOrEmpty(assetPath))
|
|
return false;
|
|
|
|
var ext = Path.GetExtension(assetPath);
|
|
if (string.IsNullOrEmpty(ext))
|
|
return false;
|
|
|
|
ext = ext.ToLowerInvariant();
|
|
return ext == ".unity" || ext == ".prefab";
|
|
}
|
|
|
|
private static string FormatTime(string isoTime)
|
|
{
|
|
if (string.IsNullOrEmpty(isoTime))
|
|
return "-";
|
|
|
|
if (DateTime.TryParse(isoTime, out var dt))
|
|
return dt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
|
|
|
|
return isoTime;
|
|
}
|
|
}
|
|
}
|