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 = "名称收集器"; 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 _sortedEntries = new List(); private readonly Dictionary> _entriesByName = new Dictionary>(StringComparer.OrdinalIgnoreCase); private readonly HashSet _conflictNames = new HashSet(StringComparer.OrdinalIgnoreCase); private HandlerNameCacheEntry _selectedEntry; [MenuItem(AibisEditorMenus.NameCollector)] public static void ShowWindow() { var window = GetWindow(WindowTitle); window.minSize = new Vector2(700, 400); window.Show(); } private void OnEnable() { LoadCacheIfNeeded(); RebuildIndexes(); } private void LoadCacheIfNeeded() { _cache ??= HandlerNameCache.Load(); _cache.entries ??= new List(); } 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(); _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(sceneGuids); foreach (var g in prefabGuids) allGuids.Add(g); var guidList = new List(allGuids); guidList.Sort(); var existingHashes = new Dictionary(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; } } }