123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911 |
- // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
- // FlexiMotion // https://kybernetik.com.au/flexi-motion // Copyright 2023-2024 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 UnityEditor.PackageManager.UI;
- using UnityEngine;
- // Shared File Last Modified: 2024-07-13
- namespace Animancer.Editor
- // namespace FlexiMotion.Editor
- {
- /// <summary>[Editor-Only] A welcome screen for an asset.</summary>
- /// https://kybernetik.com.au/animancer/api/Animancer.Editor/ReadMe
- /// https://kybernetik.com.au/flexi-motion/api/FlexiMotion.Editor/ReadMe
- ///
- public abstract class ReadMe : ScriptableObject
- {
- /************************************************************************************************************************/
- #region Fields and Properties
- /************************************************************************************************************************/
- /// <summary>The release ID of the current version.</summary>
- public abstract int ReleaseNumber { get; }
- /// <summary>The display name of this product version.</summary>
- public abstract string VersionName { get; }
- /// <summary>The key used to save the release number.</summary>
- public abstract string PrefKey { get; }
- /// <summary>An introductory explanation of this asset.</summary>
- public virtual string Introduction => null;
- /// <summary>The base name of this product (without any "Lite", "Pro", "Demo", etc.).</summary>
- public abstract string BaseProductName { get; }
- /// <summary>The name of this product.</summary>
- public virtual string ProductName => BaseProductName;
- /// <summary>The display name for the samples section.</summary>
- public virtual string SamplesLabel => "Samples";
- /// <summary>The URL for the documentation.</summary>
- public abstract string DocumentationURL { get; }
- /// <summary>The URL for the change log of this version.</summary>
- public abstract string ChangeLogURL { get; }
- /// <summary>The URL for the sample documentation.</summary>
- public abstract string SamplesURL { get; }
- /// <summary>The URL to check for the latest version.</summary>
- public virtual string UpdateURL => null;
- /************************************************************************************************************************/
- /// <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.
- /// </summary>
- /// <remarks>
- /// 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.
- /// </remarks>
- private bool HasCorrectName => name.EndsWith(VersionName);
- /************************************************************************************************************************/
- /// <summary>Sections to be displayed below the samples.</summary>
- public LinkSection[] LinkSections { get; set; }
- /// <summary>Extra sections to be displayed with the samples.</summary>
- public LinkSection[] ExtraSamples { get; set; }
- /************************************************************************************************************************/
- /// <summary>Creates a new <see cref="ReadMe"/> and sets the <see cref="LinkSections"/>.</summary>
- public ReadMe(params LinkSection[] linkSections)
- {
- LinkSections = linkSections;
- _CheckForUpdatesKey = $"{PrefKey}.{nameof(CheckForUpdates)}";
- }
- /************************************************************************************************************************/
- /// <summary>A heading with a link to be displayed in the Inspector.</summary>
- public class LinkSection
- {
- /************************************************************************************************************************/
- /// <summary>The main label.</summary>
- public readonly string Heading;
- /// <summary>A short description to be displayed near the <see cref="Heading"/>.</summary>
- public readonly string Description;
- /// <summary>A link that can be opened by clicking the <see cref="Heading"/>.</summary>
- public readonly string URL;
- /// <summary>An optional user-friendly version of the <see cref="URL"/>.</summary>
- public readonly string DisplayURL;
- /************************************************************************************************************************/
- /// <summary>Creates a new <see cref="LinkSection"/>.</summary>
- public LinkSection(string heading, string description, string url, string displayURL = null)
- {
- Heading = heading;
- Description = description;
- URL = url;
- DisplayURL = displayURL;
- }
- /************************************************************************************************************************/
- }
- /************************************************************************************************************************/
- /// <summary>Returns a <c>mailto</c> link.</summary>
- public static string GetEmailURL(string address, string subject)
- => $"mailto:{address}?subject={subject.Replace(" ", "%20")}";
- /************************************************************************************************************************/
- #endregion
- /************************************************************************************************************************/
- #region Show On Startup and Check for Updates
- /************************************************************************************************************************/
- private const string PrefPrefix = nameof(ReadMe) + ".";
- [SerializeField] private bool _DontShowOnStartup;
- [NonSerialized] private string _CheckForUpdatesKey;
- [NonSerialized] private bool _NewVersionAvailable;
- [NonSerialized] private string _UpdateCheckFailureMessage;
- [NonSerialized] private string _LatestVersionName;
- [NonSerialized] private string _LatestVersionChangeLogURL;
- [NonSerialized] private int _LatestVersionNumber;
- #if UNITY_WEB_REQUEST
- [NonSerialized] private bool _CheckedForUpdates;
- #endif
- private bool CheckForUpdates
- {
- get => EditorPrefs.GetBool(_CheckForUpdatesKey, true);
- set => EditorPrefs.SetBool(_CheckForUpdatesKey, value);
- }
- /************************************************************************************************************************/
- private static readonly Dictionary<Type, IDisposable>
- TypeToUpdateCheck = new();
- static ReadMe()
- {
- AssemblyReloadEvents.beforeAssemblyReload += () =>
- {
- foreach (var webRequest in TypeToUpdateCheck.Values)
- webRequest.Dispose();
- TypeToUpdateCheck.Clear();
- };
- }
- /************************************************************************************************************************/
- /// <summary>Automatically checks for updates and selects a <see cref="ReadMe"/> on startup.</summary>
- [InitializeOnLoadMethod]
- private static void ShowReadMe()
- {
- EditorApplication.delayCall += () =>
- {
- var instances = FindInstances(out var autoSelect);
- for (int i = 0; i < instances.Count; i++)
- instances[i].StartCheckForUpdates();
- // Delay the call again to ensure that the Project window actually shows the selection.
- if (autoSelect != null)
- EditorApplication.delayCall += () =>
- Selection.activeObject = autoSelect;
- };
- }
- /************************************************************************************************************************/
- /// <summary>
- /// Finds the most recently modified <see cref="ReadMe"/> asset with <see cref="_DontShowOnStartup"/> disabled.
- /// </summary>
- private static List<ReadMe> FindInstances(out ReadMe autoSelect)
- {
- var instances = new List<ReadMe>();
- DateTime latestWriteTime = default;
- autoSelect = null;
- string autoSelectGUID = null;
- var guids = AssetDatabase.FindAssets($"t:{nameof(ReadMe)}");
- for (int i = 0; i < guids.Length; i++)
- {
- var guid = guids[i];
- var assetPath = AssetDatabase.GUIDToAssetPath(guid);
- var asset = AssetDatabase.LoadAssetAtPath<ReadMe>(assetPath);
- if (asset == null)
- continue;
- instances.Add(asset);
- if (asset._DontShowOnStartup && asset.HasCorrectName)
- continue;
- // Check if already shown since opening the Unity Editor.
- if (SessionState.GetBool(PrefPrefix + guid, false))
- continue;
- var writeTime = File.GetLastWriteTimeUtc(assetPath);
- if (latestWriteTime < writeTime)
- {
- latestWriteTime = writeTime;
- autoSelect = asset;
- autoSelectGUID = guid;
- }
- }
- if (autoSelectGUID != null)
- SessionState.SetBool(PrefPrefix + autoSelectGUID, true);
- return instances;
- }
- /************************************************************************************************************************/
- /// <summary>Called after this object is loaded.</summary>
- protected virtual void OnEnable()
- {
- var name = GetType().FullName;
- var updateText = SessionState.GetString(PrefPrefix + name, "");
- OnUpdateCheckComplete(updateText, false);
- }
- /************************************************************************************************************************/
- private void StartCheckForUpdates()
- {
- #if UNITY_WEB_REQUEST
- if (!CheckForUpdates ||
- _CheckedForUpdates)
- return;
- var type = GetType();
- if (TypeToUpdateCheck.ContainsKey(type))
- return;
- var url = UpdateURL;
- if (string.IsNullOrEmpty(url))
- return;
- _CheckedForUpdates = true;
- var webRequest = UnityEngine.Networking.UnityWebRequest.Get(url);
- TypeToUpdateCheck.Add(type, webRequest);
- webRequest.SendWebRequest().completed += _ =>
- {
- var name = type.FullName;
- if (webRequest.result == UnityEngine.Networking.UnityWebRequest.Result.Success)
- {
- var text = webRequest.downloadHandler.text;
- OnUpdateCheckComplete(text, true);
- SessionState.SetString(PrefPrefix + name, text);
- }
- else
- {
- _UpdateCheckFailureMessage = $"Update check failed: {webRequest.error}.";
- SessionState.SetString(PrefPrefix + name, "");
- }
- TypeToUpdateCheck.Remove(type);
- webRequest.Dispose();
- };
- #endif
- }
- /************************************************************************************************************************/
- private void OnUpdateCheckComplete(string text, bool log)
- {
- #if UNITY_WEB_REQUEST
- if (string.IsNullOrEmpty(text))
- return;
- _CheckedForUpdates = true;
- var lines = text.Split('\n');
- if (lines.Length < 3)
- {
- _UpdateCheckFailureMessage = "Update check failed: text is malformed:\n" + text;
- return;
- }
- int.TryParse(lines[0], out _LatestVersionNumber);
- _LatestVersionName = lines[1].Trim();
- _LatestVersionChangeLogURL = $"{DocumentationURL}/{lines[2].Trim()}";
- if (ReleaseNumber >= _LatestVersionNumber)
- return;
- _NewVersionAvailable = true;
- if (log)
- Debug.Log($"{_LatestVersionName} is now available." +
- $"\n• Change Log: {_LatestVersionChangeLogURL}" +
- $"\n• This check can be disabled in the Read Me asset's Inspector.",
- this);
- Selection.activeObject = this;
- #endif
- }
- #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
- {
- /************************************************************************************************************************/
- private static readonly GUIContent
- GUIContent = new();
- private static GUIContent TempContent(string text, string tooltip = null)
- {
- GUIContent.text = text;
- GUIContent.tooltip = tooltip;
- return GUIContent;
- }
- /************************************************************************************************************************/
- [NonSerialized] private ReadMe _Target;
- [NonSerialized] private Texture2D _Icon;
- [NonSerialized] private string _ReleaseNumberPrefKey;
- [NonSerialized] private int _PreviousVersion;
- [NonSerialized] private IEnumerable<Sample> _Samples;
- [NonSerialized] private string _Title;
- [NonSerialized] private SerializedProperty _DontShowOnStartupProperty;
- /// <summary>The <see cref="ReadMe"/> being edited.</summary>
- public ReadMe Target => _Target;
- /************************************************************************************************************************/
- /// <summary>Don't use any margins.</summary>
- public override bool UseDefaultMargins() => false;
- /************************************************************************************************************************/
- protected virtual void OnEnable()
- {
- _Target = (ReadMe)target;
- _Icon = AssetPreview.GetMiniThumbnail(target);
- _ReleaseNumberPrefKey = _Target.PrefKey + "." + nameof(_Target.ReleaseNumber);
- _PreviousVersion = PlayerPrefs.GetInt(_ReleaseNumberPrefKey, -1);
- _Title = $"{_Target.ProductName}\n{_Target.VersionName}";
- _DontShowOnStartupProperty = serializedObject.FindProperty(nameof(_DontShowOnStartup));
- if (!string.IsNullOrEmpty(_Target.SamplesLabel))
- {
- var assetPath = AssetDatabase.GetAssetPath(_Target);
- var package = UnityEditor.PackageManager.PackageInfo.FindForAssetPath(assetPath);
- if (package != null)
- {
- try
- {
- _Samples = Sample.FindByPackage(package.name, "");
- }
- catch { }// Unity sometimes throws an exception here. Not sure why.
- }
- }
- }
- /************************************************************************************************************************/
- protected override void OnHeaderGUI()
- {
- GUILayout.BeginHorizontal(Styles.TitleArea);
- {
- var title = TempContent(_Title);
- var iconWidth = Styles.Title.CalcHeight(title, EditorGUIUtility.currentViewWidth);
- GUILayout.Label(_Icon, GUILayout.Width(iconWidth), GUILayout.Height(iconWidth));
- GUILayout.Label(title, Styles.Title);
- }
- GUILayout.EndHorizontal();
- }
- /************************************************************************************************************************/
- /// <inheritdoc/>
- public override void OnInspectorGUI()
- {
- serializedObject.Update();
- DoIntroduction();
- DoSpace();
- DoWarnings();
- DoNewVersionDetails();
- DoCheckForUpdates();
- DoShowOnStartup();
- DoSpace();
- DoIntroductionBlock();
- DoSpace();
- DoSampleBlock();
- DoSpace();
- DoSupportBlock();
- DoSpace();
- DoCheckForUpdates();
- DoShowOnStartup();
- serializedObject.ApplyModifiedProperties();
- }
- /************************************************************************************************************************/
- /// <summary>Draws a GUI space 20% of the height of a standard line.</summary>
- protected static void DoSpace()
- => GUILayout.Space(EditorGUIUtility.singleLineHeight * 0.2f);
- /************************************************************************************************************************/
- /// <summary>Draws the <see cref="ReadMe.Introduction"/> if it isn't <c>null</c>.</summary>
- protected virtual void DoIntroduction()
- {
- var introduction = _Target.Introduction;
- if (introduction == null)
- return;
- DoSpace();
- GUILayout.Label(introduction, EditorStyles.wordWrappedLabel);
- }
- /************************************************************************************************************************/
- /// <summary>Draws a message indicating whether a new version is available.</summary>
- protected virtual void DoNewVersionDetails()
- {
- if (_Target._UpdateCheckFailureMessage != null)
- {
- EditorGUILayout.HelpBox(_Target._UpdateCheckFailureMessage, MessageType.Info);
- return;
- }
- if (_Target._LatestVersionName == null ||
- _Target._LatestVersionChangeLogURL == null)
- return;
- var message = _Target._NewVersionAvailable
- ? $"{_Target._LatestVersionName} is now available.\nClick here to view the Change Log."
- : $"{_Target.BaseProductName} is up to date.";
- EditorGUILayout.HelpBox(message, MessageType.Info);
- if (TryUseClickEventInLastRect())
- Application.OpenURL(_Target._LatestVersionChangeLogURL);
- }
- /************************************************************************************************************************/
- /// <summary>Draws a toggle to disable automatic update checks.</summary>
- protected virtual void DoCheckForUpdates()
- {
- #if UNITY_WEB_REQUEST
- if (string.IsNullOrEmpty(_Target.UpdateURL))
- return;
- var area = GUILayoutUtility.GetRect(0, EditorGUIUtility.singleLineHeight);
- area.xMin += EditorGUIUtility.singleLineHeight * 0.2f;
- EditorGUI.BeginChangeCheck();
- var value = GUI.Toggle(area, _Target.CheckForUpdates, "Check For Updates");
- if (EditorGUI.EndChangeCheck())
- {
- _Target.CheckForUpdates = value;
- if (value)
- _Target.StartCheckForUpdates();
- }
- #endif
- }
- /************************************************************************************************************************/
- /// <summary>Draws a toggle to disable automatically selecting the <see cref="ReadMe"/> on startup.</summary>
- protected virtual void DoShowOnStartup()
- {
- var area = GUILayoutUtility.GetRect(0, EditorGUIUtility.singleLineHeight);
- area.xMin += EditorGUIUtility.singleLineHeight * 0.2f;
- var content = TempContent(_DontShowOnStartupProperty.displayName, _DontShowOnStartupProperty.tooltip);
- 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(_ReleaseNumberPrefKey, _Target.ReleaseNumber);
- }
- EditorGUI.EndProperty();
- }
- /************************************************************************************************************************/
- /// <summary>Draws warnings about deleting older versions of the product.</summary>
- protected virtual 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 (!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);
- }
- /************************************************************************************************************************/
- /// <summary>
- /// Returns true and uses the current event if it is <see cref="EventType.MouseUp"/> inside the specified
- /// `area`.
- /// </summary>
- public static bool TryUseClickEvent(Rect area, int button = -1)
- {
- var currentEvent = Event.current;
- if (currentEvent.type != EventType.MouseUp ||
- (button >= 0 && currentEvent.button != button) ||
- !area.Contains(currentEvent.mousePosition))
- return false;
- GUI.changed = true;
- GUIUtility.hotControl = 0;
- currentEvent.Use();
- if (currentEvent.button == 2)
- GUIUtility.keyboardControl = 0;
- return true;
- }
- /// <summary>
- /// Returns true and uses the current event if it is <see cref="EventType.MouseUp"/> inside the last GUI Layout
- /// <see cref="Rect"/> that was drawn.
- /// </summary>
- public static bool TryUseClickEventInLastRect(int button = -1)
- => TryUseClickEvent(GUILayoutUtility.GetLastRect(), button);
- /************************************************************************************************************************/
- protected virtual void DoIntroductionBlock()
- {
- GUILayout.BeginVertical(Styles.Block);
- DoHeadingLink("Documentation", null, _Target.DocumentationURL);
- DoSpace();
- DoHeadingLink("Change Log", null, _Target.ChangeLogURL, fontSize: GUI.skin.label.fontSize);
- GUILayout.EndVertical();
- }
- /************************************************************************************************************************/
- protected virtual void DoSampleBlock()
- {
- var label = _Target.SamplesLabel;
- if (string.IsNullOrEmpty(label))
- return;
- GUILayout.BeginVertical(Styles.Block);
- DoHeadingLink(label, null, _Target.SamplesURL);
- if (_Samples != null)
- {
- foreach (var sample in _Samples)
- {
- if (sample.isImported)
- {
- try
- {
- var path = Path.GetRelativePath(Environment.CurrentDirectory, sample.importPath);
- var folder = AssetDatabase.LoadAssetAtPath<DefaultAsset>(path);
- using (new EditorGUI.DisabledScope(true))
- EditorGUILayout.ObjectField(GUIContent.none, folder, typeof(DefaultAsset), false);
- }
- catch (Exception exception)
- {
- if (GUILayout.Button($"{sample.description}: {exception.GetType().Name}"))
- Debug.LogException(exception);
- }
- }
- else
- {
- EditorGUILayout.LabelField(sample.displayName, "Not Imported");
- }
- }
- var buttonContent = TempContent(
- "Open Package Manager",
- "Samples can be imported via the Samples tab in the Package Manager" +
- "\n\nIt's generally recommended to delete any samples after you're done with them");
- if (GUILayout.Button(buttonContent))
- Window.Open("Animancer");
- }
- DoExtraSamples();
- GUILayout.EndVertical();
- }
- /************************************************************************************************************************/
- protected virtual void DoExtraSamples()
- {
- if (_Target.ExtraSamples == null)
- return;
- for (int i = 0; i < _Target.ExtraSamples.Length; i++)
- {
- if (i > 0)
- DoSpace();
- var section = _Target.ExtraSamples[i];
- DoHeadingLink(
- section.Heading,
- section.Description,
- section.URL,
- section.DisplayURL,
- GUI.skin.label.fontSize);
- }
- }
- /************************************************************************************************************************/
- protected virtual void DoSupportBlock()
- {
- GUILayout.BeginVertical(Styles.Block);
- for (int i = 0; i < _Target.LinkSections.Length; i++)
- {
- if (i > 0)
- DoSpace();
- var section = _Target.LinkSections[i];
- DoHeadingLink(
- section.Heading,
- section.Description,
- section.URL,
- section.DisplayURL);
- }
- GUILayout.EndVertical();
- }
- /************************************************************************************************************************/
- /// <summary>Draws a headding which acts as a button to open a URL.</summary>
- public static void DoHeadingLink(
- string heading,
- string description,
- string url,
- string displayURL = null,
- int fontSize = 22)
- {
- // Heading.
- var style = url == null
- ? Styles.HeaderLabel
- : Styles.HeaderLink;
- var area = DoLinkButton(heading, url, style, fontSize);
- // Description.
- area.y += EditorGUIUtility.standardVerticalSpacing;
- 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;
- displayURL ??= url;
- if (displayURL != null)
- {
- var content = TempContent(displayURL, "Click to copy this link to the clipboard");
- if (GUI.Button(area, content, Styles.URL))
- {
- GUIUtility.systemCopyBuffer = displayURL;
- Debug.Log($"Copied '{displayURL}' to the clipboard.");
- }
- EditorGUIUtility.AddCursorRect(area, MouseCursor.Text);
- }
- }
- /************************************************************************************************************************/
- /// <summary>Draws a button to open a URL.</summary>
- public static Rect DoLinkButton(string text, string url, GUIStyle style, int fontSize = 22)
- {
- var content = TempContent(text, url);
- style.fontSize = fontSize;
- var size = style.CalcSize(content);
- var area = GUILayoutUtility.GetRect(0, size.y);
- var linkArea = new Rect(area.x, area.y, size.x, area.height);
- area.xMin += size.x;
- if (url == null)
- {
- GUI.Label(linkArea, content, style);
- }
- else
- {
- if (GUI.Button(linkArea, content, style))
- Application.OpenURL(url);
- EditorGUIUtility.AddCursorRect(linkArea, MouseCursor.Link);
- DrawLine(
- new(linkArea.xMin, linkArea.yMax),
- new(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(GUI.skin.label)
- {
- fontSize = 26,
- };
- public static readonly GUIStyle Block = GUI.skin.box;
- public static readonly GUIStyle HeaderLabel = new(GUI.skin.label)
- {
- stretchWidth = false,
- };
- public static readonly GUIStyle HeaderLink = new(HeaderLabel);
- public static readonly GUIStyle Description = new(GUI.skin.label)
- {
- alignment = TextAnchor.LowerLeft,
- };
- public static readonly GUIStyle URL = new(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);
- }
- /************************************************************************************************************************/
- }
- /************************************************************************************************************************/
- }
- /************************************************************************************************************************/
- #endregion
- /************************************************************************************************************************/
- }
- }
- #endif
|