using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; namespace Ashwild.EditorTools { /// /// The Scene Finder — a lightweight editor window that lists every scene in the project, filters /// them by a search box, and lets you pin the ones you use most as favourites so you never hunt /// through the Project window again. Favourites are keyed by asset GUID (so they survive renames /// and moves) and persisted per-machine in EditorPrefs, which means the pinned set follows the /// user's PC rather than living in the repo. Each row can open the scene (single or additive) or /// ping it in the Project window. /// public class SceneFinderWindow : EditorWindow { #region Constants /// /// EditorPrefs key holding the favourite scene GUIDs joined by ';'. Per-user, per-machine — the /// favourites are a personal shortcut list, deliberately not shared through the project. /// private const string FavoritesKey = "Ashwild.SceneFinder.Favorites"; #endregion #region State private readonly List allScenes = new List(); private readonly HashSet favoriteGuids = new HashSet(); private string search = string.Empty; private Vector2 scroll; private bool showFavoritesOnly; private GUIStyle pathStyle; private GUIStyle sectionStyle; #endregion #region Types /// /// A single discovered scene: its GUID (stable key), display name and project-relative path. /// private struct SceneEntry { public string Guid; public string Name; public string Path; } #endregion #region Menu /// /// Opens (or focuses) the Scene Finder window. /// [MenuItem("Tools/Scene Finder")] public static void Open() { SceneFinderWindow window = GetWindow(); window.titleContent = new GUIContent("Scene Finder", EditorGUIUtility.IconContent("SceneAsset Icon").image); window.minSize = new Vector2(340, 300); } #endregion #region Window Lifecycle /// /// Loads the persisted favourites and scans the project for scenes when the window appears. /// private void OnEnable() { LoadFavorites(); RefreshScenes(); } #endregion #region Scene Discovery /// /// Rebuilds the scene list from the AssetDatabase, sorted by name. Called on open and whenever /// the user hits Refresh so newly created or deleted scenes are reflected. /// private void RefreshScenes() { allScenes.Clear(); foreach (string guid in AssetDatabase.FindAssets("t:Scene")) { string path = AssetDatabase.GUIDToAssetPath(guid); if (string.IsNullOrEmpty(path)) continue; allScenes.Add(new SceneEntry { Guid = guid, Name = System.IO.Path.GetFileNameWithoutExtension(path), Path = path }); } allScenes.Sort((a, b) => string.Compare(a.Name, b.Name, System.StringComparison.OrdinalIgnoreCase)); } #endregion #region Favorites Persistence /// /// Reads the favourite GUID set from EditorPrefs into memory. /// private void LoadFavorites() { favoriteGuids.Clear(); string raw = EditorPrefs.GetString(FavoritesKey, string.Empty); if (string.IsNullOrEmpty(raw)) return; foreach (string guid in raw.Split(';')) if (!string.IsNullOrEmpty(guid)) favoriteGuids.Add(guid); } /// /// Writes the current favourite GUID set back to EditorPrefs so it survives domain reloads and /// editor restarts on this machine. /// private void SaveFavorites() { EditorPrefs.SetString(FavoritesKey, string.Join(";", favoriteGuids)); } /// /// Pins or unpins a scene and persists the change immediately. /// private void ToggleFavorite(string guid) { if (!favoriteGuids.Remove(guid)) favoriteGuids.Add(guid); SaveFavorites(); } #endregion #region GUI /// /// Draws the toolbar and the favourites + all-scenes lists. /// private void OnGUI() { EnsureStyles(); DrawToolbar(); scroll = EditorGUILayout.BeginScrollView(scroll); List filtered = Filter(); List favorites = filtered.Where(s => favoriteGuids.Contains(s.Guid)).ToList(); if (favorites.Count > 0) { DrawSectionHeader($"★ Favorites ({favorites.Count})"); foreach (SceneEntry entry in favorites) DrawRow(entry); GUILayout.Space(6); } if (!showFavoritesOnly) { List rest = filtered.Where(s => !favoriteGuids.Contains(s.Guid)).ToList(); DrawSectionHeader($"All Scenes ({rest.Count})"); if (rest.Count == 0 && favorites.Count == 0) EditorGUILayout.HelpBox("No scene matches the search.", MessageType.Info); foreach (SceneEntry entry in rest) DrawRow(entry); } EditorGUILayout.EndScrollView(); } /// /// Draws the top toolbar: search field with a clear button, a favourites-only toggle and refresh. /// private void DrawToolbar() { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); search = GUILayout.TextField(search, EditorStyles.toolbarSearchField, GUILayout.MinWidth(80)); if (GUILayout.Button("✕", EditorStyles.toolbarButton, GUILayout.Width(22))) { search = string.Empty; GUI.FocusControl(null); } showFavoritesOnly = GUILayout.Toggle(showFavoritesOnly, "★ Only", EditorStyles.toolbarButton, GUILayout.Width(50)); if (GUILayout.Button("Refresh", EditorStyles.toolbarButton, GUILayout.Width(60))) RefreshScenes(); EditorGUILayout.EndHorizontal(); } /// /// Draws a single scene row: the pin toggle, its name and dimmed path, and the open/ping actions. /// private void DrawRow(SceneEntry entry) { EditorGUILayout.BeginHorizontal(EditorStyles.helpBox); bool isFav = favoriteGuids.Contains(entry.Guid); bool newFav = GUILayout.Toggle(isFav, isFav ? "★" : "☆", EditorStyles.label, GUILayout.Width(20)); if (newFav != isFav) ToggleFavorite(entry.Guid); EditorGUILayout.BeginVertical(); EditorGUILayout.LabelField(entry.Name, EditorStyles.boldLabel); EditorGUILayout.LabelField(entry.Path, pathStyle); EditorGUILayout.EndVertical(); if (GUILayout.Button(new GUIContent("Open", "Open this scene (replaces the current one)"), GUILayout.Width(46))) OpenScene(entry.Path, OpenSceneMode.Single); if (GUILayout.Button(new GUIContent("+", "Open additively alongside the current scene(s)"), GUILayout.Width(24))) OpenScene(entry.Path, OpenSceneMode.Additive); if (GUILayout.Button(new GUIContent("◎", "Ping in the Project window"), GUILayout.Width(24))) PingScene(entry.Path); EditorGUILayout.EndHorizontal(); } /// /// Draws a small section separator label. /// private void DrawSectionHeader(string title) { GUILayout.Label(title, sectionStyle); } #endregion #region Actions /// /// Opens a scene, prompting to save any unsaved changes first (single mode only). Guards the /// missing-scene case with a clear log so a stale favourite fails loudly rather than silently. /// private void OpenScene(string path, OpenSceneMode mode) { if (!System.IO.File.Exists(path)) { Debug.LogError($"[SceneFinder] Scene not found at '{path}' — it may have been moved or deleted."); RefreshScenes(); return; } if (mode == OpenSceneMode.Single && !EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) return; EditorSceneManager.OpenScene(path, mode); } /// /// Selects and pings the scene asset in the Project window. /// private void PingScene(string path) { SceneAsset asset = AssetDatabase.LoadAssetAtPath(path); if (asset == null) return; Selection.activeObject = asset; EditorGUIUtility.PingObject(asset); } #endregion #region Internal Helpers /// /// Returns the scenes matching the current search text (case-insensitive, matched against both /// name and path). An empty search returns everything. /// private List Filter() { if (string.IsNullOrWhiteSpace(search)) return allScenes; string needle = search.Trim().ToLowerInvariant(); return allScenes.Where(s => s.Name.ToLowerInvariant().Contains(needle) || s.Path.ToLowerInvariant().Contains(needle)).ToList(); } /// /// Lazily builds the cached GUI styles once the skin is available (styles can't be created in /// OnEnable because the editor skin isn't ready yet). /// private void EnsureStyles() { if (pathStyle == null) { pathStyle = new GUIStyle(EditorStyles.miniLabel) { wordWrap = false }; pathStyle.normal.textColor = new Color(0.55f, 0.55f, 0.55f); } if (sectionStyle == null) { sectionStyle = new GUIStyle(EditorStyles.boldLabel) { margin = new RectOffset(4, 4, 6, 2) }; } } #endregion } }