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
}
}