ReadMe.cs 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
  2. // FlexiMotion // https://kybernetik.com.au/flexi-motion // Copyright 2023-2024 Kybernetik //
  3. #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
  4. #if UNITY_EDITOR
  5. using System;
  6. using System.Collections.Generic;
  7. using System.IO;
  8. using UnityEditor;
  9. using UnityEditor.PackageManager.UI;
  10. using UnityEngine;
  11. // Shared File Last Modified: 2024-07-13
  12. namespace Animancer.Editor
  13. // namespace FlexiMotion.Editor
  14. {
  15. /// <summary>[Editor-Only] A welcome screen for an asset.</summary>
  16. /// https://kybernetik.com.au/animancer/api/Animancer.Editor/ReadMe
  17. /// https://kybernetik.com.au/flexi-motion/api/FlexiMotion.Editor/ReadMe
  18. ///
  19. public abstract class ReadMe : ScriptableObject
  20. {
  21. /************************************************************************************************************************/
  22. #region Fields and Properties
  23. /************************************************************************************************************************/
  24. /// <summary>The release ID of the current version.</summary>
  25. public abstract int ReleaseNumber { get; }
  26. /// <summary>The display name of this product version.</summary>
  27. public abstract string VersionName { get; }
  28. /// <summary>The key used to save the release number.</summary>
  29. public abstract string PrefKey { get; }
  30. /// <summary>An introductory explanation of this asset.</summary>
  31. public virtual string Introduction => null;
  32. /// <summary>The base name of this product (without any "Lite", "Pro", "Demo", etc.).</summary>
  33. public abstract string BaseProductName { get; }
  34. /// <summary>The name of this product.</summary>
  35. public virtual string ProductName => BaseProductName;
  36. /// <summary>The display name for the samples section.</summary>
  37. public virtual string SamplesLabel => "Samples";
  38. /// <summary>The URL for the documentation.</summary>
  39. public abstract string DocumentationURL { get; }
  40. /// <summary>The URL for the change log of this version.</summary>
  41. public abstract string ChangeLogURL { get; }
  42. /// <summary>The URL for the sample documentation.</summary>
  43. public abstract string SamplesURL { get; }
  44. /// <summary>The URL to check for the latest version.</summary>
  45. public virtual string UpdateURL => null;
  46. /************************************************************************************************************************/
  47. /// <summary>
  48. /// The <see cref="ReadMe"/> file name ends with the <see cref="VersionName"/> to detect if the user imported
  49. /// this version without deleting a previous version.
  50. /// </summary>
  51. /// <remarks>
  52. /// When Unity's package importer sees an existing file with the same GUID as one in the package, it will
  53. /// overwrite that file but not move or rename it if the name has changed. So it will leave the file there with
  54. /// the old version name.
  55. /// </remarks>
  56. private bool HasCorrectName => name.EndsWith(VersionName);
  57. /************************************************************************************************************************/
  58. /// <summary>Sections to be displayed below the samples.</summary>
  59. public LinkSection[] LinkSections { get; set; }
  60. /// <summary>Extra sections to be displayed with the samples.</summary>
  61. public LinkSection[] ExtraSamples { get; set; }
  62. /************************************************************************************************************************/
  63. /// <summary>Creates a new <see cref="ReadMe"/> and sets the <see cref="LinkSections"/>.</summary>
  64. public ReadMe(params LinkSection[] linkSections)
  65. {
  66. LinkSections = linkSections;
  67. _CheckForUpdatesKey = $"{PrefKey}.{nameof(CheckForUpdates)}";
  68. }
  69. /************************************************************************************************************************/
  70. /// <summary>A heading with a link to be displayed in the Inspector.</summary>
  71. public class LinkSection
  72. {
  73. /************************************************************************************************************************/
  74. /// <summary>The main label.</summary>
  75. public readonly string Heading;
  76. /// <summary>A short description to be displayed near the <see cref="Heading"/>.</summary>
  77. public readonly string Description;
  78. /// <summary>A link that can be opened by clicking the <see cref="Heading"/>.</summary>
  79. public readonly string URL;
  80. /// <summary>An optional user-friendly version of the <see cref="URL"/>.</summary>
  81. public readonly string DisplayURL;
  82. /************************************************************************************************************************/
  83. /// <summary>Creates a new <see cref="LinkSection"/>.</summary>
  84. public LinkSection(string heading, string description, string url, string displayURL = null)
  85. {
  86. Heading = heading;
  87. Description = description;
  88. URL = url;
  89. DisplayURL = displayURL;
  90. }
  91. /************************************************************************************************************************/
  92. }
  93. /************************************************************************************************************************/
  94. /// <summary>Returns a <c>mailto</c> link.</summary>
  95. public static string GetEmailURL(string address, string subject)
  96. => $"mailto:{address}?subject={subject.Replace(" ", "%20")}";
  97. /************************************************************************************************************************/
  98. #endregion
  99. /************************************************************************************************************************/
  100. #region Show On Startup and Check for Updates
  101. /************************************************************************************************************************/
  102. private const string PrefPrefix = nameof(ReadMe) + ".";
  103. [SerializeField] private bool _DontShowOnStartup;
  104. [NonSerialized] private string _CheckForUpdatesKey;
  105. [NonSerialized] private bool _NewVersionAvailable;
  106. [NonSerialized] private string _UpdateCheckFailureMessage;
  107. [NonSerialized] private string _LatestVersionName;
  108. [NonSerialized] private string _LatestVersionChangeLogURL;
  109. [NonSerialized] private int _LatestVersionNumber;
  110. #if UNITY_WEB_REQUEST
  111. [NonSerialized] private bool _CheckedForUpdates;
  112. #endif
  113. private bool CheckForUpdates
  114. {
  115. get => EditorPrefs.GetBool(_CheckForUpdatesKey, true);
  116. set => EditorPrefs.SetBool(_CheckForUpdatesKey, value);
  117. }
  118. /************************************************************************************************************************/
  119. private static readonly Dictionary<Type, IDisposable>
  120. TypeToUpdateCheck = new();
  121. static ReadMe()
  122. {
  123. AssemblyReloadEvents.beforeAssemblyReload += () =>
  124. {
  125. foreach (var webRequest in TypeToUpdateCheck.Values)
  126. webRequest.Dispose();
  127. TypeToUpdateCheck.Clear();
  128. };
  129. }
  130. /************************************************************************************************************************/
  131. /// <summary>Automatically checks for updates and selects a <see cref="ReadMe"/> on startup.</summary>
  132. [InitializeOnLoadMethod]
  133. private static void ShowReadMe()
  134. {
  135. EditorApplication.delayCall += () =>
  136. {
  137. var instances = FindInstances(out var autoSelect);
  138. for (int i = 0; i < instances.Count; i++)
  139. instances[i].StartCheckForUpdates();
  140. // Delay the call again to ensure that the Project window actually shows the selection.
  141. if (autoSelect != null)
  142. EditorApplication.delayCall += () =>
  143. Selection.activeObject = autoSelect;
  144. };
  145. }
  146. /************************************************************************************************************************/
  147. /// <summary>
  148. /// Finds the most recently modified <see cref="ReadMe"/> asset with <see cref="_DontShowOnStartup"/> disabled.
  149. /// </summary>
  150. private static List<ReadMe> FindInstances(out ReadMe autoSelect)
  151. {
  152. var instances = new List<ReadMe>();
  153. DateTime latestWriteTime = default;
  154. autoSelect = null;
  155. string autoSelectGUID = null;
  156. var guids = AssetDatabase.FindAssets($"t:{nameof(ReadMe)}");
  157. for (int i = 0; i < guids.Length; i++)
  158. {
  159. var guid = guids[i];
  160. var assetPath = AssetDatabase.GUIDToAssetPath(guid);
  161. var asset = AssetDatabase.LoadAssetAtPath<ReadMe>(assetPath);
  162. if (asset == null)
  163. continue;
  164. instances.Add(asset);
  165. if (asset._DontShowOnStartup && asset.HasCorrectName)
  166. continue;
  167. // Check if already shown since opening the Unity Editor.
  168. if (SessionState.GetBool(PrefPrefix + guid, false))
  169. continue;
  170. var writeTime = File.GetLastWriteTimeUtc(assetPath);
  171. if (latestWriteTime < writeTime)
  172. {
  173. latestWriteTime = writeTime;
  174. autoSelect = asset;
  175. autoSelectGUID = guid;
  176. }
  177. }
  178. if (autoSelectGUID != null)
  179. SessionState.SetBool(PrefPrefix + autoSelectGUID, true);
  180. return instances;
  181. }
  182. /************************************************************************************************************************/
  183. /// <summary>Called after this object is loaded.</summary>
  184. protected virtual void OnEnable()
  185. {
  186. var name = GetType().FullName;
  187. var updateText = SessionState.GetString(PrefPrefix + name, "");
  188. OnUpdateCheckComplete(updateText, false);
  189. }
  190. /************************************************************************************************************************/
  191. private void StartCheckForUpdates()
  192. {
  193. #if UNITY_WEB_REQUEST
  194. if (!CheckForUpdates ||
  195. _CheckedForUpdates)
  196. return;
  197. var type = GetType();
  198. if (TypeToUpdateCheck.ContainsKey(type))
  199. return;
  200. var url = UpdateURL;
  201. if (string.IsNullOrEmpty(url))
  202. return;
  203. _CheckedForUpdates = true;
  204. var webRequest = UnityEngine.Networking.UnityWebRequest.Get(url);
  205. TypeToUpdateCheck.Add(type, webRequest);
  206. webRequest.SendWebRequest().completed += _ =>
  207. {
  208. var name = type.FullName;
  209. if (webRequest.result == UnityEngine.Networking.UnityWebRequest.Result.Success)
  210. {
  211. var text = webRequest.downloadHandler.text;
  212. OnUpdateCheckComplete(text, true);
  213. SessionState.SetString(PrefPrefix + name, text);
  214. }
  215. else
  216. {
  217. _UpdateCheckFailureMessage = $"Update check failed: {webRequest.error}.";
  218. SessionState.SetString(PrefPrefix + name, "");
  219. }
  220. TypeToUpdateCheck.Remove(type);
  221. webRequest.Dispose();
  222. };
  223. #endif
  224. }
  225. /************************************************************************************************************************/
  226. private void OnUpdateCheckComplete(string text, bool log)
  227. {
  228. #if UNITY_WEB_REQUEST
  229. if (string.IsNullOrEmpty(text))
  230. return;
  231. _CheckedForUpdates = true;
  232. var lines = text.Split('\n');
  233. if (lines.Length < 3)
  234. {
  235. _UpdateCheckFailureMessage = "Update check failed: text is malformed:\n" + text;
  236. return;
  237. }
  238. int.TryParse(lines[0], out _LatestVersionNumber);
  239. _LatestVersionName = lines[1].Trim();
  240. _LatestVersionChangeLogURL = $"{DocumentationURL}/{lines[2].Trim()}";
  241. if (ReleaseNumber >= _LatestVersionNumber)
  242. return;
  243. _NewVersionAvailable = true;
  244. if (log)
  245. Debug.Log($"{_LatestVersionName} is now available." +
  246. $"\n• Change Log: {_LatestVersionChangeLogURL}" +
  247. $"\n• This check can be disabled in the Read Me asset's Inspector.",
  248. this);
  249. Selection.activeObject = this;
  250. #endif
  251. }
  252. #endregion
  253. /************************************************************************************************************************/
  254. #region Custom Editor
  255. /************************************************************************************************************************/
  256. /// <summary>[Editor-Only] A custom Inspector for <see cref="ReadMe"/>.</summary>
  257. [CustomEditor(typeof(ReadMe), editorForChildClasses: true)]
  258. public class Editor : UnityEditor.Editor
  259. {
  260. /************************************************************************************************************************/
  261. private static readonly GUIContent
  262. GUIContent = new();
  263. private static GUIContent TempContent(string text, string tooltip = null)
  264. {
  265. GUIContent.text = text;
  266. GUIContent.tooltip = tooltip;
  267. return GUIContent;
  268. }
  269. /************************************************************************************************************************/
  270. [NonSerialized] private ReadMe _Target;
  271. [NonSerialized] private Texture2D _Icon;
  272. [NonSerialized] private string _ReleaseNumberPrefKey;
  273. [NonSerialized] private int _PreviousVersion;
  274. [NonSerialized] private IEnumerable<Sample> _Samples;
  275. [NonSerialized] private string _Title;
  276. [NonSerialized] private SerializedProperty _DontShowOnStartupProperty;
  277. /// <summary>The <see cref="ReadMe"/> being edited.</summary>
  278. public ReadMe Target => _Target;
  279. /************************************************************************************************************************/
  280. /// <summary>Don't use any margins.</summary>
  281. public override bool UseDefaultMargins() => false;
  282. /************************************************************************************************************************/
  283. protected virtual void OnEnable()
  284. {
  285. _Target = (ReadMe)target;
  286. _Icon = AssetPreview.GetMiniThumbnail(target);
  287. _ReleaseNumberPrefKey = _Target.PrefKey + "." + nameof(_Target.ReleaseNumber);
  288. _PreviousVersion = PlayerPrefs.GetInt(_ReleaseNumberPrefKey, -1);
  289. _Title = $"{_Target.ProductName}\n{_Target.VersionName}";
  290. _DontShowOnStartupProperty = serializedObject.FindProperty(nameof(_DontShowOnStartup));
  291. if (!string.IsNullOrEmpty(_Target.SamplesLabel))
  292. {
  293. var assetPath = AssetDatabase.GetAssetPath(_Target);
  294. var package = UnityEditor.PackageManager.PackageInfo.FindForAssetPath(assetPath);
  295. if (package != null)
  296. {
  297. try
  298. {
  299. _Samples = Sample.FindByPackage(package.name, "");
  300. }
  301. catch { }// Unity sometimes throws an exception here. Not sure why.
  302. }
  303. }
  304. }
  305. /************************************************************************************************************************/
  306. protected override void OnHeaderGUI()
  307. {
  308. GUILayout.BeginHorizontal(Styles.TitleArea);
  309. {
  310. var title = TempContent(_Title);
  311. var iconWidth = Styles.Title.CalcHeight(title, EditorGUIUtility.currentViewWidth);
  312. GUILayout.Label(_Icon, GUILayout.Width(iconWidth), GUILayout.Height(iconWidth));
  313. GUILayout.Label(title, Styles.Title);
  314. }
  315. GUILayout.EndHorizontal();
  316. }
  317. /************************************************************************************************************************/
  318. /// <inheritdoc/>
  319. public override void OnInspectorGUI()
  320. {
  321. serializedObject.Update();
  322. DoIntroduction();
  323. DoSpace();
  324. DoWarnings();
  325. DoNewVersionDetails();
  326. DoCheckForUpdates();
  327. DoShowOnStartup();
  328. DoSpace();
  329. DoIntroductionBlock();
  330. DoSpace();
  331. DoSampleBlock();
  332. DoSpace();
  333. DoSupportBlock();
  334. DoSpace();
  335. DoCheckForUpdates();
  336. DoShowOnStartup();
  337. serializedObject.ApplyModifiedProperties();
  338. }
  339. /************************************************************************************************************************/
  340. /// <summary>Draws a GUI space 20% of the height of a standard line.</summary>
  341. protected static void DoSpace()
  342. => GUILayout.Space(EditorGUIUtility.singleLineHeight * 0.2f);
  343. /************************************************************************************************************************/
  344. /// <summary>Draws the <see cref="ReadMe.Introduction"/> if it isn't <c>null</c>.</summary>
  345. protected virtual void DoIntroduction()
  346. {
  347. var introduction = _Target.Introduction;
  348. if (introduction == null)
  349. return;
  350. DoSpace();
  351. GUILayout.Label(introduction, EditorStyles.wordWrappedLabel);
  352. }
  353. /************************************************************************************************************************/
  354. /// <summary>Draws a message indicating whether a new version is available.</summary>
  355. protected virtual void DoNewVersionDetails()
  356. {
  357. if (_Target._UpdateCheckFailureMessage != null)
  358. {
  359. EditorGUILayout.HelpBox(_Target._UpdateCheckFailureMessage, MessageType.Info);
  360. return;
  361. }
  362. if (_Target._LatestVersionName == null ||
  363. _Target._LatestVersionChangeLogURL == null)
  364. return;
  365. var message = _Target._NewVersionAvailable
  366. ? $"{_Target._LatestVersionName} is now available.\nClick here to view the Change Log."
  367. : $"{_Target.BaseProductName} is up to date.";
  368. EditorGUILayout.HelpBox(message, MessageType.Info);
  369. if (TryUseClickEventInLastRect())
  370. Application.OpenURL(_Target._LatestVersionChangeLogURL);
  371. }
  372. /************************************************************************************************************************/
  373. /// <summary>Draws a toggle to disable automatic update checks.</summary>
  374. protected virtual void DoCheckForUpdates()
  375. {
  376. #if UNITY_WEB_REQUEST
  377. if (string.IsNullOrEmpty(_Target.UpdateURL))
  378. return;
  379. var area = GUILayoutUtility.GetRect(0, EditorGUIUtility.singleLineHeight);
  380. area.xMin += EditorGUIUtility.singleLineHeight * 0.2f;
  381. EditorGUI.BeginChangeCheck();
  382. var value = GUI.Toggle(area, _Target.CheckForUpdates, "Check For Updates");
  383. if (EditorGUI.EndChangeCheck())
  384. {
  385. _Target.CheckForUpdates = value;
  386. if (value)
  387. _Target.StartCheckForUpdates();
  388. }
  389. #endif
  390. }
  391. /************************************************************************************************************************/
  392. /// <summary>Draws a toggle to disable automatically selecting the <see cref="ReadMe"/> on startup.</summary>
  393. protected virtual void DoShowOnStartup()
  394. {
  395. var area = GUILayoutUtility.GetRect(0, EditorGUIUtility.singleLineHeight);
  396. area.xMin += EditorGUIUtility.singleLineHeight * 0.2f;
  397. var content = TempContent(_DontShowOnStartupProperty.displayName, _DontShowOnStartupProperty.tooltip);
  398. var label = EditorGUI.BeginProperty(area, content, _DontShowOnStartupProperty);
  399. EditorGUI.BeginChangeCheck();
  400. var value = _DontShowOnStartupProperty.boolValue;
  401. value = GUI.Toggle(area, value, label);
  402. if (EditorGUI.EndChangeCheck())
  403. {
  404. _DontShowOnStartupProperty.boolValue = value;
  405. if (value)
  406. PlayerPrefs.SetInt(_ReleaseNumberPrefKey, _Target.ReleaseNumber);
  407. }
  408. EditorGUI.EndProperty();
  409. }
  410. /************************************************************************************************************************/
  411. /// <summary>Draws warnings about deleting older versions of the product.</summary>
  412. protected virtual void DoWarnings()
  413. {
  414. MessageType messageType;
  415. if (!_Target.HasCorrectName)
  416. {
  417. messageType = MessageType.Error;
  418. }
  419. else if (_PreviousVersion >= 0 && _PreviousVersion < _Target.ReleaseNumber)
  420. {
  421. messageType = MessageType.Warning;
  422. }
  423. else return;
  424. // Upgraded from any older version.
  425. DoSpace();
  426. var directory = AssetDatabase.GetAssetPath(_Target);
  427. if (string.IsNullOrEmpty(directory))
  428. return;
  429. directory = Path.GetDirectoryName(directory);
  430. var productName = _Target.ProductName;
  431. string versionWarning;
  432. if (messageType == MessageType.Error)
  433. {
  434. versionWarning =
  435. $"You must fully delete any old version of {productName} before importing a new version." +
  436. $"\n1. Check the Upgrade Guide in the Change Log." +
  437. $"\n2. Click here to delete '{directory}'." +
  438. $"\n3. Import {productName} again.";
  439. }
  440. else
  441. {
  442. versionWarning =
  443. $"You must fully delete any old version of {productName} before importing a new version." +
  444. $"\n1. Ignore this message if you have already deleted the old version." +
  445. $"\n2. Check the Upgrade Guide in the Change Log." +
  446. $"\n3. Click here to delete '{directory}'." +
  447. $"\n4. Import {productName} again.";
  448. }
  449. EditorGUILayout.HelpBox(versionWarning, messageType);
  450. CheckDeleteDirectory(directory);
  451. DoSpace();
  452. }
  453. /************************************************************************************************************************/
  454. /// <summary>Asks if the user wants to delete the `directory` and does so if they confirm.</summary>
  455. private void CheckDeleteDirectory(string directory)
  456. {
  457. if (!TryUseClickEventInLastRect())
  458. return;
  459. var name = _Target.ProductName;
  460. if (!AssetDatabase.IsValidFolder(directory))
  461. {
  462. Debug.Log($"{directory} doesn't exist." +
  463. $" You must have moved {name} somewhere else so you will need to delete it manually.", this);
  464. return;
  465. }
  466. if (!EditorUtility.DisplayDialog($"Delete {name}? ",
  467. $"Would you like to delete {directory}?\n\nYou will then need to reimport {name} manually.",
  468. "Delete", "Cancel"))
  469. return;
  470. AssetDatabase.DeleteAsset(directory);
  471. }
  472. /************************************************************************************************************************/
  473. /// <summary>
  474. /// Returns true and uses the current event if it is <see cref="EventType.MouseUp"/> inside the specified
  475. /// `area`.
  476. /// </summary>
  477. public static bool TryUseClickEvent(Rect area, int button = -1)
  478. {
  479. var currentEvent = Event.current;
  480. if (currentEvent.type != EventType.MouseUp ||
  481. (button >= 0 && currentEvent.button != button) ||
  482. !area.Contains(currentEvent.mousePosition))
  483. return false;
  484. GUI.changed = true;
  485. GUIUtility.hotControl = 0;
  486. currentEvent.Use();
  487. if (currentEvent.button == 2)
  488. GUIUtility.keyboardControl = 0;
  489. return true;
  490. }
  491. /// <summary>
  492. /// Returns true and uses the current event if it is <see cref="EventType.MouseUp"/> inside the last GUI Layout
  493. /// <see cref="Rect"/> that was drawn.
  494. /// </summary>
  495. public static bool TryUseClickEventInLastRect(int button = -1)
  496. => TryUseClickEvent(GUILayoutUtility.GetLastRect(), button);
  497. /************************************************************************************************************************/
  498. protected virtual void DoIntroductionBlock()
  499. {
  500. GUILayout.BeginVertical(Styles.Block);
  501. DoHeadingLink("Documentation", null, _Target.DocumentationURL);
  502. DoSpace();
  503. DoHeadingLink("Change Log", null, _Target.ChangeLogURL, fontSize: GUI.skin.label.fontSize);
  504. GUILayout.EndVertical();
  505. }
  506. /************************************************************************************************************************/
  507. protected virtual void DoSampleBlock()
  508. {
  509. var label = _Target.SamplesLabel;
  510. if (string.IsNullOrEmpty(label))
  511. return;
  512. GUILayout.BeginVertical(Styles.Block);
  513. DoHeadingLink(label, null, _Target.SamplesURL);
  514. if (_Samples != null)
  515. {
  516. foreach (var sample in _Samples)
  517. {
  518. if (sample.isImported)
  519. {
  520. try
  521. {
  522. var path = Path.GetRelativePath(Environment.CurrentDirectory, sample.importPath);
  523. var folder = AssetDatabase.LoadAssetAtPath<DefaultAsset>(path);
  524. using (new EditorGUI.DisabledScope(true))
  525. EditorGUILayout.ObjectField(GUIContent.none, folder, typeof(DefaultAsset), false);
  526. }
  527. catch (Exception exception)
  528. {
  529. if (GUILayout.Button($"{sample.description}: {exception.GetType().Name}"))
  530. Debug.LogException(exception);
  531. }
  532. }
  533. else
  534. {
  535. EditorGUILayout.LabelField(sample.displayName, "Not Imported");
  536. }
  537. }
  538. var buttonContent = TempContent(
  539. "Open Package Manager",
  540. "Samples can be imported via the Samples tab in the Package Manager" +
  541. "\n\nIt's generally recommended to delete any samples after you're done with them");
  542. if (GUILayout.Button(buttonContent))
  543. Window.Open("Animancer");
  544. }
  545. DoExtraSamples();
  546. GUILayout.EndVertical();
  547. }
  548. /************************************************************************************************************************/
  549. protected virtual void DoExtraSamples()
  550. {
  551. if (_Target.ExtraSamples == null)
  552. return;
  553. for (int i = 0; i < _Target.ExtraSamples.Length; i++)
  554. {
  555. if (i > 0)
  556. DoSpace();
  557. var section = _Target.ExtraSamples[i];
  558. DoHeadingLink(
  559. section.Heading,
  560. section.Description,
  561. section.URL,
  562. section.DisplayURL,
  563. GUI.skin.label.fontSize);
  564. }
  565. }
  566. /************************************************************************************************************************/
  567. protected virtual void DoSupportBlock()
  568. {
  569. GUILayout.BeginVertical(Styles.Block);
  570. for (int i = 0; i < _Target.LinkSections.Length; i++)
  571. {
  572. if (i > 0)
  573. DoSpace();
  574. var section = _Target.LinkSections[i];
  575. DoHeadingLink(
  576. section.Heading,
  577. section.Description,
  578. section.URL,
  579. section.DisplayURL);
  580. }
  581. GUILayout.EndVertical();
  582. }
  583. /************************************************************************************************************************/
  584. /// <summary>Draws a headding which acts as a button to open a URL.</summary>
  585. public static void DoHeadingLink(
  586. string heading,
  587. string description,
  588. string url,
  589. string displayURL = null,
  590. int fontSize = 22)
  591. {
  592. // Heading.
  593. var style = url == null
  594. ? Styles.HeaderLabel
  595. : Styles.HeaderLink;
  596. var area = DoLinkButton(heading, url, style, fontSize);
  597. // Description.
  598. area.y += EditorGUIUtility.standardVerticalSpacing;
  599. var urlHeight = Styles.URL.fontSize + Styles.URL.margin.vertical;
  600. area.height -= urlHeight;
  601. if (description != null)
  602. GUI.Label(area, description, Styles.Description);
  603. // URL.
  604. area.y += area.height;
  605. area.height = urlHeight;
  606. displayURL ??= url;
  607. if (displayURL != null)
  608. {
  609. var content = TempContent(displayURL, "Click to copy this link to the clipboard");
  610. if (GUI.Button(area, content, Styles.URL))
  611. {
  612. GUIUtility.systemCopyBuffer = displayURL;
  613. Debug.Log($"Copied '{displayURL}' to the clipboard.");
  614. }
  615. EditorGUIUtility.AddCursorRect(area, MouseCursor.Text);
  616. }
  617. }
  618. /************************************************************************************************************************/
  619. /// <summary>Draws a button to open a URL.</summary>
  620. public static Rect DoLinkButton(string text, string url, GUIStyle style, int fontSize = 22)
  621. {
  622. var content = TempContent(text, url);
  623. style.fontSize = fontSize;
  624. var size = style.CalcSize(content);
  625. var area = GUILayoutUtility.GetRect(0, size.y);
  626. var linkArea = new Rect(area.x, area.y, size.x, area.height);
  627. area.xMin += size.x;
  628. if (url == null)
  629. {
  630. GUI.Label(linkArea, content, style);
  631. }
  632. else
  633. {
  634. if (GUI.Button(linkArea, content, style))
  635. Application.OpenURL(url);
  636. EditorGUIUtility.AddCursorRect(linkArea, MouseCursor.Link);
  637. DrawLine(
  638. new(linkArea.xMin, linkArea.yMax),
  639. new(linkArea.xMax, linkArea.yMax),
  640. style.normal.textColor);
  641. }
  642. return area;
  643. }
  644. /************************************************************************************************************************/
  645. /// <summary>Draws a line between the `start` and `end` using the `color`.</summary>
  646. public static void DrawLine(Vector2 start, Vector2 end, Color color)
  647. {
  648. var previousColor = Handles.color;
  649. Handles.BeginGUI();
  650. Handles.color = color;
  651. Handles.DrawLine(start, end);
  652. Handles.color = previousColor;
  653. Handles.EndGUI();
  654. }
  655. /************************************************************************************************************************/
  656. /// <summary>Various <see cref="GUIStyle"/>s used by the <see cref="Editor"/>.</summary>
  657. protected static class Styles
  658. {
  659. /************************************************************************************************************************/
  660. public static readonly GUIStyle TitleArea = "In BigTitle";
  661. public static readonly GUIStyle Title = new(GUI.skin.label)
  662. {
  663. fontSize = 26,
  664. };
  665. public static readonly GUIStyle Block = GUI.skin.box;
  666. public static readonly GUIStyle HeaderLabel = new(GUI.skin.label)
  667. {
  668. stretchWidth = false,
  669. };
  670. public static readonly GUIStyle HeaderLink = new(HeaderLabel);
  671. public static readonly GUIStyle Description = new(GUI.skin.label)
  672. {
  673. alignment = TextAnchor.LowerLeft,
  674. };
  675. public static readonly GUIStyle URL = new(GUI.skin.label)
  676. {
  677. fontSize = 9,
  678. alignment = TextAnchor.LowerLeft,
  679. };
  680. /************************************************************************************************************************/
  681. static Styles()
  682. {
  683. HeaderLink.normal.textColor = HeaderLink.hover.textColor =
  684. new Color32(0x00, 0x78, 0xDA, 0xFF);
  685. URL.normal.textColor = Color.Lerp(URL.normal.textColor, Color.grey, 0.8f);
  686. }
  687. /************************************************************************************************************************/
  688. }
  689. /************************************************************************************************************************/
  690. }
  691. /************************************************************************************************************************/
  692. #endregion
  693. /************************************************************************************************************************/
  694. }
  695. }
  696. #endif