// Animancer // https://kybernetik.com.au/animancer // Copyright 2022 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 { /// [Editor-Only] A welcome screen for . // [CreateAssetMenu(menuName = Strings.MenuPrefix + "Read Me", order = Strings.AssetMenuOrder)] [HelpURL(Strings.DocsURLs.APIDocumentation + "." + nameof(Animancer.Editor) + "/" + nameof(ReadMe))] public class ReadMe : ScriptableObject { /************************************************************************************************************************/ #region Fields and Properties /************************************************************************************************************************/ /// The release ID of the current version. /// /// [ 1] = v1.0: 2018-05-02. /// [ 2] = v1.1: 2018-05-29. /// [ 3] = v1.2: 2018-08-14. /// [ 4] = v1.3: 2018-09-12. /// [ 5] = v2.0: 2018-10-08. /// [ 6] = v3.0: 2019-05-27. /// [ 7] = v3.1: 2019-08-12. /// [ 8] = v4.0: 2020-01-28. /// [ 9] = v4.1: 2020-02-21. /// [10] = v4.2: 2020-03-02. /// [11] = v4.3: 2020-03-13. /// [12] = v4.4: 2020-03-27. /// [13] = v5.0: 2020-07-17. /// [14] = v5.1: 2020-07-27. /// [15] = v5.2: 2020-09-16. /// [16] = v5.3: 2020-10-06. /// [17] = v6.0: 2020-12-04. /// [18] = v6.1: 2021-04-13. /// [19] = v7.0: 2021-07-29. /// [20] = v7.1: 2021-08-13. /// [21] = v7.2: 2021-10-17. /// [22] = v7.3: 2022-07-03. /// protected virtual int ReleaseNumber => 22; /// The display name of this product version. protected virtual string VersionName => "v7.3"; /// The URL for the change log of this Animancer version. protected virtual string ChangeLogURL => Strings.DocsURLs.ChangeLogPrefix + "v7-3"; /// The key used to save the release number. protected virtual string ReleaseNumberPrefKey => nameof(Animancer) + "." + nameof(ReleaseNumber); /// The name of this product. protected virtual string ProductName => Strings.ProductName + " Pro"; /// The URL for the documentation. protected virtual string DocumentationURL => Strings.DocsURLs.Documentation; /// The URL for the example documentation. protected virtual string ExampleURL => Strings.DocsURLs.Examples; /// The URL for the Unity Forum thread. protected virtual string ForumURL => Strings.DocsURLs.Forum; /// The URL for the Github Issues page. protected virtual string IssuesURL => Strings.DocsURLs.Issues; /// The developer email address. protected virtual string DeveloperEmail => Strings.DocsURLs.DeveloperEmail; /************************************************************************************************************************/ /// /// The file name ends with the to detect if the user imported /// this version without deleting a previous version. /// /// 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. /// private bool HasCorrectName => name.EndsWith(VersionName); /************************************************************************************************************************/ [SerializeField] private DefaultAsset _ExamplesFolder; /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Show On Startup /************************************************************************************************************************/ [SerializeField] private bool _DontShowOnStartup; /// Should the system be prevented from automatically selecting this asset on startup? public bool DontShowOnStartup => _DontShowOnStartup && HasCorrectName; /************************************************************************************************************************/ /// Automatically selects a on startup. [InitializeOnLoadMethod] private static void ShowReadMe() { EditorApplication.delayCall += () => { var asset = FindReadMe(); if (asset != null)// Delay the call again to ensure that the Project window actually shows the selection. EditorApplication.delayCall += () => Selection.activeObject = asset; }; } /************************************************************************************************************************/ /// /// Finds the most recently modified asset with disabled. /// private static ReadMe FindReadMe() { DateTime latestWriteTime = default; ReadMe latestReadMe = null; string latestGUID = null; var guids = AssetDatabase.FindAssets($"t:{nameof(ReadMe)}"); for (int i = 0; i < guids.Length; i++) { var guid = guids[i]; if (SessionState.GetBool(guid, false)) continue; var assetPath = AssetDatabase.GUIDToAssetPath(guid); var asset = AssetDatabase.LoadAssetAtPath(assetPath); if (asset != null && !asset.DontShowOnStartup) { var writeTime = File.GetLastWriteTimeUtc(assetPath); if (latestWriteTime < writeTime) { latestWriteTime = writeTime; latestReadMe = asset; latestGUID = guid; } } } if (latestGUID != null) SessionState.SetBool(latestGUID, true); return latestReadMe; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Custom Editor /************************************************************************************************************************/ /// [Editor-Only] A custom Inspector for . [CustomEditor(typeof(ReadMe), editorForChildClasses: true)] public class Editor : UnityEditor.Editor { /************************************************************************************************************************/ [NonSerialized] private ReadMe _Target; [NonSerialized] private Texture2D _Icon; [NonSerialized] private int _PreviousVersion; [NonSerialized] private string _ExamplesDirectory; [NonSerialized] private List _Examples; [NonSerialized] private string _Title; [NonSerialized] private string _EmailLink; [NonSerialized] private SerializedProperty _DontShowOnStartupProperty; /************************************************************************************************************************/ /// Don't use any margins. public override bool UseDefaultMargins() => false; /************************************************************************************************************************/ protected virtual void OnEnable() { _Target = (ReadMe)target; _Icon = AssetPreview.GetMiniThumbnail(target); var key = _Target.ReleaseNumberPrefKey; _PreviousVersion = PlayerPrefs.GetInt(key, -1); if (_PreviousVersion < 0) _PreviousVersion = EditorPrefs.GetInt(key, -1);// Animancer v2.0 used EditorPrefs. _Examples = ExampleGroup.Gather(_Target._ExamplesFolder, out _ExamplesDirectory); _Title = $"{_Target.ProductName}\n{_Target.VersionName}"; _EmailLink = $"mailto:{_Target.DeveloperEmail}?subject={_Target.ProductName.Replace(" ", "%20")}"; _DontShowOnStartupProperty = serializedObject.FindProperty(nameof(_DontShowOnStartup)); } /************************************************************************************************************************/ protected override void OnHeaderGUI() { GUILayout.BeginHorizontal(Styles.TitleArea); { using (ObjectPool.Disposable.AcquireContent(out var label, _Title, null, false)) { var iconWidth = Styles.Title.CalcHeight(label, EditorGUIUtility.currentViewWidth); GUILayout.Label(_Icon, GUILayout.Width(iconWidth), GUILayout.Height(iconWidth)); GUILayout.Label(label, Styles.Title); } } GUILayout.EndHorizontal(); } /************************************************************************************************************************/ public override void OnInspectorGUI() { serializedObject.Update(); DoSpace(); DoWarnings(); DoShowOnStartup(); DoSpace(); DoIntroductionBlock(); DoSpace(); DoExampleBlock(); DoSpace(); DoSupportBlock(); DoSpace(); DoShowOnStartup(); serializedObject.ApplyModifiedProperties(); } /************************************************************************************************************************/ protected static void DoSpace() => GUILayout.Space(AnimancerGUI.LineHeight * 0.2f); /************************************************************************************************************************/ private void DoShowOnStartup() { var area = AnimancerGUI.LayoutSingleLineRect(); area.xMin += AnimancerGUI.LineHeight * 0.2f; using (ObjectPool.Disposable.AcquireContent(out var content, _DontShowOnStartupProperty, false)) { var label = EditorGUI.BeginProperty(area, content, _DontShowOnStartupProperty); EditorGUI.BeginChangeCheck(); var value = _DontShowOnStartupProperty.boolValue; value = GUI.Toggle(area, value, label); if (EditorGUI.EndChangeCheck()) { _DontShowOnStartupProperty.boolValue = value; if (value) PlayerPrefs.SetInt(_Target.ReleaseNumberPrefKey, _Target.ReleaseNumber); } EditorGUI.EndProperty(); } } /************************************************************************************************************************/ private void DoWarnings() { MessageType messageType; if (!_Target.HasCorrectName) { messageType = MessageType.Error; } else if (_PreviousVersion >= 0 && _PreviousVersion < _Target.ReleaseNumber) { messageType = MessageType.Warning; } else return; // Upgraded from any older version. DoSpace(); var directory = AssetDatabase.GetAssetPath(_Target); if (string.IsNullOrEmpty(directory)) return; directory = Path.GetDirectoryName(directory); var productName = _Target.ProductName; string versionWarning; if (messageType == MessageType.Error) { versionWarning = $"You must fully delete any old version of {productName} before importing a new version." + $"\n1. Check the Upgrade Guide in the Change Log." + $"\n2. Click here to delete '{directory}'." + $"\n3. Import {productName} again."; } else { versionWarning = $"You must fully delete any old version of {productName} before importing a new version." + $"\n1. Ignore this message if you have already deleted the old version." + $"\n2. Check the Upgrade Guide in the Change Log." + $"\n3. Click here to delete '{directory}'." + $"\n4. Import {productName} again."; } EditorGUILayout.HelpBox(versionWarning, messageType); CheckDeleteDirectory(directory); DoSpace(); } /************************************************************************************************************************/ /// Asks if the user wants to delete the `directory` and does so if they confirm. private void CheckDeleteDirectory(string directory) { if (!AnimancerGUI.TryUseClickEventInLastRect()) return; var name = _Target.ProductName; if (!AssetDatabase.IsValidFolder(directory)) { Debug.Log($"{directory} doesn't exist." + $" You must have moved {name} somewhere else so you will need to delete it manually.", this); return; } if (!EditorUtility.DisplayDialog($"Delete {name}? ", $"Would you like to delete {directory}?\n\nYou will then need to reimport {name} manually.", "Delete", "Cancel")) return; AssetDatabase.DeleteAsset(directory); } /************************************************************************************************************************/ protected virtual void DoIntroductionBlock() { GUILayout.BeginVertical(Styles.Block); DoHeadingLink("Documentation", null, _Target.DocumentationURL); DoSpace(); DoHeadingLink("Change Log", null, _Target.ChangeLogURL); GUILayout.EndVertical(); } /************************************************************************************************************************/ protected virtual void DoExampleBlock() { GUILayout.BeginVertical(Styles.Block); DoHeadingLink("Examples", null, _Target.ExampleURL); if (_Target._ExamplesFolder != null) { EditorGUILayout.ObjectField(_ExamplesDirectory, _Target._ExamplesFolder, typeof(SceneAsset), false); ExampleGroup.DoExampleGUI(_Examples); } DoExtraExamples(); GUILayout.EndVertical(); } protected virtual void DoExtraExamples() { DoHeadingLink("Platformer Game Kit", null, "https://kybernetik.com.au/platformer", null, GUI.skin.label.fontSize); } /************************************************************************************************************************/ protected virtual void DoSupportBlock() { GUILayout.BeginVertical(Styles.Block); DoHeadingLink("Forum", "for general discussions, feedback, and news", _Target.ForumURL); DoSpace(); DoHeadingLink("Issues", "for questions, suggestions, and bug reports", _Target.IssuesURL); DoSpace(); DoHeadingLink("Email", "for anything private", _EmailLink, _Target.DeveloperEmail); GUILayout.EndVertical(); } /************************************************************************************************************************/ protected void DoHeadingLink(string heading, string description, string url, string displayURL = null, int fontSize = 22) { // Heading. var area = DoLinkButton(heading, url, url == null ? Styles.HeaderLabel : Styles.HeaderLink, fontSize); // Description. area.y += AnimancerGUI.StandardSpacing; var urlHeight = Styles.URL.fontSize + Styles.URL.margin.vertical; area.height -= urlHeight; if (description != null) GUI.Label(area, description, Styles.Description); // URL. area.y += area.height; area.height = urlHeight; if (displayURL == null) displayURL = url; if (displayURL != null) { using (ObjectPool.Disposable.AcquireContent(out var label, displayURL, "Click to copy this link to the clipboard", false)) { if (GUI.Button(area, label, Styles.URL)) { GUIUtility.systemCopyBuffer = displayURL; Debug.Log($"Copied '{displayURL}' to the clipboard.", this); } EditorGUIUtility.AddCursorRect(area, MouseCursor.Text); } } } /************************************************************************************************************************/ protected Rect DoLinkButton(string text, string url, GUIStyle style, int fontSize = 22) { using (ObjectPool.Disposable.AcquireContent(out var label, text, url, false)) { style.fontSize = fontSize; var size = style.CalcSize(label); var area = GUILayoutUtility.GetRect(0, size.y); var linkArea = AnimancerGUI.StealFromLeft(ref area, size.x); if (url == null) { GUI.Label(linkArea, label, style); } else { if (GUI.Button(linkArea, label, style)) Application.OpenURL(url); EditorGUIUtility.AddCursorRect(linkArea, MouseCursor.Link); DrawLine( new Vector2(linkArea.xMin, linkArea.yMax), new Vector2(linkArea.xMax, linkArea.yMax), style.normal.textColor); } return area; } } /************************************************************************************************************************/ /// Draws a line between the `start` and `end` using the `color`. public static void DrawLine(Vector2 start, Vector2 end, Color color) { var previousColor = Handles.color; Handles.BeginGUI(); Handles.color = color; Handles.DrawLine(start, end); Handles.color = previousColor; Handles.EndGUI(); } /************************************************************************************************************************/ /// Various s used by the . protected static class Styles { /************************************************************************************************************************/ public static readonly GUIStyle TitleArea = "In BigTitle"; public static readonly GUIStyle Title = new GUIStyle(GUI.skin.label) { fontSize = 26, }; public static readonly GUIStyle Block = GUI.skin.box; public static readonly GUIStyle HeaderLabel = new GUIStyle(GUI.skin.label) { stretchWidth = false, }; public static readonly GUIStyle HeaderLink = new GUIStyle(HeaderLabel); public static readonly GUIStyle Description = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.LowerLeft, }; public static readonly GUIStyle URL = new GUIStyle(GUI.skin.label) { fontSize = 9, alignment = TextAnchor.LowerLeft, }; /************************************************************************************************************************/ static Styles() { HeaderLink.normal.textColor = HeaderLink.hover.textColor = new Color32(0x00, 0x78, 0xDA, 0xFF); URL.normal.textColor = Color.Lerp(URL.normal.textColor, Color.grey, 0.8f); } /************************************************************************************************************************/ } /************************************************************************************************************************/ /// A group of example scenes. private class ExampleGroup { /************************************************************************************************************************/ /// The name of this group. public readonly string Name; /// The scenes in this group. public readonly List Scenes = new List(); /// The folder paths of each of the . public readonly List Directories = new List(); /// Indicates whether this group should show its contents in the GUI. private bool _IsExpanded; /************************************************************************************************************************/ public static List 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(); for (int i = 0; i < directories.Length; i++) { var group = Gather(examplesDirectory, directories[i]); if (group != null) examples.Add(group); } if (examples.Count == 0) { var group = Gather(examplesDirectory, examplesDirectory); if (group != null) examples.Add(group); } examplesDirectory = Path.GetDirectoryName(examplesDirectory); return examples; } /************************************************************************************************************************/ public static ExampleGroup Gather(string rootDirectory, string directory) { var files = Directory.GetFiles(directory, "*.unity", SearchOption.AllDirectories); List scenes = null; for (int j = 0; j < files.Length; j++) { var scene = AssetDatabase.LoadAssetAtPath(files[j]); if (scene != null) { if (scenes == null) scenes = new List(); scenes.Add(scene); } } if (scenes == null) return null; return new ExampleGroup(rootDirectory, directory, scenes); } /************************************************************************************************************************/ public ExampleGroup(string rootDirectory, string directory, List scenes) { var start = rootDirectory.Length + 1; Name = start < directory.Length ? directory.Substring(start, directory.Length - start) : Path.GetFileName(directory); 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 examples) { if (examples == null) return; for (int i = 0; i < examples.Count; i++) examples[i].DoExampleGUI(); } public void DoExampleGUI() { EditorGUI.indentLevel++; using (ObjectPool.Disposable.AcquireContent(out var label, Name, null, false)) _IsExpanded = EditorGUILayout.Foldout(_IsExpanded, label, true); if (_IsExpanded) { EditorGUI.indentLevel++; for (int i = 0; i < Scenes.Count; i++) EditorGUILayout.ObjectField(Directories[i], Scenes[i], typeof(SceneAsset), false); EditorGUI.indentLevel--; } EditorGUI.indentLevel--; } /************************************************************************************************************************/ } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } } #endif