Files
aibis-dream/Assets/Editor/HandlerNameCollector/HandlerNameCollectorWindow.cs
T

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;
}
}
}