using System.IO; using System.Linq; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; namespace Unity.Tutorials.Core.Editor { /// /// Runs IET project initialization logic. /// [InitializeOnLoad] public static class UserStartupCode { internal static void RunStartupCode(TutorialProjectSettings projectSettings) { if (projectSettings.InitialScene != null) EditorSceneManager.OpenScene(AssetDatabase.GetAssetPath(projectSettings.InitialScene)); TutorialManager.WriteAssetsToTutorialDefaultsFolder(); // Ensure Editor is in predictable state EditorPrefs.SetString("ComponentSearchString", string.Empty); Tools.current = Tool.Move; if (TutorialEditorUtils.FindAssets().Any()) { var existingWindow = EditorWindowUtils.FindOpenInstance(); if (existingWindow) existingWindow.Close(); ShowTutorialWindow(); } // NOTE camera settings can be applied successfully only after potential layout changes if (projectSettings.InitialCameraSettings != null && projectSettings.InitialCameraSettings.Enabled) projectSettings.InitialCameraSettings.Apply(); if (projectSettings.WelcomePage) TutorialModalWindow.Show(projectSettings.WelcomePage); } /// /// Shows Tutorials window using the currently specified behaviour. /// /// /// Different behaviors: /// 1. If a single root tutorial container (TutorialContainer.ParentContainer is null) that has Project Layout specified exists, /// the window is loaded and shown using the specified project window layout (old behaviour). /// If the project layout does not contain Tutorials window, the window is shown an as a free-floating window. /// 2. If no root tutorial containers exist, or a root container's Project Layout is not specified, the window is shown /// by anchoring and docking it next to the Inspector (new behaviour). If the Inspector is not available, /// the window is shown an as a free-floating window. /// 3. If there is more than one root tutorial container with different Project Layout setting in the project, /// one asset is chosen randomly to specify the behavior. /// 4. If Tutorials window is already created, it is simply brought to the foreground and focused. /// /// The the created, or aleady existing, window instance. public static TutorialWindow ShowTutorialWindow() { var rootContainers = TutorialEditorUtils.FindAssets() .Where(container => container.ParentContainer is null); var defaultContainer = rootContainers.FirstOrDefault(); var projectLayout = defaultContainer?.ProjectLayout; if (rootContainers.Any(container => container.ProjectLayout != projectLayout)) { Debug.LogWarningFormat( "There is more than one TutorialContainers asset with different Project Layout setting in the project. " + "Using asset at path {0} for the window behavior settings.", AssetDatabase.GetAssetPath(defaultContainer) ); } TutorialWindow window = null; if (!rootContainers.Any() || defaultContainer.ProjectLayout == null) window = TutorialWindow.GetOrCreateWindowNextToInspector(); else if (defaultContainer.ProjectLayout != null) window = TutorialWindow.GetOrCreateWindowAndLoadLayout(defaultContainer); // If we have more than one root container, we show a selection view. Exactly one (or zero) container // is set active immediately without possibility to return to the the selection view. if (rootContainers.Count() > 1) window.SetContainers(rootContainers); else window.ActiveContainer = defaultContainer; return window; } internal static readonly string initFileMarkerPath = "InitCodeMarker"; // Folder so that user can easily create this from the Editor's Project view. internal static readonly string dontRunInitCodeMarker = "Assets/DontRunInitCodeMarker"; static UserStartupCode() { if (EditorApplication.isPlayingOrWillChangePlaymode || TutorialManager.IsLoadingLayout) return; // Language change triggers an assembly reload. if (LoadPreviousEditorLanguage() != LocalizationDatabaseProxy.currentEditorLanguage) { SaveCurrentEditorLanguage(); // There are several smaller and bigger localization issues with if we don't restart // the Editor so let's query the user to do so. var title = Localization.Tr("Editor Language Change Detected"); var msg = Localization.Tr("It's recommended to restart the Editor for the language change to be applied fully."); var ok = Localization.Tr("Restart"); var cancel = Localization.Tr("Continue without restarting"); if (EditorUtility.DisplayDialog(title, msg, ok, cancel)) RestartEditor(); } EditorApplication.update += InitRunStartupCode; } static void InitRunStartupCode() { if (IsDontRunInitCodeMarkerSet()) return; if (LocalizationDatabaseProxy.enableEditorLocalization && !IsLanguageInitialized()) { // Need to Request a script reload in order overcome Editor Localization issues // with static initialization when opening the project for the first time. SetLanguageInitialized(); EditorUtility.RequestScriptReload(); return; } // Prepare the layout always. For example, the user might have moved the project around, // so we need to ensure the file paths in the layouts are correct. PrepareWindowLayouts(); EditorApplication.update -= InitRunStartupCode; if (IsInitialized()) return; SetInitialized(); RunStartupCode(TutorialProjectSettings.Instance); } /// /// Has the IET project initialization been performed? /// /// static bool IsInitialized() => File.Exists(initFileMarkerPath); static bool IsDontRunInitCodeMarkerSet() => Directory.Exists(dontRunInitCodeMarker); /// /// Marks the IET project initialization to be done. /// static void SetInitialized() => File.CreateText(initFileMarkerPath).Close(); static bool IsLanguageInitialized() => SessionState.GetBool("EditorLanguageInitialized", false); static void SetLanguageInitialized() => SessionState.SetBool("EditorLanguageInitialized", true); // Replaces LastProjectPaths in window layouts used in tutorials so that e.g. // pre-saved Project window states work correctly. internal static void PrepareWindowLayouts() { AssetDatabase.FindAssets($"t:{typeof(TutorialContainer).FullName}") .Select(guid => AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid)).ProjectLayoutPath ) .Concat( AssetDatabase.FindAssets($"t:{typeof(Tutorial).FullName}") .Select(guid => AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid)).WindowLayoutPath ) ) .Where(StringExt.IsNotNullOrEmpty) .Distinct() .ToList() .ForEach(layoutPath => TutorialManager.PrepareWindowLayout(layoutPath)); } static SystemLanguage LoadPreviousEditorLanguage() => (SystemLanguage)EditorPrefs.GetInt("EditorLanguage", (int)SystemLanguage.English); static void SaveCurrentEditorLanguage() => EditorPrefs.SetInt("EditorLanguage", (int)LocalizationDatabaseProxy.currentEditorLanguage); /// /// Restart the Editor. /// internal static void RestartEditor() { // In older versions, calling EditorApplication.OpenProject() while having unsaved modifications // can cause us to get stuck in a dialog loop. This seems to be fixed in 2020.1 (and newer?). // As a workaround, ask for saving before starting to restart the Editor for real. However, // we get the dialog twice and it can cause issues if user chooses first "Don't save" and then tries // to "Cancel" in the second dialog. #if !UNITY_2020_1_OR_NEWER if (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) #endif { EditorApplication.OpenProject("."); } } } }