Files
aibis-dream/Assets/Editor/TimelineNameCollector/TimelineNameCollectorWindow.cs
T

568 lines
19 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
namespace AibisDream.EditorTools
{
public class TimelineNameCollectorWindow : EditorWindow
{
private TimelineNameCache _cache;
private Vector2 _scrollPos;
private string _searchText = string.Empty;
private bool _showScenes = true;
private bool _showPrefabs = true;
private bool _showConflictsOnly;
private readonly List<TimelineNameCacheEntry> _sortedEntries = new List<TimelineNameCacheEntry>();
private readonly Dictionary<string, List<TimelineNameCacheEntry>> _entriesByName =
new Dictionary<string, List<TimelineNameCacheEntry>>(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _conflictNames =
new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private TimelineNameCacheEntry _selectedEntry;
[MenuItem("Tools/Timeline Name Collector")]
public static void ShowWindow()
{
var window = GetWindow<TimelineNameCollectorWindow>("Timeline Name Collector");
window.minSize = new Vector2(700, 400);
window.Show();
}
private void OnEnable()
{
LoadCacheIfNeeded();
RebuildIndexes();
}
private void LoadCacheIfNeeded()
{
_cache ??= TimelineNameCache.Load();
_cache.entries ??= new List<TimelineNameCacheEntry>();
}
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)
continue;
_sortedEntries.Add(entry);
if (!_entriesByName.TryGetValue(entry.timelineName, out var list))
{
list = new List<TimelineNameCacheEntry>();
_entriesByName[entry.timelineName] = list;
}
list.Add(entry);
}
_sortedEntries.Sort((a, b) =>
{
var nameCompare = string.Compare(a.timelineName, b.timelineName, 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);
}
}
}
private void OnGUI()
{
LoadCacheIfNeeded();
DrawToolbar();
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 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 = _sortedEntries.Count;
var conflictCount = _conflictNames.Count;
EditorGUILayout.LabelField($"找到 {total} 个 TimelineName 条目({conflictCount} 个重名)",
EditorStyles.boldLabel);
}
private void DrawListArea()
{
EditorGUILayout.LabelField("列表", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
GUILayout.Label("Timeline Name", GUILayout.Width(220));
GUILayout.Label("类型", GUILayout.Width(50));
GUILayout.Label("位置", GUILayout.ExpandWidth(true));
GUILayout.Label("", GUILayout.Width(90));
EditorGUILayout.EndHorizontal();
var rect = GUILayoutUtility.GetRect(0, 100000, 0, 100000);
_scrollPos = GUI.BeginScrollView(rect, _scrollPos, new Rect(0, 0, rect.width - 20, _sortedEntries.Count * 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.timelineName);
var rowRect = new Rect(0, y, viewWidth, rowHeight);
DrawRow(rowRect, entry, isConflict);
y += rowHeight;
}
GUI.EndScrollView();
}
private void DrawRow(Rect rect, TimelineNameCacheEntry 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.timelineName);
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 void DrawDetailArea()
{
EditorGUILayout.LabelField("选中项详情", EditorStyles.boldLabel);
if (_selectedEntry == null)
{
EditorGUILayout.HelpBox("在上方列表中点击某一行查看详情。", MessageType.Info);
return;
}
GUILayout.Label($"Timeline Name: {_selectedEntry.timelineName}", EditorStyles.boldLabel);
if (_entriesByName.TryGetValue(_selectedEntry.timelineName, 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.timelineName;
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
}
}
private bool PassFilter(TimelineNameCacheEntry entry)
{
if (!_showScenes && entry.isInScene)
return false;
if (!_showPrefabs && !entry.isInScene)
return false;
if (_showConflictsOnly && !_conflictNames.Contains(entry.timelineName))
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.timelineName) &&
entry.timelineName.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(TimelineNameCacheEntry 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(TimelineNameCacheEntry 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(TimelineNameCacheEntry 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("Timeline Name Collector", "请在 Project 窗口中先选中场景或预制体资源。", "确定");
return;
}
try
{
EditorUtility.DisplayProgressBar("Timeline Name Collector", "正在刷新选中资源...", 0f);
var processed = 0;
foreach (var guid in guids)
{
var assetPath = AssetDatabase.GUIDToAssetPath(guid);
if (!IsSupportedAsset(assetPath))
continue;
ProcessSingleAsset(guid, assetPath);
processed++;
EditorUtility.DisplayProgressBar(
"Timeline Name Collector",
$"正在解析: {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();
// 现有文件哈希,按 assetPath 去重
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("Timeline Name Collector",
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 = TimelineNameCache.GetAssetFullPath(assetPath);
if (string.IsNullOrEmpty(fullPath) || !File.Exists(fullPath))
continue;
var newHash = TimelineNameCache.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("Timeline Name Collector",
$"正在解析: {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 = TimelineNameCache.GetAssetFullPath(assetPath);
if (string.IsNullOrEmpty(fullPath) || !File.Exists(fullPath))
return;
var fileHash = precomputedHash ?? TimelineNameCache.ComputeFileHash(fullPath);
var lastModifiedUtc = File.GetLastWriteTimeUtc(fullPath).ToString("o");
_cache.RemoveEntriesForAsset(assetPath);
var entries = YamlTimelineParser.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;
}
}
}