Skip to content

Commit d3420ac

Browse files
committed
feat: Implemented Creatable attribute for generic MonoBehaviours
1 parent 1602288 commit d3420ac

8 files changed

+203
-50
lines changed

Editor/Drawers/CreatableObjectDrawer.cs

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
using System;
44
using System.IO;
55
using System.Reflection;
6+
using MonoBehaviours;
67
using ScriptableObjects;
78
using SolidUtilities;
89
using SolidUtilities.Editor;
910
using SolidUtilities.UnityEditorInternals;
11+
using TypeReferences;
1012
using UnityEditor;
1113
using UnityEngine;
14+
using Util;
15+
using Object = UnityEngine.Object;
1216

1317
#if ODIN_INSPECTOR
1418
using Sirenix.OdinInspector.Editor;
@@ -24,6 +28,9 @@ public class CreatableObjectDrawer : PropertyDrawer
2428
private static readonly string _assetsPath;
2529
private static readonly string _packagesPath;
2630

31+
private static CreatableObjectDrawer _instance;
32+
public static CreatableObjectDrawer Instance => _instance ??= new CreatableObjectDrawer();
33+
2734
static CreatableObjectDrawer()
2835
{
2936
_projectPath = Directory.GetCurrentDirectory();
@@ -35,9 +42,15 @@ public override void OnGUI(Rect rect, SerializedProperty property, GUIContent la
3542
{
3643
(_, Type type) = property.GetFieldInfoAndType();
3744

38-
if (!type.InheritsFrom(typeof(ScriptableObject)))
45+
if (!type.InheritsFrom(typeof(ScriptableObject)) && !type.InheritsFrom(typeof(MonoBehaviour)))
46+
{
47+
EditorGUILayoutHelper.DrawErrorMessage("Creatable attribute can only be used on ScriptableObjects and MonoBehaviours.");
48+
return;
49+
}
50+
51+
if (type.IsAbstract)
3952
{
40-
EditorGUILayoutHelper.DrawErrorMessage("Creatable attribute can only be used on ScriptableObjects.");
53+
EditorGUILayoutHelper.DrawErrorMessage("Creatable attribute can only be used on fields of non-abstract type.");
4154
return;
4255
}
4356

@@ -57,6 +70,21 @@ public override void OnGUI(Rect rect, SerializedProperty property, GUIContent la
5770
if ( ! GUI.Button(buttonRect, "+"))
5871
return;
5972

73+
// this is for scriptable objects
74+
if (type.InheritsFrom(typeof(ScriptableObject)))
75+
{
76+
CreateScriptableObject(property, type);
77+
}
78+
else
79+
{
80+
CreateMonoBehaviour(property, type);
81+
}
82+
}
83+
84+
#region Create Scriptable Object
85+
86+
private void CreateScriptableObject(SerializedProperty property, Type type)
87+
{
6088
var asset = CreateAsset(property, type);
6189
property.objectReferenceValue = asset;
6290
EditorGUIUtility.PingObject(asset);
@@ -66,7 +94,7 @@ private ScriptableObject CreateAsset(SerializedProperty property, Type type)
6694
{
6795
var folderPath = ProjectWindowUtilProxy.GetActiveFolderPath();
6896

69-
bool isGeneric = type.InheritsFrom(typeof(ScriptableObject)) && type.IsGenericType && !type.IsAbstract;
97+
bool isGeneric = type.IsGenericType;
7098

7199
string fileName = isGeneric
72100
? type.GetCustomAttribute<CreateGenericAssetMenuAttribute>()?.FileName
@@ -115,5 +143,75 @@ private bool HasCorrectExtension(string path)
115143
Debug.LogError("The file must have the '.asset' extension.");
116144
return false;
117145
}
146+
147+
#endregion
148+
149+
#region Create MonoBehaviour
150+
151+
private const string TargetComponentKey = "CreatableObjectDrawer_TargetComponent";
152+
private const string PropertyPathKey = "CreatableObjectDrawer_PropertyPath";
153+
private const string AddedComponentTypeKey = "CreatableObjectDrawer_AddedComponent";
154+
155+
private void CreateMonoBehaviour(SerializedProperty property, Type type)
156+
{
157+
var parentComponent = property.serializedObject.targetObject as MonoBehaviour;
158+
159+
if (parentComponent == null)
160+
return;
161+
162+
var gameObject = parentComponent.gameObject;
163+
164+
var component = AddComponentHelper.AddComponent(gameObject, type, out bool reloadRequired);
165+
166+
if (reloadRequired)
167+
{
168+
PersistentStorage.SaveData(TargetComponentKey, property.serializedObject.targetObject);
169+
PersistentStorage.SaveData(PropertyPathKey, property.propertyPath);
170+
PersistentStorage.SaveData(AddedComponentTypeKey, new TypeReference(type));
171+
PersistentStorage.ExecuteOnScriptsReload(OnAfterComponentAdded);
172+
AssetDatabase.Refresh();
173+
}
174+
else
175+
{
176+
property.objectReferenceValue = component;
177+
}
178+
}
179+
180+
private static void OnAfterComponentAdded()
181+
{
182+
try
183+
{
184+
var targetComponent = PersistentStorage.GetData<Object>(TargetComponentKey);
185+
var propertyPath = PersistentStorage.GetData<string>(PropertyPathKey);
186+
var componentType = PersistentStorage.GetData<TypeReference>(AddedComponentTypeKey).Type;
187+
188+
var addedComponent = ((MonoBehaviour)targetComponent).gameObject.GetComponent(componentType);
189+
var property = Editor.CreateEditor(targetComponent).serializedObject.FindProperty(propertyPath);
190+
property.objectReferenceValue = addedComponent;
191+
property.serializedObject.ApplyModifiedProperties();
192+
}
193+
finally
194+
{
195+
PersistentStorage.DeleteData(TargetComponentKey);
196+
PersistentStorage.DeleteData(PropertyPathKey);
197+
PersistentStorage.DeleteData(AddedComponentTypeKey);
198+
}
199+
}
200+
201+
#endregion
202+
}
203+
204+
public static class AddComponentHelper
205+
{
206+
public static Component AddComponent(GameObject gameObject, Type componentType, out bool reloadRequired)
207+
{
208+
if (!componentType.IsGenericType)
209+
{
210+
reloadRequired = false;
211+
return Undo.AddComponent(gameObject, componentType);
212+
}
213+
214+
return GenericBehaviourCreator.AddComponent(null, gameObject, componentType.GetGenericTypeDefinition(), componentType.GetGenericArguments(), out reloadRequired);
215+
}
118216
}
119217
}

Editor/Drawers/GenericUnityObjectDrawer.cs

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,21 @@
77
using Sirenix.OdinInspector.Editor;
88
#endif
99

10-
internal class GenericUnityObjectDrawer : PropertyDrawer
10+
internal abstract class GenericUnityObjectDrawer : PropertyDrawer
1111
{
12+
protected abstract bool AlwaysCreatable { get; }
13+
1214
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
1315
{
14-
GenericObjectDrawer.ObjectField(position, property, label);
16+
if (AlwaysCreatable)
17+
{
18+
CreatableObjectDrawer.Instance.OnGUI(position, property, label);
19+
}
20+
else
21+
{
22+
// ReSharper disable once Unity.PropertyDrawerOnGUIBase
23+
GenericObjectDrawer.ObjectField(position, property, label);
24+
}
1525
}
1626
}
1727

@@ -21,26 +31,15 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten
2131
#endif
2232
internal class GenericSODrawer : GenericUnityObjectDrawer
2333
{
24-
private static CreatableObjectDrawer _creatableObjectDrawer;
25-
26-
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
27-
{
28-
if (ProjectSettings.AlwaysCreatable)
29-
{
30-
_creatableObjectDrawer ??= new CreatableObjectDrawer();
31-
_creatableObjectDrawer.OnGUI(position, property, label);
32-
}
33-
else
34-
{
35-
// ReSharper disable once Unity.PropertyDrawerOnGUIBase
36-
base.OnGUI(position, property, label);
37-
}
38-
}
34+
protected override bool AlwaysCreatable => ProjectSettings.AlwaysCreatableScriptableObject;
3935
}
4036

4137
[CustomPropertyDrawer(typeof(MonoBehaviour), true)]
4238
#if ODIN_INSPECTOR
4339
[DrawerPriority(0, 0, 2)]
4440
#endif
45-
internal class GenericBehaviourDrawer : GenericUnityObjectDrawer { }
41+
internal class GenericBehaviourDrawer : GenericUnityObjectDrawer
42+
{
43+
protected override bool AlwaysCreatable => ProjectSettings.AlwaysCreatableMonoBehaviour;
44+
}
4645
}

Editor/GenericUnityObjects.Editor.asmdef

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"EasyButtons.Editor",
1111
"Unity.Settings.Editor",
1212
"UnityDropdown.Editor",
13-
"MissingScriptType.Editor"
13+
"MissingScriptType.Editor",
14+
"ExtEvents"
1415
],
1516
"includePlatforms": [
1617
"Editor"

Editor/Settings/ProjectSettings.cs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,29 @@ public static class ProjectSettings
88

99
private static Settings _instance;
1010

11-
private static UserSetting<bool> _alwaysCreatable;
11+
private static UserSetting<bool> _alwaysCreatableScriptableObject;
12+
private static UserSetting<bool> _alwaysCreatableMonoBehaviour;
1213

13-
public static bool AlwaysCreatable
14+
public static bool AlwaysCreatableScriptableObject
1415
{
1516
get
1617
{
1718
InitializeIfNeeded();
18-
return _alwaysCreatable.value;
19+
return _alwaysCreatableScriptableObject.value;
1920
}
2021

21-
set => _alwaysCreatable.value = value;
22+
set => _alwaysCreatableScriptableObject.value = value;
23+
}
24+
25+
public static bool AlwaysCreatableMonoBehaviour
26+
{
27+
get
28+
{
29+
InitializeIfNeeded();
30+
return _alwaysCreatableMonoBehaviour.value;
31+
}
32+
33+
set => _alwaysCreatableMonoBehaviour.value = value;
2234
}
2335

2436
private static void InitializeIfNeeded()
@@ -28,7 +40,12 @@ private static void InitializeIfNeeded()
2840

2941
_instance = new Settings(PackageName);
3042

31-
_alwaysCreatable = new UserSetting<bool>(_instance, nameof(_alwaysCreatable), false);
43+
// Previously, there was only one always-creatable parameter and it was used for scriptable objects.
44+
// For backwards compatibility, it stores the previous name.
45+
const string alwaysCreatableScriptableObjectKey = "_alwaysCreatable";
46+
_alwaysCreatableScriptableObject = new UserSetting<bool>(_instance, alwaysCreatableScriptableObjectKey, false);
47+
48+
_alwaysCreatableMonoBehaviour = new UserSetting<bool>(_instance, nameof(_alwaysCreatableMonoBehaviour), false);
3249
}
3350
}
3451
}

