Forums › 💬 Slate Sequencer › 🤩 Custom Clips › Animation Preview/selection window(not a clip)
I made this custom Humanoid Animation Preview/selection window to make it easier to find/use clips for Slate.
I’ve made two full length films in Slate and this window saves a ton of time when using a lot of animation with Slate.
The gist is here gist link

* Press Enter/Return after a query to make arrow key navigation active.
Code
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
using UnityEngine; using UnityEditor; using System.Collections.Generic; public class HumanoidAnimationWindow : EditorWindow { private Vector2 scrollPos; private string searchQuery = ""; private string lastSearchQuery = ""; private AnimationClip selectedClip; // A small helper class to cache information about each clip. private class AnimationClipInfo { public AnimationClip clip; public string normalizedName; // Lower-case, underscores replaced with spaces. public string assetPath; public AnimationClipInfo(AnimationClip clip, string normalizedName, string assetPath) { this.clip = clip; this.normalizedName = normalizedName; this.assetPath = assetPath; } } // Static cache of *all* humanoid clips found in the project. private static List<AnimationClipInfo> cachedClips = null; // Filtered list used for display based on search terms. private List<AnimationClipInfo> filteredClips = new List<AnimationClipInfo>(); [MenuItem("Window/Humanoid Animations")] public static void ShowWindow() { GetWindow<HumanoidAnimationWindow>("Humanoid Animations"); } private void OnEnable() { EditorApplication.projectChanged += OnProjectChanged; RefreshAnimations(); ApplySearchFilter(); } private void OnDisable() { EditorApplication.projectChanged -= OnProjectChanged; } private void OnProjectChanged() { cachedClips = null; RefreshAnimations(); ApplySearchFilter(); Repaint(); } // Force a (re)scan of humanoid clips in the project. private void RefreshAnimations() { if (cachedClips != null) return; // Only rebuild if needed cachedClips = new List<AnimationClipInfo>(); string[] guids = AssetDatabase.FindAssets("t:AnimationClip"); for (int i = 0; i < guids.Length; i++) { string assetPath = AssetDatabase.GUIDToAssetPath(guids[i]); AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(assetPath); if (clip != null) { // Check if it's a humanoid clip var importer = AssetImporter.GetAtPath(assetPath) as ModelImporter; if (importer != null && importer.animationType == ModelImporterAnimationType.Human) { string normalizedName = clip.name.ToLower().Replace("_", " "); cachedClips.Add(new AnimationClipInfo(clip, normalizedName, assetPath)); } } } } // Only re-filter when the search query changes or when the cache is invalidated private void ApplySearchFilter() { if (cachedClips == null) { filteredClips.Clear(); return; } // If there's no search query, just show everything if (string.IsNullOrEmpty(searchQuery)) { filteredClips = new List<AnimationClipInfo>(cachedClips); return; } // Preprocess the search query string[] terms = searchQuery.ToLower().Split(new char[] { ' ' }, System.StringSplitOptions.RemoveEmptyEntries); filteredClips.Clear(); foreach (var clipInfo in cachedClips) { bool allMatch = true; foreach (string term in terms) { if (!clipInfo.normalizedName.Contains(term)) { allMatch = false; break; } } if (allMatch) filteredClips.Add(clipInfo); } } private void OnGUI() { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Search:", GUILayout.Width(50)); string newQuery = EditorGUILayout.TextField(searchQuery); EditorGUILayout.EndHorizontal(); // Only re-apply filter if the query has changed if (newQuery != searchQuery) { searchQuery = newQuery; ApplySearchFilter(); // NEW: auto-select first clip if any results exist if (filteredClips.Count > 0) { SelectClip(filteredClips[0]); } } // Arrow-key handling HandleKeyboardInput(); scrollPos = EditorGUILayout.BeginScrollView(scrollPos); if (filteredClips != null) { foreach (var clipInfo in filteredClips) { // Reserve a rect for a single row Rect lineRect = EditorGUILayout.GetControlRect(GUILayout.Height(EditorGUIUtility.singleLineHeight)); float buttonWidth = 60f; Rect objectFieldRect = new Rect(lineRect.x, lineRect.y, lineRect.width - buttonWidth, lineRect.height); Rect buttonRect = new Rect(lineRect.x + lineRect.width - buttonWidth, lineRect.y, buttonWidth, lineRect.height); // Draw highlight behind the row if it's selected if (selectedClip == clipInfo.clip) { Color oldColor = GUI.color; GUI.color = new Color(1f, 1f, 0f, 0.2f); // semi-transparent yellow GUI.Box(lineRect, GUIContent.none); GUI.color = oldColor; } // Highlight the currently selected clip. if (selectedClip == clipInfo.clip) { Color highlightColor = Color.yellow; float thickness = 20f; // Draw borders around the row. EditorGUI.DrawRect(new Rect(lineRect.x, lineRect.y, lineRect.width, thickness), highlightColor); // Top EditorGUI.DrawRect(new Rect(lineRect.x, lineRect.y + lineRect.height - thickness, lineRect.width, thickness), highlightColor); // Bottom EditorGUI.DrawRect(new Rect(lineRect.x, lineRect.y, thickness, lineRect.height), highlightColor); // Left EditorGUI.DrawRect(new Rect(lineRect.x + lineRect.width - thickness, lineRect.y, thickness, lineRect.height), highlightColor); // Right } // Draw the clip in a read-only object field EditorGUI.ObjectField(objectFieldRect, clipInfo.clip, typeof(AnimationClip), false); // "Select" button now actively selects the clip in the Project & Inspector if (GUI.Button(buttonRect, "Select")) { SelectClip(clipInfo); } // Optional drag-and-drop support if (Event.current.type == EventType.MouseDrag && objectFieldRect.Contains(Event.current.mousePosition)) { DragAndDrop.PrepareStartDrag(); DragAndDrop.objectReferences = new Object[] { clipInfo.clip }; DragAndDrop.StartDrag(clipInfo.clip.name); Event.current.Use(); } } } EditorGUILayout.EndScrollView(); GUILayout.FlexibleSpace(); EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (GUILayout.Button("Refresh", GUILayout.Width(position.width * 0.15f))) { cachedClips = null; RefreshAnimations(); ApplySearchFilter(); } GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); } private void HandleKeyboardInput() { Event e = Event.current; if (e.type == EventType.KeyDown) { if (filteredClips.Count == 0) return; int currentIndex = filteredClips.FindIndex(info => info.clip == selectedClip); if (e.keyCode == KeyCode.UpArrow) { e.Use(); // If nothing is selected, or index is -1, select the first item when pressing Up. currentIndex = (currentIndex < 0) ? 0 : currentIndex - 1; if (currentIndex < 0) currentIndex = 0; SelectClip(filteredClips[currentIndex]); } else if (e.keyCode == KeyCode.DownArrow) { e.Use(); // If nothing is selected, or index is -1, select the first item when pressing Down. currentIndex = (currentIndex < 0) ? 0 : currentIndex + 1; if (currentIndex >= filteredClips.Count) currentIndex = filteredClips.Count - 1; SelectClip(filteredClips[currentIndex]); } } } private void SelectClip(AnimationClipInfo clipInfo) { selectedClip = clipInfo.clip; Selection.activeObject = clipInfo.clip; EditorGUIUtility.PingObject(clipInfo.clip); Debug.Log(clipInfo.clip.name + " with path: " + clipInfo.assetPath); } } |
Great, Thanks for sharing! 🙂
Video showing usage
this sounds neat but that link is broken.
it says “http://press%20enter/Return%20after%20a%20query%20to%20make%20arrow%20key%20nav%20active”
