#if UNITY_EDITOR using System.IO; using System.Linq; using System.Reflection; using System.Collections.Generic; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.UIElements; using UnityEngine.SceneManagement; [InitializeOnLoad] public static class SceneSwitcherToolbar { private const string NotInBuildSuffix = " (not in build)"; private const string FetchAllScenesKey = "SceneSwitcher_FetchAllScenes"; private static string[] sceneNames = new string[0]; private static int selectedIndex = 0; private static string lastActiveScene = string.Empty; private static VisualElement toolbarUI; private static int lastToolbarInstanceId = -1; private static double nextToolbarCheckTime = 0d; private const double ToolbarCheckInterval = 0.25d; private static bool fetchAllScenes { get => EditorPrefs.GetBool(FetchAllScenesKey, false); set => EditorPrefs.SetBool(FetchAllScenesKey, value); } static SceneSwitcherToolbar() { RefreshSceneList(); SelectCurrentScene(); EditorSceneManager.activeSceneChangedInEditMode += (_, __) => UpdateSceneSelection(); EditorApplication.playModeStateChanged += OnPlayModeChanged; EditorApplication.update += EnsureToolbarExists; AssemblyReloadEvents.afterAssemblyReload += OnAfterAssemblyReload; EditorApplication.delayCall += AddToolbarUI; } private static void OnAfterAssemblyReload() { EditorApplication.delayCall += AddToolbarUI; } private static void EnsureToolbarExists() { if (EditorApplication.timeSinceStartup < nextToolbarCheckTime) return; nextToolbarCheckTime = EditorApplication.timeSinceStartup + ToolbarCheckInterval; var toolbarType = typeof(Editor).Assembly.GetType("UnityEditor.Toolbar"); if (toolbarType == null) return; var toolbars = Resources.FindObjectsOfTypeAll(toolbarType); if (toolbars == null || toolbars.Length == 0) return; foreach (var toolbar in toolbars) { TryInjectIntoToolbar(toolbarType, toolbar); } } private static void TryInjectIntoToolbar(System.Type toolbarType, UnityEngine.Object toolbar) { var rootField = toolbarType.GetField("m_Root", BindingFlags.NonPublic | BindingFlags.Instance); if (rootField == null) return; if (rootField.GetValue(toolbar) is not VisualElement root) return; var leftContainer = root.Q("ToolbarZoneLeftAlign"); if (leftContainer == null) return; var existing = leftContainer.Q("SceneSwitcherToolbar"); if (existing != null) { toolbarUI = existing; return; } var newToolbarUI = new IMGUIContainer(DrawToolbarGUI); newToolbarUI.name = "SceneSwitcherToolbar"; newToolbarUI.style.flexGrow = 0; newToolbarUI.style.flexShrink = 1; newToolbarUI.style.minWidth = 80; newToolbarUI.style.maxWidth = 320; newToolbarUI.style.marginLeft = 4; newToolbarUI.style.marginRight = 4; newToolbarUI.style.overflow = Overflow.Hidden; leftContainer.Add(newToolbarUI); toolbarUI = newToolbarUI; } private static void AddToolbarUI() { var toolbarType = typeof(Editor).Assembly.GetType("UnityEditor.Toolbar"); if (toolbarType == null) return; var toolbars = Resources.FindObjectsOfTypeAll(toolbarType); if (toolbars == null || toolbars.Length == 0) return; foreach (var toolbar in toolbars) { TryInjectIntoToolbar(toolbarType, toolbar); } } private static void DrawToolbarGUI() { CheckAndRefreshScenes(); GUILayout.BeginHorizontal(EditorStyles.toolbar); if (sceneNames == null || sceneNames.Length == 0) { GUILayout.Label("No scenes", EditorStyles.toolbarButton, GUILayout.Width(100), GUILayout.Height(18)); GUILayout.EndHorizontal(); return; } if (selectedIndex < 0 || selectedIndex >= sceneNames.Length) selectedIndex = 0; bool isPlaying = EditorApplication.isPlaying; bool currentSceneNotInBuild = selectedIndex >= 0 && selectedIndex < sceneNames.Length && sceneNames[selectedIndex].EndsWith(NotInBuildSuffix); float viewWidth = Mathf.Max(120f, EditorGUIUtility.currentViewWidth); const float toggleWidth = 22f; const float addWidth = 22f; const float spacing = 0f; const float controlHeight = 18f; // Порог узкого режима bool compactMode = viewWidth < 1400f; float sceneButtonWidth = compactMode ? 22f : Mathf.Clamp( viewWidth - toggleWidth - addWidth - 10f, 90f, 190f ); EditorGUI.BeginDisabledGroup(isPlaying); bool newFetchAllScenes = GUILayout.Toggle( fetchAllScenes, new GUIContent("A", "Toggle All Scenes / Build Settings scenes"), EditorStyles.toolbarButton, GUILayout.Width(toggleWidth), GUILayout.Height(controlHeight) ); if (newFetchAllScenes != fetchAllScenes) { fetchAllScenes = newFetchAllScenes; RefreshSceneList(); SelectCurrentScene(); GUIUtility.ExitGUI(); } EditorGUI.EndDisabledGroup(); GUILayout.Space(spacing); EditorGUI.BeginDisabledGroup(isPlaying || fetchAllScenes); if (GUILayout.Button( new GUIContent("+", "Add current scene to Build Settings"), EditorStyles.toolbarButton, GUILayout.Width(addWidth), GUILayout.Height(controlHeight))) { AddCurrentSceneToBuildSettings(); GUIUtility.ExitGUI(); } EditorGUI.EndDisabledGroup(); GUILayout.Space(spacing); EditorGUI.BeginDisabledGroup(isPlaying); Rect sceneButtonRect = GUILayoutUtility.GetRect( GUIContent.none, GetSceneButtonStyle(currentSceneNotInBuild), GUILayout.Width(sceneButtonWidth), GUILayout.Height(controlHeight) ); string buttonText = compactMode ? "▼" : GetDisplaySceneName(); string tooltip = compactMode ? "Open scene list" : "Current scene"; if (GUI.Button( sceneButtonRect, new GUIContent(buttonText, tooltip), compactMode ? EditorStyles.toolbarButton : GetSceneButtonStyle(currentSceneNotInBuild))) { ShowSceneMenu(sceneButtonRect); } EditorGUI.EndDisabledGroup(); GUILayout.EndHorizontal(); } private static GUIStyle GetSceneButtonStyle(bool highlight) { var style = new GUIStyle(EditorStyles.toolbarDropDown) { alignment = TextAnchor.MiddleLeft, fixedHeight = 20, clipping = TextClipping.Clip, imagePosition = ImagePosition.TextOnly, padding = new RectOffset(6, 18, 0, 0), margin = new RectOffset(0, 0, 0, 0), fontStyle = highlight ? FontStyle.Italic : FontStyle.Normal }; if (highlight) style.normal.textColor = new Color(1f, 0.82f, 0.35f); return style; } private static string GetDisplaySceneName() { if (sceneNames == null || sceneNames.Length == 0) return "No scenes"; if (selectedIndex < 0 || selectedIndex >= sceneNames.Length) return "No scene"; return sceneNames[selectedIndex]; } private static void ShowSceneMenu(Rect buttonRect) { var menu = new GenericMenu(); string currentDisplayName = GetDisplaySceneName(); foreach (string displayName in sceneNames) { string cleanName = StripNotInBuildSuffix(displayName); bool isCurrent = displayName == currentDisplayName; menu.AddItem(new GUIContent(cleanName), isCurrent, () => { OpenSceneByName(cleanName, additive: false); }); } buttonRect.y += buttonRect.height - 10f; menu.DropDown(buttonRect); } private static void PingSceneAsset(string scenePath) { var asset = AssetDatabase.LoadAssetAtPath(scenePath); if (asset != null) { EditorGUIUtility.PingObject(asset); Selection.activeObject = asset; } } private static string StripNotInBuildSuffix(string displaySceneName) { return displaySceneName.EndsWith(NotInBuildSuffix) ? displaySceneName.Substring(0, displaySceneName.Length - NotInBuildSuffix.Length) : displaySceneName; } private static void OpenSceneByName(string sceneName, bool additive) { string scenePath = FindScenePathByName(sceneName, fetchAllScenes); if (string.IsNullOrEmpty(scenePath)) { Debug.LogError("Scene not found: " + sceneName); return; } if (!additive) { if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) return; EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single); } else { if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) return; EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive); } } private static string FindScenePathByName(string sceneName, bool searchAllAssets) { if (searchAllAssets) { return Directory.GetFiles("Assets", "*.unity", SearchOption.AllDirectories) .FirstOrDefault(path => Path.GetFileNameWithoutExtension(path) == sceneName); } return EditorBuildSettings.scenes .FirstOrDefault(scene => scene.enabled && Path.GetFileNameWithoutExtension(scene.path) == sceneName )?.path; } private static void AddCurrentSceneToBuildSettings() { string currentScenePath = EditorSceneManager.GetActiveScene().path; if (string.IsNullOrEmpty(currentScenePath)) { EditorUtility.DisplayDialog( "Scene Not Saved", "Please save the scene before adding it to Build Settings.", "OK" ); return; } var buildScenes = EditorBuildSettings.scenes.ToList(); int existingIndex = buildScenes.FindIndex(s => s.path == currentScenePath); if (existingIndex >= 0) { if (!buildScenes[existingIndex].enabled) { buildScenes[existingIndex] = new EditorBuildSettingsScene(currentScenePath, true); EditorBuildSettings.scenes = buildScenes.ToArray(); Debug.Log($"Scene '{Path.GetFileNameWithoutExtension(currentScenePath)}' enabled in Build Settings."); } else { Debug.Log($"Scene '{Path.GetFileNameWithoutExtension(currentScenePath)}' is already in Build Settings."); } } else { buildScenes.Add(new EditorBuildSettingsScene(currentScenePath, true)); EditorBuildSettings.scenes = buildScenes.ToArray(); Debug.Log($"Scene '{Path.GetFileNameWithoutExtension(currentScenePath)}' added to Build Settings."); } RefreshSceneList(); SelectCurrentScene(); } private static void RefreshSceneList() { if (fetchAllScenes) { var buildSceneSet = new HashSet( EditorBuildSettings.scenes .Where(scene => scene.enabled) .Select(scene => Path.GetFileNameWithoutExtension(scene.path)) ); sceneNames = Directory.GetFiles("Assets", "*.unity", SearchOption.AllDirectories) .Select(Path.GetFileNameWithoutExtension) .Where(n => !string.IsNullOrWhiteSpace(n)) .Distinct() .OrderBy(n => n) .Select(name => buildSceneSet.Contains(name) ? name : name + NotInBuildSuffix) .ToArray(); } else { sceneNames = EditorBuildSettings.scenes .Where(scene => scene.enabled) .Select(scene => Path.GetFileNameWithoutExtension(scene.path)) .Where(n => !string.IsNullOrWhiteSpace(n)) .Distinct() .OrderBy(n => n) .ToArray(); } } private static void CheckAndRefreshScenes() { string[] currentScenes; if (fetchAllScenes) { var buildSceneSet = new HashSet( EditorBuildSettings.scenes .Where(scene => scene.enabled) .Select(scene => Path.GetFileNameWithoutExtension(scene.path)) ); currentScenes = Directory.GetFiles("Assets", "*.unity", SearchOption.AllDirectories) .Select(Path.GetFileNameWithoutExtension) .Where(n => !string.IsNullOrWhiteSpace(n)) .Distinct() .OrderBy(n => n) .Select(name => buildSceneSet.Contains(name) ? name : name + NotInBuildSuffix) .ToArray(); } else { currentScenes = EditorBuildSettings.scenes .Where(scene => scene.enabled) .Select(scene => Path.GetFileNameWithoutExtension(scene.path)) .Where(n => !string.IsNullOrWhiteSpace(n)) .Distinct() .OrderBy(n => n) .ToArray(); } if (!currentScenes.SequenceEqual(sceneNames)) { sceneNames = currentScenes; SelectCurrentScene(); } } private static void SelectCurrentScene() { string currentScene = Path.GetFileNameWithoutExtension(EditorSceneManager.GetActiveScene().path); if (string.IsNullOrEmpty(currentScene)) { selectedIndex = 0; lastActiveScene = string.Empty; return; } int index = System.Array.FindIndex(sceneNames, n => StripNotInBuildSuffix(n) == currentScene); if (index >= 0) { selectedIndex = index; } else { string displayName = currentScene + NotInBuildSuffix; sceneNames = new[] { displayName }.Concat(sceneNames).ToArray(); selectedIndex = 0; } lastActiveScene = currentScene; } private static void UpdateSceneSelection() { string currentScene = Path.GetFileNameWithoutExtension(EditorSceneManager.GetActiveScene().path); if (currentScene != lastActiveScene) { SelectCurrentScene(); } } private static void OnPlayModeChanged(PlayModeStateChange state) { if (state == PlayModeStateChange.EnteredPlayMode || state == PlayModeStateChange.ExitingPlayMode || state == PlayModeStateChange.EnteredEditMode) { EditorApplication.delayCall += AddToolbarUI; } } } #endif