123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699 |
- // 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
- {
- /// <summary>[Editor-Only] A welcome screen for <see cref="Animancer"/>.</summary>
- // [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
- /************************************************************************************************************************/
- /// <summary>The release ID of the current version.</summary>
- /// <example><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>
- /// <item>[ 9] = v4.1: 2020-02-21.</item>
- /// <item>[10] = v4.2: 2020-03-02.</item>
- /// <item>[11] = v4.3: 2020-03-13.</item>
- /// <item>[12] = v4.4: 2020-03-27.</item>
- /// <item>[13] = v5.0: 2020-07-17.</item>
- /// <item>[14] = v5.1: 2020-07-27.</item>
- /// <item>[15] = v5.2: 2020-09-16.</item>
- /// <item>[16] = v5.3: 2020-10-06.</item>
- /// <item>[17] = v6.0: 2020-12-04.</item>
- /// <item>[18] = v6.1: 2021-04-13.</item>
- /// <item>[19] = v7.0: 2021-07-29.</item>
- /// <item>[20] = v7.1: 2021-08-13.</item>
- /// <item>[21] = v7.2: 2021-10-17.</item>
- /// <item>[22] = v7.3: 2022-07-03.</item>
- /// </list></example>
- protected virtual int ReleaseNumber => 22;
- /// <summary>The display name of this product version.</summary>
- protected virtual string VersionName => "v7.3";
- /// <summary>The URL for the change log of this Animancer version.</summary>
- protected virtual string ChangeLogURL => Strings.DocsURLs.ChangeLogPrefix + "v7-3";
- /// <summary>The key used to save the release number.</summary>
- protected virtual string ReleaseNumberPrefKey => nameof(Animancer) + "." + nameof(ReleaseNumber);
- /// <summary>The name of this product.</summary>
- protected virtual string ProductName => Strings.ProductName + " Pro";
- /// <summary>The URL for the documentation.</summary>
- protected virtual string DocumentationURL => Strings.DocsURLs.Documentation;
- /// <summary>The URL for the example documentation.</summary>
- protected virtual string ExampleURL => Strings.DocsURLs.Examples;
- /// <summary>The URL for the Unity Forum thread.</summary>
- protected virtual string ForumURL => Strings.DocsURLs.Forum;
- /// <summary>The URL for the Github Issues page.</summary>
- protected virtual string IssuesURL => Strings.DocsURLs.Issues;
- /// <summary>The developer email address.</summary>
- protected virtual string DeveloperEmail => Strings.DocsURLs.DeveloperEmail;
- /************************************************************************************************************************/
- /// <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 => name.EndsWith(VersionName);
- /************************************************************************************************************************/
- [SerializeField]
- private DefaultAsset _ExamplesFolder;
- /************************************************************************************************************************/
- #endregion
- /************************************************************************************************************************/
- #region Show On Startup
- /************************************************************************************************************************/
- [SerializeField]
- private bool _DontShowOnStartup;
- /// <summary>Should the system be prevented from automatically selecting this asset on startup?</summary>
- public bool DontShowOnStartup => _DontShowOnStartup && HasCorrectName;
- /************************************************************************************************************************/
- /// <summary>Automatically selects a <see cref="ReadMe"/> on startup.</summary>
- [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;
- };
- }
- /************************************************************************************************************************/
- /// <summary>
- /// Finds the most recently modified <see cref="ReadMe"/> asset with <see cref="DontShowOnStartup"/> disabled.
- /// </summary>
- 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<ReadMe>(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
- /************************************************************************************************************************/
- /// <summary>[Editor-Only] A custom Inspector for <see cref="ReadMe"/>.</summary>
- [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<ExampleGroup> _Examples;
- [NonSerialized] private string _Title;
- [NonSerialized] private string _EmailLink;
- [NonSerialized] private SerializedProperty _DontShowOnStartupProperty;
- /************************************************************************************************************************/
- /// <summary>Don't use any margins.</summary>
- 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();
- }
- /************************************************************************************************************************/
- /// <summary>Asks if the user wants to delete the `directory` and does so if they confirm.</summary>
- 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;
- }
- }
- /************************************************************************************************************************/
- /// <summary>Draws a line between the `start` and `end` using the `color`.</summary>
- 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();
- }
- /************************************************************************************************************************/
- /// <summary>Various <see cref="GUIStyle"/>s used by the <see cref="Editor"/>.</summary>
- 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);
- }
- /************************************************************************************************************************/
- }
- /************************************************************************************************************************/
- /// <summary>A group of example scenes.</summary>
- private class ExampleGroup
- {
- /************************************************************************************************************************/
- /// <summary>The name of this group.</summary>
- public readonly string Name;
- /// <summary>The scenes in this group.</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>();
- 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<SceneAsset> scenes = null;
- 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)
- return null;
- return new ExampleGroup(rootDirectory, directory, scenes);
- }
- /************************************************************************************************************************/
- public ExampleGroup(string rootDirectory, string directory, List<SceneAsset> 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<ExampleGroup> 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
|