Editor/Settings/ProjectSettingsDrawer.cs

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace GenericUnityObjects.Editor
22
{
3+
using System;
34
using System.Collections.Generic;
45
using System.Reflection;
56
using SolidUtilities.Editor;
@@ -8,17 +9,24 @@
89

910
public static class ProjectSettingsDrawer
1011
{
11-
private const string AlwaysCreatableLabel = "Enable + button for all GenericScriptableObject fields";
12+
private const string AlwaysCreatableScriptableObjectLabel = "Enable + button for all generic ScriptableObject fields";
13+
private const string AlwaysCreatableMonoBehaviourLabel = "Enable + button for all generic MonoBehaviour fields";
1214

13-
private static readonly GUIContent _alwaysCreatableContent = new GUIContent(AlwaysCreatableLabel,
14-
"Use this to add the + button next to object field when using GenericScriptableObjects without the need to put [Creatable] attribute.");
15+
private static readonly GUIContent _alwaysCreatableScriptableObjectContent = new GUIContent(AlwaysCreatableScriptableObjectLabel,
16+
"Use this to add the + button next to object field when using generic ScriptableObjects without the need to put [Creatable] attribute.");
1517

16-
private static readonly MethodInfo _repaintAllMethod;
18+
private static readonly GUIContent _alwaysCreatableMonoBehaviourContent = new GUIContent(AlwaysCreatableMonoBehaviourLabel,
19+
"Use this to add the + button next to object field when using generic MonoBehaviours without the need to put [Creatable] attribute.");
20+
21+
private static readonly Action _repaintAllInspectors;
1722

1823
static ProjectSettingsDrawer()
1924
{
2025
var inspectorType = typeof(SceneView).Assembly.GetType("UnityEditor.InspectorWindow");
21-
_repaintAllMethod = inspectorType.GetMethod("RepaintAllInspectors", BindingFlags.Static | BindingFlags.NonPublic);
26+
var repaintAllMethod = inspectorType.GetMethod("RepaintAllInspectors", BindingFlags.Static | BindingFlags.NonPublic);
27+
28+
if (repaintAllMethod != null)
29+
_repaintAllInspectors = (Action) Delegate.CreateDelegate(typeof(Action), repaintAllMethod);
2230
}
2331

2432
[SettingsProvider]
@@ -33,23 +41,41 @@ public static SettingsProvider CreateSettingsProvider()
3341

3442
private static void OnGUI(string searchContext)
3543
{
36-
using var _ = EditorGUIUtilityHelper.LabelWidthBlock(310f);
44+
using var _ = EditorGUIUtilityHelper.LabelWidthBlock(320f);
3745

38-
bool newValue = EditorGUILayout.Toggle(_alwaysCreatableContent, ProjectSettings.AlwaysCreatable);
46+
bool newScriptableObjectValue = EditorGUILayout.Toggle(_alwaysCreatableScriptableObjectContent, ProjectSettings.AlwaysCreatableScriptableObject);
3947

40-
if (ProjectSettings.AlwaysCreatable == newValue)
41-
return;
48+
if (ProjectSettings.AlwaysCreatableScriptableObject != newScriptableObjectValue)
49+
{
50+
ProjectSettings.AlwaysCreatableScriptableObject = newScriptableObjectValue;
51+
RepaintAllInspectors();
52+
}
53+
54+
bool newMonoBehaviourValue = EditorGUILayout.Toggle(_alwaysCreatableMonoBehaviourContent, ProjectSettings.AlwaysCreatableMonoBehaviour);
4255

43-
ProjectSettings.AlwaysCreatable = newValue;
44-
RepaintAllInspectors();
56+
if (ProjectSettings.AlwaysCreatableMonoBehaviour != newMonoBehaviourValue)
57+
{
58+
ProjectSettings.AlwaysCreatableMonoBehaviour = newMonoBehaviourValue;
59+
RepaintAllInspectors();
60+
}
4561
}
4662

47-
private static void RepaintAllInspectors() => _repaintAllMethod?.Invoke(null, null);
63+
private static void RepaintAllInspectors()
64+
{
65+
if (_repaintAllInspectors == null)
66+
{
67+
Debug.LogError("There is no method called UnityEditor.InspectorWindow.RepaintAllInspectors() in this version of Unity.");
68+
return;
69+
}
70+
71+
_repaintAllInspectors();
72+
}
4873

4974
private static HashSet<string> GetKeywords()
5075
{
5176
var keywords = new HashSet<string>();
52-
keywords.AddWords(AlwaysCreatableLabel);
77+
keywords.AddWords(AlwaysCreatableScriptableObjectLabel);
78+
keywords.AddWords(AlwaysCreatableMonoBehaviourLabel);
5379
return keywords;
5480
}
5581

Editor/Util/ExampleInstaller.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using JetBrains.Annotations;
77
using SolidUtilities.Editor;
88
using UnityEditor;
9-
using UnityEngine.Events;
109
using Object = UnityEngine.Object;
1110

1211
/// <summary>
@@ -16,7 +15,7 @@
1615
public static class ExampleInstaller
1716
{
1817
[UsedImplicitly]
19-
public static void AddConcreteClasses<TObject>(KeyValuePair<Type, Type[]>[] typesToAdd, UnityAction afterAddingTypes)
18+
public static void AddConcreteClasses<TObject>(KeyValuePair<Type, Type[]>[] typesToAdd, Action afterAddingTypes)
2019
where TObject : Object
2120
{
2221
using (AssetDatabaseHelper.DisabledScope())

0 commit comments

Comments
 (0)