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(".");
}
}
}
}