317 lines
11 KiB
C#
317 lines
11 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEditor;
|
|
using UnityEditor.SceneManagement;
|
|
using UnityEngine;
|
|
|
|
namespace Ashwild.EditorTools
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public class SceneFinderWindow : EditorWindow
|
|
{
|
|
#region Constants
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private const string FavoritesKey = "Ashwild.SceneFinder.Favorites";
|
|
|
|
#endregion
|
|
|
|
#region State
|
|
|
|
private readonly List<SceneEntry> allScenes = new List<SceneEntry>();
|
|
private readonly HashSet<string> favoriteGuids = new HashSet<string>();
|
|
|
|
private string search = string.Empty;
|
|
private Vector2 scroll;
|
|
private bool showFavoritesOnly;
|
|
|
|
private GUIStyle pathStyle;
|
|
private GUIStyle sectionStyle;
|
|
|
|
#endregion
|
|
|
|
#region Types
|
|
|
|
/// <summary>
|
|
/// A single discovered scene: its GUID (stable key), display name and project-relative path.
|
|
/// </summary>
|
|
private struct SceneEntry
|
|
{
|
|
public string Guid;
|
|
public string Name;
|
|
public string Path;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Menu
|
|
|
|
/// <summary>
|
|
/// Opens (or focuses) the Scene Finder window.
|
|
/// </summary>
|
|
[MenuItem("Tools/Scene Finder")]
|
|
public static void Open()
|
|
{
|
|
SceneFinderWindow window = GetWindow<SceneFinderWindow>();
|
|
window.titleContent = new GUIContent("Scene Finder", EditorGUIUtility.IconContent("SceneAsset Icon").image);
|
|
window.minSize = new Vector2(340, 300);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Window Lifecycle
|
|
|
|
/// <summary>
|
|
/// Loads the persisted favourites and scans the project for scenes when the window appears.
|
|
/// </summary>
|
|
private void OnEnable()
|
|
{
|
|
LoadFavorites();
|
|
RefreshScenes();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Scene Discovery
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Reads the favourite GUID set from EditorPrefs into memory.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes the current favourite GUID set back to EditorPrefs so it survives domain reloads and
|
|
/// editor restarts on this machine.
|
|
/// </summary>
|
|
private void SaveFavorites()
|
|
{
|
|
EditorPrefs.SetString(FavoritesKey, string.Join(";", favoriteGuids));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pins or unpins a scene and persists the change immediately.
|
|
/// </summary>
|
|
private void ToggleFavorite(string guid)
|
|
{
|
|
if (!favoriteGuids.Remove(guid)) favoriteGuids.Add(guid);
|
|
SaveFavorites();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GUI
|
|
|
|
/// <summary>
|
|
/// Draws the toolbar and the favourites + all-scenes lists.
|
|
/// </summary>
|
|
private void OnGUI()
|
|
{
|
|
EnsureStyles();
|
|
DrawToolbar();
|
|
|
|
scroll = EditorGUILayout.BeginScrollView(scroll);
|
|
|
|
List<SceneEntry> filtered = Filter();
|
|
List<SceneEntry> 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<SceneEntry> 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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draws the top toolbar: search field with a clear button, a favourites-only toggle and refresh.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draws a single scene row: the pin toggle, its name and dimmed path, and the open/ping actions.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draws a small section separator label.
|
|
/// </summary>
|
|
private void DrawSectionHeader(string title)
|
|
{
|
|
GUILayout.Label(title, sectionStyle);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Actions
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Selects and pings the scene asset in the Project window.
|
|
/// </summary>
|
|
private void PingScene(string path)
|
|
{
|
|
SceneAsset asset = AssetDatabase.LoadAssetAtPath<SceneAsset>(path);
|
|
if (asset == null) return;
|
|
|
|
Selection.activeObject = asset;
|
|
EditorGUIUtility.PingObject(asset);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Internal Helpers
|
|
|
|
/// <summary>
|
|
/// Returns the scenes matching the current search text (case-insensitive, matched against both
|
|
/// name and path). An empty search returns everything.
|
|
/// </summary>
|
|
private List<SceneEntry> 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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
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
|
|
}
|
|
}
|