// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// [Editor-Only] Persistent settings used by Animancer.
///
/// This asset automatically creates itself when first accessed.
///
/// The default location is Packages/com.kybernetik.animancer/Code/Editor, but you can freely move it
/// (and the whole Animancer folder) anywhere in your project.
///
/// These settings can also be accessed via the Settings in the
/// (Window/Animation/Animancer Tools).
///
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerSettings
///
[AnimancerHelpUrl(typeof(AnimancerSettings))]
public class AnimancerSettings : ScriptableObject
{
/************************************************************************************************************************/
private static AnimancerSettings _Instance;
///
/// Loads an existing if there is one anywhere in your project.
/// Otherwise, creates a new one and saves it in the Assets folder.
///
public static AnimancerSettings Instance
{
get
{
if (_Instance != null)
return _Instance;
_Instance = AnimancerEditorUtilities.FindAssetOfType();
if (_Instance != null)
return _Instance;
_Instance = CreateInstance();
_Instance.name = "Animancer Settings";
_Instance.hideFlags = HideFlags.DontSaveInBuild;
var path = $"Assets/{_Instance.name}.asset";
path = AssetDatabase.GenerateUniqueAssetPath(path);
AssetDatabase.CreateAsset(_Instance, path);
return _Instance;
}
}
/************************************************************************************************************************/
/// Finds an existing instance of this asset anywhere in the project.
[InitializeOnLoadMethod]
private static void FindExistingInstance()
{
if (_Instance == null)
_Instance = AnimancerEditorUtilities.FindAssetOfType();
}
/************************************************************************************************************************/
private SerializedObject _SerializedObject;
/// The representing the .
public static SerializedObject SerializedObject
=> Instance._SerializedObject ?? (Instance._SerializedObject = new(Instance));
/************************************************************************************************************************/
private readonly List>
SerializedProperties = new();
private static SerializedProperty GetSerializedProperty(int index, string propertyPath)
{
while (index >= Instance.SerializedProperties.Count)
Instance.SerializedProperties.Add(null);
var properties = Instance.SerializedProperties[index];
properties ??= Instance.SerializedProperties[index] = new();
if (!properties.TryGetValue(propertyPath, out var property))
{
property = SerializedObject.FindProperty(propertyPath);
properties.Add(propertyPath, property);
}
return property;
}
/// Returns a relative to the data at the specified `index`.
public static SerializedProperty GetSerializedProperty(
int index,
ref string basePropertyPath,
string propertyPath)
{
if (string.IsNullOrEmpty(basePropertyPath))
basePropertyPath =
$"{nameof(_Data)}{Serialization.ArrayDataPrefix}{index}{Serialization.ArrayDataSuffix}";
if (string.IsNullOrEmpty(propertyPath))
propertyPath = basePropertyPath;
else
propertyPath = $"{basePropertyPath}.{propertyPath}";
return GetSerializedProperty(index, propertyPath);
}
/************************************************************************************************************************/
[SerializeReference]
private List _Data;
/// Returns a stored item of the specified type or creates a new one if necessary.
public static T GetOrCreateData()
where T : AnimancerSettingsGroup, new()
{
ref var data = ref Instance._Data;
data ??= new();
var index = AnimancerEditorUtilities.IndexOfType(Instance._Data, typeof(T));
if (index >= 0)
return (T)data[index];
var newT = new T();
newT.SetDataIndex(data.Count);
data.Add(newT);
SetDirty();
return newT;
}
/************************************************************************************************************************/
/// Calls on the .
public static new void SetDirty()
=> EditorUtility.SetDirty(_Instance);
/************************************************************************************************************************/
///
/// Ensures that there is an instance of each class derived from .
///
protected virtual void OnEnable()
{
AnimancerEditorUtilities.InstantiateDerivedTypes(ref _Data);
for (int i = 0; i < _Data.Count; i++)
_Data[i].SetDataIndex(i);
}
/************************************************************************************************************************/
/// A custom Inspector for .
[CustomEditor(typeof(AnimancerSettings), true), CanEditMultipleObjects]
public class Editor : UnityEditor.Editor
{
/************************************************************************************************************************/
[NonSerialized]
private SerializedProperty _Data;
/************************************************************************************************************************/
/// Called when this object is first loaded.
protected virtual void OnEnable()
{
_Data = serializedObject.FindProperty(nameof(AnimancerSettings._Data));
}
/************************************************************************************************************************/
///
public override void OnInspectorGUI()
{
DoInfoGUI();
DoOptionalWarningsGUI();
serializedObject.Update();
var count = _Data.arraySize;
for (int i = 0; i < count; i++)
DoDataGUI(_Data.GetArrayElementAtIndex(i), i);
serializedObject.ApplyModifiedProperties();
}
/************************************************************************************************************************/
///
/// If true, the next will skip drawing the info panel.
///
public static bool HideNextInfo { get; set; }
private void DoInfoGUI()
{
if (HideNextInfo)
{
HideNextInfo = false;
}
else
{
EditorGUILayout.HelpBox(
"Feel free to move this asset anywhere in your project." +
"\n\nIt should generally not be in the Animancer folder" +
" so that if you ever update Animancer you can delete that folder" +
" without losing these settings." +
"\n\nIf this asset is deleted, it will be automatically recreated" +
" with default values when something needs it.",
MessageType.Info);
}
}
/************************************************************************************************************************/
private void DoDataGUI(SerializedProperty property, int index)
{
if (property.managedReferenceValue is AnimancerSettingsGroup value)
{
DoHeading(value.DisplayName);
var first = true;
var depth = property.depth;
while (property.NextVisible(first) && property.depth > depth)
{
first = false;
EditorGUILayout.PropertyField(property, true);
}
}
else
{
EditorGUILayout.BeginHorizontal();
DoHeading("Missing Type");
if (GUILayout.Button("X", AnimancerGUI.MiniButtonStyle))
{
var count = _Data.arraySize;
_Data.DeleteArrayElementAtIndex(index);
if (count == _Data.arraySize)
_Data.DeleteArrayElementAtIndex(index);
serializedObject.ApplyModifiedProperties();
GUIUtility.ExitGUI();
}
EditorGUILayout.EndHorizontal();
}
}
/************************************************************************************************************************/
private void DoOptionalWarningsGUI()
{
DoHeading("Optional Warnings");
EditorGUILayout.BeginHorizontal();
using (var label = PooledGUIContent.Acquire("Disabled Warnings"))
{
EditorGUI.BeginChangeCheck();
var value = EditorGUILayout.EnumFlagsField(label, Validate.PermanentlyDisabledWarnings);
if (EditorGUI.EndChangeCheck())
Validate.PermanentlyDisabledWarnings = (OptionalWarning)value;
}
if (GUILayout.Button("Help", EditorStyles.miniButton, AnimancerGUI.DontExpandWidth))
Application.OpenURL(Strings.DocsURLs.OptionalWarning);
EditorGUILayout.EndHorizontal();
}
/************************************************************************************************************************/
private static GUIStyle _HeadingStyle;
/// Draws a heading label.
public static void DoHeading(string text)
=> GUILayout.Label(text, _HeadingStyle ??= new(EditorStyles.largeLabel)
{
fontSize = 18,
});
/************************************************************************************************************************/
/// Creates the Project Settings page.
[SettingsProvider]
public static SettingsProvider CreateSettingsProvider()
{
UnityEditor.Editor editor = null;
return new("Project/" + Strings.ProductName, SettingsScope.Project)
{
keywords = new HashSet() { Strings.ProductName },
guiHandler = searchContext =>
{
if (editor == null)
editor = CreateEditor(Instance);
HideNextInfo = true;
editor.OnInspectorGUI();
},
};
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
}
}
#endif