// Animancer // Copyright 2020 Kybernetik // #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value. #if UNITY_EDITOR using System; using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; namespace Animancer.Editor { /// <summary>[Editor-Only] A welcome screen for Animancer.</summary> /// <remarks>Automatic selection is handled by <c>ShowReadMeOnStartup</c>.</remarks> //[CreateAssetMenu(menuName = Strings.MenuItemPrefix + "Read Me", order = Strings.AssetMenuOrder + 4)] public sealed class ReadMe : ScriptableObject { /************************************************************************************************************************/ /// <summary> /// The release ID of this Animancer version. /// </summary> /// <remarks> /// <list type="bullet"> /// <item>[1] = v1.0: 2018-05-02.</item> /// <item>[2] = v1.1: 2018-05-29.</item> /// <item>[3] = v1.2: 2018-08-14.</item> /// <item>[4] = v1.3: 2018-09-12.</item> /// <item>[5] = v2.0: 2018-10-08.</item> /// <item>[6] = v3.0: 2019-05-27.</item> /// <item>[7] = v3.1: 2019-08-12.</item> /// <item>[8] = v4.0: 2020-01-28.</item> /// </list> /// </remarks> private const int ReleaseNumber = 8; /// <summary>The display name of this Animancer version.</summary> private const string VersionName = "v4.0"; /// <summary>The end of the URL for the change log of this Animancer version.</summary> private const string ChangeLogSuffix = "v4-0"; /// <summary>The key used to save the release number.</summary> private const string ReleaseNumberPrefKey = "Animancer.ReleaseNumber"; /************************************************************************************************************************/ [SerializeField] private Texture2D _Icon; [SerializeField] private DefaultAsset _ExamplesFolder; [SerializeField] private bool _DontShowOnStartup; /************************************************************************************************************************/ /// <summary>If true, <c>ShowReadMeOnStartup</c> will not automatically select this asset.</summary> public bool DontShowOnStartup { get { return _DontShowOnStartup && HasCorrectName; } } /************************************************************************************************************************/ /// <summary> /// The <see cref="ReadMe"/> file name ends with the <see cref="VersionName"/> to detect if the user imported /// this version without deleting a previous version. /// <para></para> /// When Unity's package importer sees an existing file with the same GUID as one in the package, it will /// overwrite that file but not move or rename it if the name has changed. So it will leave the file there with /// the old version name. /// </summary> private bool HasCorrectName { get { return name.EndsWith(VersionName); } } /************************************************************************************************************************/ [CustomEditor(typeof(ReadMe))] private sealed class Editor : UnityEditor.Editor { /************************************************************************************************************************/ /// <summary>The <see cref="Editor.target"/> cast to <see cref="ReadMe"/>.</summary> public ReadMe Target { get; private set; } /// <summary>The <see cref="ReleaseNumber"/> from when "Don't show this again" was last ticked.</summary> [NonSerialized] private int _PreviousVersion; /// <summary>The file path of the Examples folder.</summary> [NonSerialized] private string _ExamplesDirectory; /// <summary>The details of all example scenes.</summary> [NonSerialized] private List<ExampleGroup> _Examples; /************************************************************************************************************************/ private void OnEnable() { Target = (ReadMe)target; _PreviousVersion = PlayerPrefs.GetInt(ReleaseNumberPrefKey, -1); if (_PreviousVersion < 0) _PreviousVersion = EditorPrefs.GetInt(ReleaseNumberPrefKey, -1);// Animancer v2.0 used EditorPrefs. _Examples = ExampleGroup.Gather(Target._ExamplesFolder, out _ExamplesDirectory); } /************************************************************************************************************************/ protected override void OnHeaderGUI() { GUILayout.BeginHorizontal("In BigTitle"); { const string Title = "Animancer Pro\n" + VersionName; var title = AnimancerGUI.TempContent(Title, null, false); var iconWidth = GUIStyles.Title.CalcHeight(title, EditorGUIUtility.currentViewWidth); GUILayout.Label(Target._Icon, GUILayout.Width(iconWidth), GUILayout.Height(iconWidth)); GUILayout.Label(title, GUIStyles.Title); } GUILayout.EndHorizontal(); } /************************************************************************************************************************/ public override void OnInspectorGUI() { DoWarnings(); DoShowOnStartup(); DoSpace(); GUILayout.BeginVertical(GUI.skin.box); DoHeadingLink("Documentation", null, Strings.DocumentationURL); DoSpace(); DoHeadingLink("Change Log", null, Strings.DocsURLs.ChangeLogPrefix + ChangeLogSuffix); GUILayout.EndVertical(); DoSpace(); GUILayout.BeginVertical(GUI.skin.box); DoHeadingLink("Examples", null, Strings.DocumentationURL + "/docs/examples"); if (Target._ExamplesFolder != null) { EditorGUILayout.ObjectField(_ExamplesDirectory, Target._ExamplesFolder, typeof(SceneAsset), false); ExampleGroup.DoExampleGUI(_Examples); } GUILayout.EndVertical(); DoSpace(); GUILayout.BeginVertical(GUI.skin.box); DoHeadingLink("Forum", "for general discussion, feedback, and news", "https://forum.unity.com/threads/566452"); DoSpace(); DoHeadingLink("Issues", "for questions, suggestions, and bug reports", "https://github.com/KybernetikGames/animancer/issues"); DoSpace(); DoHeadingLink("Email", "for anything private", "mailto:" + Strings.DeveloperEmail + "?subject=Animancer", Strings.DeveloperEmail); GUILayout.EndVertical(); DoSpace(); DoShowOnStartup(); } /************************************************************************************************************************/ private void DoShowOnStartup() { EditorGUI.BeginChangeCheck(); Target._DontShowOnStartup = GUILayout.Toggle(Target._DontShowOnStartup, "Don't show this Read Me on startup"); if (EditorGUI.EndChangeCheck()) { EditorUtility.SetDirty(Target); if (Target._DontShowOnStartup) PlayerPrefs.SetInt(ReleaseNumberPrefKey, ReleaseNumber); } } /************************************************************************************************************************/ private void DoSpace() { GUILayout.Space(AnimancerGUI.LineHeight * 0.2f); } /************************************************************************************************************************/ private void DoWarnings() { MessageType messageType; if (!Target.HasCorrectName) { messageType = MessageType.Error; } else if (_PreviousVersion >= 0 && _PreviousVersion < ReleaseNumber) { messageType = MessageType.Warning; } else return; // Upgraded from any older version. DoSpace(); var directory = AssetDatabase.GetAssetPath(Target); directory = Path.GetDirectoryName(directory); string versionWarning; if (messageType == MessageType.Error) { versionWarning = "You must fully delete any old version of Animancer before importing a new version." + "\n\nClick here to delete '" + directory + "' then you will need to import Animancer again."; } else { versionWarning = "You must fully delete any old version of Animancer before importing a new version." + "\n\nYou can ignore this message if you have already done so." + " Otherwise click here to delete '" + directory + "' then you will need to import Animancer again."; } EditorGUILayout.HelpBox(versionWarning, messageType); CheckDeleteAnimancer(directory); // Upgraded from before v2.0. if (_PreviousVersion < 4) { DoSpace(); EditorGUILayout.HelpBox("It seems you have just upgraded from an earlier version of Animancer" + " (before v2.0) so you will need to restart Unity before you can use it.", MessageType.Warning); } DoSpace(); } /************************************************************************************************************************/ /// <summary> /// Asks if the user wants to delete the root Animancer folder and does so if they confirm. /// </summary> private void CheckDeleteAnimancer(string directory) { if (!AnimancerGUI.TryUseClickEventInLastRect()) return; if (!AssetDatabase.IsValidFolder(directory)) { Debug.Log(directory + " doesn't exist." + " You must have moved Animancer somewhere else so you will need to delete it manually."); return; } if (!EditorUtility.DisplayDialog("Delete Animancer?", "Would you like to delete " + directory + "?" + "\n\nYou will then need to reimport Animancer manually.", "Delete", "Cancel")) return; AssetDatabase.DeleteAsset(directory); } /************************************************************************************************************************/ private bool DoHeadingLink(string heading, string description, string url, string displayURL = null) { if (DoLinkLabel(heading, description)) Application.OpenURL(url); bool clicked; if (displayURL == null) displayURL = url; var content = AnimancerGUI.TempContent(displayURL, "Click to copy this link to the clipboard", false); var area = AnimancerGUI.LayoutSingleLineRect(); if (GUI.Button(area, content, GUIStyles.URL)) { GUIUtility.systemCopyBuffer = displayURL; Debug.Log("Copied '" + displayURL + "' to the clipboard."); clicked = true; } else clicked = false; EditorGUIUtility.AddCursorRect(GUILayoutUtility.GetLastRect(), MouseCursor.Text); return clicked; } /************************************************************************************************************************/ private bool DoLinkLabel(string label, string description) { var headingStyle = GUIStyles.Heading; var content = AnimancerGUI.TempContent(label, null, false); var size = headingStyle.CalcSize(content); var labelArea = GUILayoutUtility.GetRect(0, size.y); var buttonArea = AnimancerGUI.StealFromLeft(ref labelArea, size.x); if (description != null) GUI.Label(labelArea, description, GUIStyles.Description); Handles.BeginGUI(); Handles.color = headingStyle.normal.textColor; Handles.DrawLine(new Vector3(buttonArea.xMin, buttonArea.yMax), new Vector3(buttonArea.xMax, buttonArea.yMax)); Handles.color = Color.white; Handles.EndGUI(); EditorGUIUtility.AddCursorRect(buttonArea, MouseCursor.Link); return GUI.Button(buttonArea, content, headingStyle); } /************************************************************************************************************************/ private sealed class ExampleGroup { /************************************************************************************************************************/ /// <summary>The name of this group.</summary> public readonly string Name; /// <summary>The scenes in the <see cref="Name"/>.</summary> public readonly List<SceneAsset> Scenes = new List<SceneAsset>(); /// <summary>The folder paths of each of the <see cref="Scenes"/>.</summary> public readonly List<string> Directories = new List<string>(); /// <summary>Indicates whether this group should show its contents in the GUI.</summary> private bool _IsExpanded; /************************************************************************************************************************/ public static List<ExampleGroup> Gather(DefaultAsset rootDirectoryAsset, out string examplesDirectory) { if (rootDirectoryAsset == null) { examplesDirectory = null; return null; } examplesDirectory = AssetDatabase.GetAssetPath(rootDirectoryAsset); if (string.IsNullOrEmpty(examplesDirectory)) return null; var directories = Directory.GetDirectories(examplesDirectory); var examples = new List<ExampleGroup>(); List<SceneAsset> scenes = null; for (int i = 0; i < directories.Length; i++) { var directory = directories[i]; var files = Directory.GetFiles(directory, "*.unity", SearchOption.AllDirectories); for (int j = 0; j < files.Length; j++) { var scene = AssetDatabase.LoadAssetAtPath<SceneAsset>(files[j]); if (scene != null) { if (scenes == null) scenes = new List<SceneAsset>(); scenes.Add(scene); } } if (scenes != null) { examples.Add(new ExampleGroup(examplesDirectory, directory, scenes)); scenes = null; } } examplesDirectory = Path.GetDirectoryName(examplesDirectory); return examples; } /************************************************************************************************************************/ public ExampleGroup(string rootDirectory, string directory, List<SceneAsset> scenes) { var start = rootDirectory.Length + 1; Name = directory.Substring(start, directory.Length - start); Scenes = scenes; start = directory.Length + 1; for (int i = 0; i < scenes.Count; i++) { directory = AssetDatabase.GetAssetPath(scenes[i]); directory = directory.Substring(start, directory.Length - start); directory = Path.GetDirectoryName(directory); Directories.Add(directory); } } /************************************************************************************************************************/ public static void DoExampleGUI(List<ExampleGroup> examples) { if (examples == null) return; for (int i = 0; i < examples.Count; i++) examples[i].DoExampleGUI(); } public void DoExampleGUI() { EditorGUI.indentLevel++; _IsExpanded = EditorGUILayout.Foldout(_IsExpanded, AnimancerGUI.TempContent(Name, null, false), true); if (_IsExpanded) { for (int i = 0; i < Scenes.Count; i++) { EditorGUI.indentLevel++; EditorGUILayout.ObjectField(Directories[i], Scenes[i], typeof(SceneAsset), false); EditorGUI.indentLevel--; } } EditorGUI.indentLevel--; } /************************************************************************************************************************/ } /************************************************************************************************************************/ private static class GUIStyles { /************************************************************************************************************************/ public static readonly GUIStyle Title = new GUIStyle(GUI.skin.label) { fontSize = 26, }; public static readonly GUIStyle Heading = new GUIStyle(GUI.skin.label) { fontSize = 18, stretchWidth = false, }; public static readonly GUIStyle Description = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.LowerLeft, }; public static readonly GUIStyle URL = new GUIStyle(GUI.skin.label); /************************************************************************************************************************/ static GUIStyles() { Heading.normal.textColor = Heading.hover.textColor = new Color32(0x00, 0x78, 0xDA, 0xFF); URL.fontSize = Mathf.CeilToInt(URL.fontSize * 0.8f); URL.normal.textColor = Color.Lerp(URL.normal.textColor, Color.grey, 0.75f); } /************************************************************************************************************************/ } /************************************************************************************************************************/ } /************************************************************************************************************************/ } } #endif