// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Editor { #if UNITY_EDITOR using System; using System.Collections.Generic; using System.Reflection; using WallstopStudios.UnityHelpers.Utils; using UnityEditor; /// /// Handles the IMGUI needed to pick types and methods for animation events. /// internal static class AnimationEventMethodSelector { private const int DefaultTypeLimit = 50; private const int SearchTypeLimit = 200; public static IReadOnlyDictionary> FilterLookup( AnimationEventItem item, IReadOnlyDictionary> lookup ) { if (lookup == null) { return null; } string cacheKey = item.search + "|" + item.typeSearch; if (item.cachedLookup != null && item.lastSearchForCache == cacheKey) { return item.cachedLookup; } Dictionary> filtered = new(); using PooledResource> methodSearchLease = Buffers.List.Get( out List methodSearchTerms ); using PooledResource> typeSearchLease = Buffers.List.Get( out List typeSearchTerms ); { BuildSearchTerms(item.search, methodSearchTerms); BuildSearchTerms(item.typeSearch, typeSearchTerms); foreach (KeyValuePair> entry in lookup) { Type type = entry.Key; if (typeSearchTerms.Count > 0) { string typeLower = type.FullName != null ? type.FullName.ToLowerInvariant() : string.Empty; bool matches = ContainsAllTokens(typeLower, typeSearchTerms); if (!matches) { continue; } } if (methodSearchTerms.Count == 0) { filtered[type] = entry.Value; continue; } List methodBuffer = new(); foreach (MethodInfo method in entry.Value) { string methodLower = method.Name.ToLowerInvariant(); if (ContainsAllTokens(methodLower, methodSearchTerms)) { methodBuffer.Add(method); } } if (methodBuffer.Count > 0) { filtered[type] = methodBuffer; } } } item.cachedLookup = filtered; item.lastSearchForCache = cacheKey; return filtered; } public static void EnsureSelection( AnimationEventItem item, IReadOnlyDictionary> lookup ) { if (item.selectedType != null || lookup == null) { return; } using PooledResource> sortedTypesResource = Buffers.List.Get( out List sortedTypes ); { foreach (Type type in lookup.Keys) { sortedTypes.Add(type); } sortedTypes.Sort( static (lhs, rhs) => string.Compare(lhs.FullName, rhs.FullName, StringComparison.Ordinal) ); for (int ti = 0; ti < sortedTypes.Count; ti++) { Type type = sortedTypes[ti]; if (!lookup.TryGetValue(type, out IReadOnlyList methods)) { continue; } for (int mi = 0; mi < methods.Count; mi++) { MethodInfo method = methods[mi]; if ( string.Equals( method.Name, item.animationEvent.functionName, StringComparison.Ordinal ) ) { item.selectedType = type; item.selectedMethod = method; return; } } } } } public static void ValidateSelection( AnimationEventItem item, IReadOnlyDictionary> lookup ) { item.isValid = true; item.validationMessage = string.Empty; if (string.IsNullOrEmpty(item.animationEvent.functionName)) { item.isValid = false; item.validationMessage = "Function name is empty"; return; } if (item.selectedType != null && item.selectedMethod != null) { return; } if (lookup == null) { item.isValid = false; item.validationMessage = "No types available for validation."; return; } foreach (KeyValuePair> entry in lookup) { foreach (MethodInfo method in entry.Value) { if ( string.Equals( method.Name, item.animationEvent.functionName, StringComparison.Ordinal ) ) { return; } } } item.isValid = false; item.validationMessage = $"No method named '{item.animationEvent.functionName}' found in available types"; } public static bool DrawTypeSelector( AnimationEventItem item, IReadOnlyDictionary> lookup, Action recordUndo ) { if (lookup == null || lookup.Count == 0) { EditorGUILayout.HelpBox( "No types with animation event methods found", MessageType.Info ); return false; } using PooledResource> allTypesLease = Buffers.List.Get( out List allTypes ); { foreach (Type type in lookup.Keys) { allTypes.Add(type); } allTypes.Sort( static (lhs, rhs) => string.Compare(lhs.FullName, rhs.FullName, StringComparison.Ordinal) ); using PooledResource> filteredLease = Buffers.List.Get( out List filtered ); { ApplyTypeSearch(allTypes, item, filtered, out bool truncated); string[] displayNames = new string[filtered.Count]; for (int i = 0; i < filtered.Count; i++) { displayNames[i] = filtered[i]?.FullName ?? string.Empty; } int currentIndex = Math.Max(filtered.IndexOf(item.selectedType), 0); EditorGUI.BeginChangeCheck(); int selectedIndex = EditorGUILayout.Popup( "TypeName", currentIndex, displayNames ); if (EditorGUI.EndChangeCheck()) { recordUndo?.Invoke("Change Animation Event Type"); item.selectedType = filtered[selectedIndex]; item.selectedMethod = null; } if (truncated) { EditorGUILayout.HelpBox( "Result list trimmed. Use Type Search to narrow results.", MessageType.Info ); } return item.selectedType != null; } } } public static bool DrawMethodSelector( AnimationEventItem item, IReadOnlyDictionary> lookup, Action recordUndo ) { if (lookup == null || item.selectedType == null) { return false; } if (!lookup.TryGetValue(item.selectedType, out IReadOnlyList methods)) { methods = Array.Empty(); } using PooledResource> bufferLease = Buffers.List.Get( out List buffer ); { for (int i = 0; i < methods.Count; i++) { buffer.Add(methods[i]); } if (item.selectedMethod == null || !buffer.Contains(item.selectedMethod)) { foreach (MethodInfo methodInfo in buffer) { if ( string.Equals( methodInfo.Name, item.animationEvent.functionName, StringComparison.Ordinal ) ) { item.selectedMethod = methodInfo; break; } } if (item.selectedMethod != null && !buffer.Contains(item.selectedMethod)) { buffer.Add(item.selectedMethod); } } string[] methodNames = new string[buffer.Count]; int currentIndex = -1; for (int i = 0; i < buffer.Count; i++) { methodNames[i] = buffer[i]?.Name ?? string.Empty; if (buffer[i] == item.selectedMethod) { currentIndex = i; } } EditorGUI.BeginChangeCheck(); int selectedIndex = EditorGUILayout.Popup("MethodName", currentIndex, methodNames); if (EditorGUI.EndChangeCheck() && selectedIndex >= 0) { recordUndo?.Invoke("Change Animation Event Method"); item.selectedMethod = buffer[selectedIndex]; item.animationEvent.functionName = item.selectedMethod.Name; } return item.selectedMethod != null; } } private static void BuildSearchTerms(string raw, List destination) { destination.Clear(); if (string.IsNullOrWhiteSpace(raw)) { return; } string[] parts = raw.Split(' '); for (int i = 0; i < parts.Length; i++) { string token = parts[i]; if (string.IsNullOrWhiteSpace(token) || token == "*") { continue; } destination.Add(token.Trim().ToLowerInvariant()); } } private static bool ContainsAllTokens(string haystack, IReadOnlyList tokens) { for (int i = 0; i < tokens.Count; i++) { if (haystack.IndexOf(tokens[i], StringComparison.Ordinal) < 0) { return false; } } return true; } private static void ApplyTypeSearch( List allTypes, AnimationEventItem item, List filtered, out bool truncated ) { int limit = string.IsNullOrEmpty(item.typeSearch) ? DefaultTypeLimit : SearchTypeLimit; filtered.Clear(); for (int i = 0; i < allTypes.Count; i++) { filtered.Add(allTypes[i]); } if (!string.IsNullOrEmpty(item.typeSearch)) { string searchLower = item.typeSearch.ToLowerInvariant(); filtered.Clear(); for (int i = 0; i < allTypes.Count; i++) { Type type = allTypes[i]; string fullName = type.FullName ?? string.Empty; if ( fullName.ToLowerInvariant().IndexOf(searchLower, StringComparison.Ordinal) >= 0 ) { filtered.Add(type); } } } if (item.selectedType != null && !filtered.Contains(item.selectedType)) { filtered.Insert(0, item.selectedType); } if (filtered.Count > limit) { truncated = true; filtered.RemoveRange(limit, filtered.Count - limit); return; } truncated = false; } } #endif }