From 91648de3d9be314489f8ca4230e4a55de3253463 Mon Sep 17 00:00:00 2001 From: max Date: Thu, 23 Dec 2021 04:02:01 +0100 Subject: [PATCH] ExpandableScriptableObjectDrawer --- CHANGELOG.md | 5 +- Editor.meta | 8 + Editor/Drawers.meta | 8 + .../Drawers/ExtendedScriptableObjectDrawer.cs | 218 ++++++++++++++++++ .../ExtendedScriptableObjectDrawer.cs.meta | 11 + Editor/EditorUtils.cs | 70 ++++++ Editor/EditorUtils.cs.meta | 11 + .../VertexColor.ScriptableData.Editor.asmdef | 18 ++ ...texColor.ScriptableData.Editor.asmdef.meta | 7 + README.md | 7 + Runtime/Attributes.meta | 8 + Runtime/Attributes/NonExtendableAttribute.cs | 8 + .../Attributes/NonExtendableAttribute.cs.meta | 11 + THIRD PARTY NOTICES.md | 12 +- package.json | 2 +- 15 files changed, 394 insertions(+), 10 deletions(-) create mode 100644 Editor.meta create mode 100644 Editor/Drawers.meta create mode 100644 Editor/Drawers/ExtendedScriptableObjectDrawer.cs create mode 100644 Editor/Drawers/ExtendedScriptableObjectDrawer.cs.meta create mode 100644 Editor/EditorUtils.cs create mode 100644 Editor/EditorUtils.cs.meta create mode 100644 Editor/VertexColor.ScriptableData.Editor.asmdef create mode 100644 Editor/VertexColor.ScriptableData.Editor.asmdef.meta create mode 100644 Runtime/Attributes.meta create mode 100644 Runtime/Attributes/NonExtendableAttribute.cs create mode 100644 Runtime/Attributes/NonExtendableAttribute.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 9509232..eda8d94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Change Log: - ## 1.0.0 - - Base project. + +## 1.1.0 +- ExstendableScriptableObjectDrawer. diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..0434fdb --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d458e753979e51e45b177e41f406d936 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Drawers.meta b/Editor/Drawers.meta new file mode 100644 index 0000000..4d035e8 --- /dev/null +++ b/Editor/Drawers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d838f79c4b7c4b94080dbdb3d4d7c434 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Drawers/ExtendedScriptableObjectDrawer.cs b/Editor/Drawers/ExtendedScriptableObjectDrawer.cs new file mode 100644 index 0000000..bc23641 --- /dev/null +++ b/Editor/Drawers/ExtendedScriptableObjectDrawer.cs @@ -0,0 +1,218 @@ +// References: +// https://gist.github.com/tomkail/ba4136e6aa990f4dc94e0d39ec6a058c +// Developed by Tom Kail at Inkle +// Released under the MIT Licence as held at https://opensource.org/licenses/MIT + +using System; +using System.Linq; +using UnityEngine; +using UnityEditor; +using Object = UnityEngine.Object; + +namespace ScriptableData.Editor +{ + [CustomPropertyDrawer(typeof(ScriptableObject), true)] + public class ExtendedScriptableObjectDrawer : PropertyDrawer + { + private static readonly string[] ignoreClassFullNames = new string[] { "TMPro.TMP_FontAsset" }; + private const int buttonWidth = 20; + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + float totalHeight = EditorGUIUtility.singleLineHeight; + if (property.objectReferenceValue == null || !HasVisableSubProperties(property) || fieldInfo.HasAttribute()) + { + return totalHeight; + } + + if (property.isExpanded) + { + ScriptableObject data = property.objectReferenceValue as ScriptableObject; + + if (data == null) + { + return EditorGUIUtility.singleLineHeight; + } + + SerializedObject serializedObject = new SerializedObject(data); + SerializedProperty prop = serializedObject.GetIterator(); + if (prop.NextVisible(true)) + { + do + { + if (prop.name == "m_Script") + { + continue; + } + + SerializedProperty subProp = serializedObject.FindProperty(prop.name); + float height = EditorGUI.GetPropertyHeight(subProp, null, true) + EditorGUIUtility.standardVerticalSpacing; + totalHeight += height; + } + while (prop.NextVisible(false)); + } + // Add a tiny bit of height if open for the background + totalHeight += EditorGUIUtility.standardVerticalSpacing; + } + + return totalHeight; + } + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + + Type fieldType = EditorUtils.GetFieldType(fieldInfo); + + if (fieldType == null || HasIgnoredClassName(fieldType) || fieldInfo.HasAttribute()) + { + EditorGUI.PropertyField(position, property, label); + EditorGUI.EndProperty(); + return; + } + + ScriptableObject propertySO = null; + if (!property.hasMultipleDifferentValues && property.serializedObject.targetObject != null && property.serializedObject.targetObject is ScriptableObject) + { + propertySO = property.serializedObject.targetObject as ScriptableObject; + } + + GUIContent guiContent = new GUIContent(property.displayName); + Rect foldoutRect = new Rect(position.x, position.y, EditorGUIUtility.labelWidth, EditorGUIUtility.singleLineHeight); + if (property.objectReferenceValue != null && HasVisableSubProperties(property)) + { + property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, guiContent, true); + } + else + { + foldoutRect.x += 10; + EditorGUI.Foldout(foldoutRect, property.isExpanded, guiContent, true, EditorStyles.label); + } + + Rect indentedPosition = EditorGUI.IndentedRect(position); + float indentOffset = indentedPosition.x - position.x; + Rect propertyRect = new Rect(position.x + (EditorGUIUtility.labelWidth - indentOffset + 2), position.y, position.width - (EditorGUIUtility.labelWidth - indentOffset + 2), EditorGUIUtility.singleLineHeight); + + if (propertySO != null || property.objectReferenceValue == null) + { + propertyRect.width -= buttonWidth; + } + + EditorGUI.BeginChangeCheck(); + + Object assignedObject = EditorGUI.ObjectField(propertyRect, GUIContent.none, property.objectReferenceValue, fieldType, false); + + if (EditorGUI.EndChangeCheck()) + { + property.objectReferenceValue = assignedObject; + property.serializedObject.ApplyModifiedProperties(); + } + + Rect buttonRect = new Rect(position.x + position.width - buttonWidth, position.y, buttonWidth, EditorGUIUtility.singleLineHeight); + + if (property.propertyType == SerializedPropertyType.ObjectReference && property.objectReferenceValue != null) + { + if (property.isExpanded) + { + DrawScriptableObjectChildFields(position, property); + } + } + else + { + DrawScriptableObjectCreateButton(buttonRect, property, fieldType); + } + + property.serializedObject.ApplyModifiedProperties(); + EditorGUI.EndProperty(); + } + + private void DrawScriptableObjectChildFields(Rect position, SerializedProperty property) + { + ScriptableObject scriptableObject = (ScriptableObject)property.objectReferenceValue; + + if (scriptableObject == null) + { + return; + } + + // Draw a background that shows us clearly which fields are part of the ScriptableObject + GUI.Box(new Rect(position.x, position.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing - 1, position.width, position.height - EditorGUIUtility.singleLineHeight - EditorGUIUtility.standardVerticalSpacing), ""); + + EditorGUI.indentLevel++; + SerializedObject serializedObject = new SerializedObject(scriptableObject); + + // Iterate over all the values and draw them + SerializedProperty prop = serializedObject.GetIterator(); + float y = position.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; + + EditorGUI.BeginChangeCheck(); + + if (prop.NextVisible(true)) + { + do + { + // Don't bother drawing the class file + if (prop.name == "m_Script") continue; + float height = EditorGUI.GetPropertyHeight(prop, new GUIContent(prop.displayName), true); + EditorGUI.PropertyField(new Rect(position.x, y, position.width /*- buttonWidth*/, height), prop, true); + y += height + EditorGUIUtility.standardVerticalSpacing; + } + while (prop.NextVisible(false)); + } + + if (EditorGUI.EndChangeCheck()) + { + serializedObject.ApplyModifiedProperties(); + } + + EditorGUI.indentLevel--; + } + + private void DrawScriptableObjectCreateButton(Rect position, SerializedProperty property, Type type) + { + if (GUI.Button(position, "+")) + { + if (type.IsAbstract) + { + GenericMenu typeChooser = new GenericMenu(); + foreach (var elem in type.Assembly.GetTypes().Where(t => type.IsAssignableFrom(t))) + { + if (elem.IsAbstract) continue; + + typeChooser.AddItem(new GUIContent(elem.Name), false, (elem) => { + property.objectReferenceValue = EditorUtils.CreateAssetWithSavePrompt(elem as Type, EditorUtils.GetSelectedAssetPath(property)); + property.serializedObject.ApplyModifiedProperties(); + }, elem); + } + typeChooser.ShowAsContext(); + } + else + { + property.objectReferenceValue = EditorUtils.CreateAssetWithSavePrompt(type, EditorUtils.GetSelectedAssetPath(property)); + } + } + } + + private bool HasIgnoredClassName(Type type) + { + return ignoreClassFullNames.Contains(type.FullName); + } + + private bool HasVisableSubProperties(SerializedProperty property) + { + ScriptableObject scriptableObject = (ScriptableObject)property.objectReferenceValue; + + if (scriptableObject != null) + { + SerializedObject serializedObject = new SerializedObject(scriptableObject); + SerializedProperty prop = serializedObject.GetIterator(); + while (prop.NextVisible(true)) + { + if (prop.name == "m_Script") continue; + return true; //if theres any visible property other than m_script + } + } + return false; + } + } +} \ No newline at end of file diff --git a/Editor/Drawers/ExtendedScriptableObjectDrawer.cs.meta b/Editor/Drawers/ExtendedScriptableObjectDrawer.cs.meta new file mode 100644 index 0000000..c052b21 --- /dev/null +++ b/Editor/Drawers/ExtendedScriptableObjectDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c9558a1cbd654b5499f5394666f2479d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/EditorUtils.cs b/Editor/EditorUtils.cs new file mode 100644 index 0000000..d8c616c --- /dev/null +++ b/Editor/EditorUtils.cs @@ -0,0 +1,70 @@ +using System; +using System.Reflection; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; + +namespace ScriptableData.Editor +{ + public static class EditorUtils + { + public static string GetSelectedAssetPath(SerializedProperty property) + { + string selectedAssetPath = "Assets"; + + if (property.serializedObject.targetObject is MonoBehaviour behaviour) + { + MonoScript ms = MonoScript.FromMonoBehaviour(behaviour); + selectedAssetPath = System.IO.Path.GetDirectoryName(AssetDatabase.GetAssetPath(ms)); + } + + return selectedAssetPath; + } + + public static ScriptableObject CreateAssetWithSavePrompt(Type type, string path) + { + path = EditorUtility.SaveFilePanelInProject("Save ScriptableObject", type.Name + ".asset", "asset", "Enter a file name for the ScriptableObject.", path); + if (string.IsNullOrWhiteSpace(path)) { + return null; + } + ScriptableObject asset = ScriptableObject.CreateInstance(type); + AssetDatabase.CreateAsset(asset, path); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate); + EditorGUIUtility.PingObject(asset); + return asset; + } + + public static bool HasAttribute(this FieldInfo fieldInfo) + { + Attribute[] attributes = Attribute.GetCustomAttributes(fieldInfo); + + foreach (Attribute a in attributes) + { + if (a is T) + { + return true; + } + } + + return false; + } + + public static Type GetFieldType(FieldInfo fieldInfo) + { + Type type = fieldInfo.FieldType; + + if (type.IsArray) + { + type = type.GetElementType(); + } + else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) + { + type = type.GetGenericArguments()[0]; + } + + return type; + } + } +} \ No newline at end of file diff --git a/Editor/EditorUtils.cs.meta b/Editor/EditorUtils.cs.meta new file mode 100644 index 0000000..0c266ed --- /dev/null +++ b/Editor/EditorUtils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0a7cb0a51d6707b42a7d94c7ad8f42dc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/VertexColor.ScriptableData.Editor.asmdef b/Editor/VertexColor.ScriptableData.Editor.asmdef new file mode 100644 index 0000000..8ea9c0d --- /dev/null +++ b/Editor/VertexColor.ScriptableData.Editor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "VertexColor.ScriptableData.Editor", + "rootNamespace": "ScriptableData.Editor", + "references": [ + "GUID:bd2fa452e376b7e4e8356eed741e5eea" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Editor/VertexColor.ScriptableData.Editor.asmdef.meta b/Editor/VertexColor.ScriptableData.Editor.asmdef.meta new file mode 100644 index 0000000..c753ce8 --- /dev/null +++ b/Editor/VertexColor.ScriptableData.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 67726662be89db44e8fafa5427d4ed5f +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md index d88f444..cc7c882 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,13 @@ public class ScriptableEvent : ScriptableObject {} [CreateAssetMenu(menuName = "ScriptableData/Event/Vector3", order = 147)] public class SEVector3 : ScriptableEvent {} ``` +## ExtendedScriptableObjectDrawer +If you don't want to use the extended drawer, use the `[NonExpandable]` attribute. +### Example: +```C# +[NonExpandable] +public SEVector3 myVector3; +``` ## Install [Installing from a Git URL](https://docs.unity3d.com/Manual/upm-ui-giturl.html) diff --git a/Runtime/Attributes.meta b/Runtime/Attributes.meta new file mode 100644 index 0000000..a9105b1 --- /dev/null +++ b/Runtime/Attributes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 53eb228a806717b42ad447e290770d87 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Attributes/NonExtendableAttribute.cs b/Runtime/Attributes/NonExtendableAttribute.cs new file mode 100644 index 0000000..090a68f --- /dev/null +++ b/Runtime/Attributes/NonExtendableAttribute.cs @@ -0,0 +1,8 @@ +using System; +using UnityEngine; + +namespace ScriptableData +{ + [AttributeUsage(AttributeTargets.Field, AllowMultiple = true, Inherited = true)] + public class NonExtendableAttribute : PropertyAttribute { } +} \ No newline at end of file diff --git a/Runtime/Attributes/NonExtendableAttribute.cs.meta b/Runtime/Attributes/NonExtendableAttribute.cs.meta new file mode 100644 index 0000000..7bc4117 --- /dev/null +++ b/Runtime/Attributes/NonExtendableAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f456f876476c1a54b86a231e0271cf82 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/THIRD PARTY NOTICES.md b/THIRD PARTY NOTICES.md index 7b0cc49..75e1d0d 100644 --- a/THIRD PARTY NOTICES.md +++ b/THIRD PARTY NOTICES.md @@ -1,11 +1,9 @@ This package borrows code from various different sources, including: -# []() - -### Relevant Files -- []() +## ExtendedScriptableObjectDrawer +- [x] Modified ### Credits -- Author: []() -- Source: []() -- License: []() +- Author: Tom Kail +- Source: [ExtendedScriptableObjectDrawer](https://gist.github.com/tomkail/ba4136e6aa990f4dc94e0d39ec6a058c) +- License: [MIT](https://opensource.org/licenses/MIT) diff --git a/package.json b/package.json index 02b672e..d1808a4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.vertexcolor.scriptabledata", "displayName": "ScriptableData", - "version": "1.0.0", + "version": "1.1.0", "unity": "2019.1", "description": "ScriptableData code base for ScriptableObject workflow.", "category": "Tool",