using System; using UnityEditor; using UnityEngine; using UnityEngine.Serialization; using UnityEngine.Events; namespace Unity.Tutorials.Core.Editor { /// /// A generic event for signaling changes in a tutorial container. /// Parameters: sender. /// [Serializable] public class TutorialContainerEvent : UnityEvent { } /// /// A tutorial container is a collection of tutorial content, and is used to access the actual tutorials in the project. /// /// /// A tutorial container can be two things: /// 1. Tutorial project (null Parent): a root container which is the entry point for tutorial content in the project. /// 2. Tutorial category (non-null Parent): a set of tutorials that are a part of some other container /// public class TutorialContainer : ScriptableObject { /// /// Raised when any TutorialContainer is modified. /// /// /// Raised before Modified event. /// public static TutorialContainerEvent TutorialContainerModified = new TutorialContainerEvent(); /// /// Raised when any field of this container is modified. /// /// /// If 'this' container is parented, we consider modifications to 'this' container also to be modifications of the parent. /// public TutorialContainerEvent Modified; /// /// By setting another container as a parent, this container becomes a tutorial category in the parent container. /// [Tooltip("By setting another container as a parent, this container becomes a tutorial category in the parent container.")] public TutorialContainer ParentContainer; /// /// This value determines the position of a container / container card within a container, if this container is shown as a card. /// [Tooltip("This value determines the position of a container / container card within a container, if this container is shown as a card.")] public int OrderInView; /// /// Background texture for the card/header. /// [FormerlySerializedAs("HeaderBackground")] public Texture2D BackgroundImage; /// /// Title shown in the card/header. /// [Tooltip("Title shown in the card/header.")] public LocalizableString Title; /// /// Subtitle shown in the container card and header area. /// [Tooltip("Subtitle shown in the card/header.")] public LocalizableString Subtitle; /// /// Used as the tooltip for the container card. /// [Tooltip("Used as the tooltip for the card.")] public LocalizableString Description; /// /// Can be used to override or disable (the default behavior) the default project layout specified by the Tutorial Framework. /// [Tooltip("Can be used to override or disable (the default behavior) the default project layout specified by the Tutorial Framework.")] public UnityEngine.Object ProjectLayout; /// /// Sections (tutorial or link card) of this container. /// #if UNITY_2020_2_OR_NEWER [NonReorderable] // reordering freely would be problematic and is disallowed for now #endif public Section[] Sections = {}; /// /// Returns the path for the ProjectLayout, relative to the project folder, /// or a default tutorial layout path if ProjectLayout not specified. /// public string ProjectLayoutPath => ProjectLayout != null ? AssetDatabase.GetAssetPath(ProjectLayout) : k_DefaultLayoutPath; // The default layout used when a project is started for the first time, if project layout is used. internal static readonly string k_DefaultLayoutPath = "Packages/com.unity.learn.iet-framework/Editor/DefaultAssets/DefaultLayout.wlt"; /// /// A section/card for starting a tutorial or opening a web page. /// [Serializable] public class Section { /// /// This value determines the position of a section / section card within a container. /// [Tooltip("This value determines the position of a section / section card within a container.")] public int OrderInView; /// /// Title of the card. /// public LocalizableString Heading; /// /// Description of the card. /// public LocalizableString Text; /// /// Used as content type metadata for external references/URLs /// [Tooltip("Used as content type metadata for external references/URLs"), FormerlySerializedAs("LinkText")] public string Metadata; /// /// The URL of this section. /// Setting the URL will take precedence and make the card act as a link card instead of a tutorial card /// [Tooltip("Setting the URL will take precedence and make the card act as a link card instead of a tutorial card")] public string Url; /// /// Image for the card. /// public Texture2D Image; /// /// The tutorial this container contains /// public Tutorial Tutorial; /// /// Does this represent a tutorial? /// public bool IsTutorial => Url.IsNullOrEmpty(); /// /// The ID of the represented tutorial, if any /// public string TutorialId => Tutorial?.LessonId.AsEmptyIfNull(); /// /// Starts the tutorial of the section /// public void StartTutorial() { TutorialManager.Instance.StartTutorial(Tutorial); } /// /// Opens the URL Of the section, if any /// public void OpenUrl() { // TODO by making a static OpenUrl(string url) utility function we can easily track rich text hyperlink clicks also TutorialEditorUtils.OpenUrl(Url); AnalyticsHelper.SendExternalReferenceEvent(Url, Heading.Untranslated, Metadata, Tutorial?.LessonId); } // TODO Managing tutorials' completion states feels something that Tutorial and TutorialManager classes should be responsible of. /// /// Has the tutorial already been completed? /// internal bool TutorialCompleted { get; set; } internal string SessionStateKey => $"Unity.Tutorials.Core.Editor.lesson{TutorialId}"; /// /// Loads the state of the section from SessionState. /// /// returns true if the state was found from EditorPrefs internal bool LoadState() { const string nonexisting = "NONEXISTING"; var state = SessionState.GetString(SessionStateKey, nonexisting); if (state == "") { TutorialCompleted = false; } else if (state == "Finished") { TutorialCompleted = true; } return state != nonexisting; } /// /// Saves the state of the section from SessionState. /// internal void SaveState() { SessionState.SetString(SessionStateKey, TutorialCompleted ? "Finished" : ""); } } void OnValidate() { Title = POFileUtils.SanitizeString(Title); Subtitle = POFileUtils.SanitizeString(Subtitle); Description = POFileUtils.SanitizeString(Description); foreach (var section in Sections) { section.Heading = POFileUtils.SanitizeString(section.Heading); section.Text = POFileUtils.SanitizeString(section.Text); } Array.Sort(Sections, (x, y) => x.OrderInView.CompareTo(y.OrderInView)); } /// /// Loads the tutorial project layout /// public void LoadTutorialProjectLayout() { TutorialManager.LoadWindowLayoutWorkingCopy(ProjectLayoutPath); } /// /// Raises the Modified events for this asset. /// public void RaiseModified() { TutorialContainerModified?.Invoke(this); Modified?.Invoke(this); } } }