#441 [FEATURE] Auto drag and drop atom references through inspector without the need to manually switch usage type (#440)

* Added the ability to drag and drop any atom reference to a `AtomReference` without the need to manually select the type through the 3 dot button on inspector

- I also refactored the `AtomBaseReferenceDrawer` script so that it's easier to read/maintain.
- The class `BaseAtomInstancer` is needed to be able to auto reference instancers. Because when we drag a `GameObject` we need to get it's `AtomInstancer` component to determine the reference type. Also, the class represents something similiar to `BaseAtom` for `ScriptableObjects` but for `MonoBehaviors`, so it makes sense to have it.

* Addressed some of the key points @soraphis made and fixed the issues I mentioned on the #442 pull

- Solved the probelms that arise when a `GameObject` has multiple different instancers in it (mentioned in the #442 pull)
- Removed `UsageIndex` class as it is not needed

* Fixed file name not matching the class name

* Addressed @soraphis issue of `GuiData` not being a struct, so I changed it and modified the `AtomBaseReferenceDrawer` to handle it as a struct

* Fixed `GuiData` isn't updated even though `position` and `label` parameters of the `OnGUI` are.

* Reversed the for loops order because the order of the components inside the `GameObject` are more important than the order of the "usages" so it fixes that problem

- Also cleaned the code a bit

* Missed a line that could be simplified

- Made `Set/GetUsageIndex` into static because we can

* Fixed issue mentioned in #442 > "you only get the first component, when dragging in a game object, so there could be the case where selecting the type manually and dragging into it will swap the field"

The issue is when there are multiple instancer components in a single `GameObject` then when you drag said `GameObject` then the atom reference will switch to the first instancer (via usage index) no matter the intent of the user. However, the intent of the user could be to pick the 2nd or 3rd reference, so he could manually select the usage type using the 3 dots button, but it won't work if he decides to drag and drop a `GameObject` that has multiple instancers that the reference could switch to automatically, which will always be the first instancer of the dragged `GameObject`, which bascially makes the experience frustrating to that particular scenario. Now, you can guess that the issue is hyper specific just because of how hard it is to me to explain it in text, so don't worry if you didn't get it on the first read. If you would like me to showcase it, I will gladly share a video example of what I mean.

* Fixed index out of range exception (I forgot that I set the usage index at the end of this method and it could be -1 because of this line)

* Redone the previous push because it was incorrect

* Response to @AdamRamberg to try to make the code section clearer by refactoring it

* * Minor cosmetic changes
* Replace switch expression to be backwards compatible (for example to be able to run the unity-atoms-example project)
* Always remove the last greater than symbol in GetPropertyTypeName to take into account generic types

* Added comment to IAtomInstancer

---------

Co-authored-by: Adam Ramberg <adam@mambojambostudios.com>
This commit is contained in:
ToasterHead 2023-12-18 23:46:11 +02:00 committed by GitHub
parent 37ead24dbb
commit 53bc009865
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 227 additions and 42 deletions

View File

@ -7,7 +7,6 @@ namespace UnityAtoms.Editor
/// <summary>
/// A custom property drawer for References (Events and regular). Makes it possible to reference a resources (Variable or Event) through multiple options.
/// </summary>
public abstract class AtomBaseReferenceDrawer : PropertyDrawer
{
protected abstract class UsageData
@ -16,17 +15,60 @@ namespace UnityAtoms.Editor
public abstract string PropertyName { get; }
public abstract string DisplayName { get; }
}
private const string USAGE_PROPERTY_NAME = "_usage";
protected abstract UsageData[] GetUsages(SerializedProperty prop = null);
private string[] GetPopupOptions(SerializedProperty prop = null) => GetUsages(prop).Select(u => u.DisplayName).ToArray();
private static GUIStyle _popupStyle;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
GuiData guiData = new GuiData()
{
Position = position,
Property = property,
Label = label
};
if (_popupStyle == null)
{
_popupStyle = new GUIStyle(GUI.skin.GetStyle("PaneOptions"))
{
imagePosition = ImagePosition.ImageOnly
};
}
using (var scope = new EditorGUI.PropertyScope(position, label, property))
{
guiData.Label = scope.content;
guiData.Position = EditorGUI.PrefixLabel(position, label);
// Store old indent level and set it to 0, the PrefixLabel takes care of it
int indent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
{
EditorGUI.BeginChangeCheck();
{
DetermineDragAndDropFieldReferenceType(guiData);
DrawConfigurationButton(ref guiData);
string currentUsageTypePropertyName = GetUsages(property)[GetUsageIndex(property)].PropertyName;
DrawField(currentUsageTypePropertyName, guiData, position);
}
if (EditorGUI.EndChangeCheck())
{
property.serializedObject.ApplyModifiedProperties();
}
}
EditorGUI.indentLevel = indent;
}
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
var usageIntVal = property.FindPropertyRelative("_usage").intValue;
var usageIntVal = GetUsageIndex(property);
var usageData = GetUsages(property)[0];
for (var i = 0; i < GetUsages(property).Length; ++i)
for (int i = 0; i < GetUsages(property).Length; ++i)
{
if (GetUsages(property)[i].Value == usageIntVal)
{
@ -36,53 +78,44 @@ namespace UnityAtoms.Editor
}
var innerProperty = property.FindPropertyRelative(usageData.PropertyName);
return innerProperty == null ?
EditorGUIUtility.singleLineHeight :
EditorGUI.GetPropertyHeight(innerProperty, label);
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
private void DrawConfigurationButton(ref GuiData guiData)
{
if (_popupStyle == null)
{
_popupStyle = new GUIStyle(GUI.skin.GetStyle("PaneOptions"));
_popupStyle.imagePosition = ImagePosition.ImageOnly;
}
Rect button = new Rect(guiData.Position);
button.yMin += _popupStyle.margin.top;
button.yMax = button.yMin + EditorGUIUtility.singleLineHeight;
button.width = _popupStyle.fixedWidth + _popupStyle.margin.right;
guiData.Position.xMin = button.xMax;
Rect originalPosition = new Rect(position);
var currentUsageIndex = GetUsageIndex(guiData.Property);
var newUsageValue = EditorGUI.Popup(button, currentUsageIndex, GetPopupOptions(guiData.Property), _popupStyle);
SetUsageIndex(guiData.Property, newUsageValue);
}
label = EditorGUI.BeginProperty(position, label, property);
position = EditorGUI.PrefixLabel(position, label);
EditorGUI.BeginChangeCheck();
// Calculate rect for configuration button
Rect buttonRect = new Rect(position);
buttonRect.yMin += _popupStyle.margin.top;
buttonRect.yMax = buttonRect.yMin + EditorGUIUtility.singleLineHeight;
buttonRect.width = _popupStyle.fixedWidth + _popupStyle.margin.right;
position.xMin = buttonRect.xMax;
// Store old indent level and set it to 0, the PrefixLabel takes care of it
int indent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
var currentUsage = property.FindPropertyRelative("_usage");
var newUsageValue = EditorGUI.Popup(buttonRect, currentUsage.intValue, GetPopupOptions(property), _popupStyle);
currentUsage.intValue = newUsageValue;
var usageTypePropertyName = GetUsages(property)[newUsageValue].PropertyName;
var usageTypeProperty = property.FindPropertyRelative(usageTypePropertyName);
private static void DrawField(string usageTypePropertyName, in GuiData guiData, in Rect originalPosition)
{
var usageTypeProperty = guiData.Property.FindPropertyRelative(usageTypePropertyName);
if (usageTypeProperty == null)
{
EditorGUI.LabelField(position, "[Non serialized value]");
EditorGUI.LabelField(guiData.Position, "[Non serialized value]");
}
else
{
var expanded = usageTypeProperty.isExpanded;
usageTypeProperty.isExpanded = true;
var valueFieldHeight = EditorGUI.GetPropertyHeight(usageTypeProperty, label);
var valueFieldHeight = usageTypeProperty.propertyType == SerializedPropertyType.Quaternion ?
// In versions prior to 2022.3 GetPropertyHeight returns the wrong value for "SerializedPropertyType.Quaternion"
// In later versions, the fix is introduced _but only_ when using the SerializedPropertyType parameter, not when using the SerializedProperty parameter version.
// ALSO the SerializedPropertyType parameter version does not work with the isExpanded flag which we set to true exactly for this reason a (few) lines above.
EditorGUI.GetPropertyHeight(SerializedPropertyType.Vector3, guiData.Label) :
EditorGUI.GetPropertyHeight(usageTypeProperty, guiData.Label);
usageTypeProperty.isExpanded = expanded;
if (usageTypePropertyName == "_value" && (valueFieldHeight > EditorGUIUtility.singleLineHeight + 2))
@ -91,14 +124,127 @@ namespace UnityAtoms.Editor
}
else
{
EditorGUI.PropertyField(position, usageTypeProperty, GUIContent.none);
EditorGUI.PropertyField(guiData.Position, usageTypeProperty, GUIContent.none);
}
}
if (EditorGUI.EndChangeCheck())
property.serializedObject.ApplyModifiedProperties();
EditorGUI.indentLevel = indent;
EditorGUI.EndProperty();
}
private static void SetUsageIndex(SerializedProperty property, int index)
{
property.FindPropertyRelative(USAGE_PROPERTY_NAME).intValue = index;
}
private static int GetUsageIndex(SerializedProperty property)
{
return property.FindPropertyRelative(USAGE_PROPERTY_NAME).intValue;
}
#region Auto Drag And Drop Usage Type Detection
private void DetermineDragAndDropFieldReferenceType(in GuiData guiData)
{
EventType mouseEventType = Event.current.type;
if (mouseEventType != EventType.DragUpdated && mouseEventType != EventType.DragPerform)
{
return;
}
if (!IsMouseHoveringOverProperty(guiData.Position))
{
return;
}
var draggedObjects = DragAndDrop.objectReferences;
if (draggedObjects.Length < 1)
{
return;
}
Object draggedObject = draggedObjects[0];
if (draggedObject is GameObject gameObject)
{
object[] instancers = gameObject.GetComponents<IAtomInstancer>();
UpdateUsageConfigurationOption(guiData.Property, instancers);
}
else
{
UpdateUsageConfigurationOption(guiData.Property, draggedObject);
}
}
private void UpdateUsageConfigurationOption(SerializedProperty property, params object[] draggedObjects)
{
if (draggedObjects == null || draggedObjects.Length < 1)
{
return;
}
var usages = GetUsages(property);
int currentUsageIndex = GetUsageIndex(property);
int newUsageIndex = -1;
foreach (object draggedObject in draggedObjects)
{
for (int index = 0; index < usages.Length; index++)
{
var usage = usages[index];
var usageProperty = property.FindPropertyRelative(usage.PropertyName);
bool isDraggedTypeSameAsUsageType = AreTypesEqual(usageProperty, draggedObject);
if (isDraggedTypeSameAsUsageType)
{
bool isUsageSetByUser = currentUsageIndex == index;
if (isUsageSetByUser)
{
return;
}
bool isNewUsageIndexSet = newUsageIndex > -1;
if (!isNewUsageIndexSet)
{
newUsageIndex = index;
}
break;
}
}
}
if (newUsageIndex > -1)
{
SetUsageIndex(property, newUsageIndex);
}
}
private static bool AreTypesEqual(SerializedProperty property, object otherObject)
{
string otherObjectTypeName = otherObject.GetType().Name;
string propertyObjectTypeName = GetPropertyTypeName(property);
return otherObjectTypeName == propertyObjectTypeName;
}
private static readonly string PPTR_GENERIC_PREFIX = "PPtr<$";
private static string GetPropertyTypeName(SerializedProperty property)
{
if (!property.type.StartsWith(PPTR_GENERIC_PREFIX))
{
return property.type;
}
string fieldPropertyType = property.type.Replace(PPTR_GENERIC_PREFIX, "");
return fieldPropertyType.Remove(fieldPropertyType.Length - 1);
}
private static bool IsMouseHoveringOverProperty(in Rect rectPosition)
{
const int HEIGHT_OFFSET_TO_AVOID_OVERLAP = 1;
Rect controlRect = rectPosition;
controlRect.height -= HEIGHT_OFFSET_TO_AVOID_OVERLAP;
return controlRect.Contains(Event.current.mousePosition);
}
#endregion
}
}

View File

@ -0,0 +1,12 @@
using UnityEditor;
using UnityEngine;
namespace UnityAtoms.Editor
{
public struct GuiData
{
public Rect Position;
public SerializedProperty Property;
public GUIContent Label;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a1c20882cb5b7ab4e8c3e8fb77d1ebf8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -13,7 +13,7 @@ namespace UnityAtoms
/// <typeparam name="E">Event of type T.</typeparam>
[EditorIcon("atom-icon-sign-blue")]
[DefaultExecutionOrder(Runtime.ExecutionOrder.VARIABLE_INSTANCER)]
public abstract class AtomEventInstancer<T, E> : MonoBehaviour, IGetEvent, ISetEvent
public abstract class AtomEventInstancer<T, E> : MonoBehaviour, IGetEvent, ISetEvent, IAtomInstancer
where E : AtomEvent<T>
{
public T InspectorRaiseValue { get => _inspectorRaiseValue; }

View File

@ -17,7 +17,7 @@ namespace UnityAtoms
/// <typeparam name="F">Function of type T => T</typeparam>
[EditorIcon("atom-icon-hotpink")]
[DefaultExecutionOrder(Runtime.ExecutionOrder.VARIABLE_INSTANCER)]
public abstract class AtomBaseVariableInstancer<T, V> : MonoBehaviour, IVariable<V>
public abstract class AtomBaseVariableInstancer<T, V> : MonoBehaviour, IVariable<V>, IAtomInstancer
where V : AtomBaseVariable<T>
{
/// <summary>

View File

@ -0,0 +1,5 @@
namespace UnityAtoms
{
// Currently only used in AtomBaseReferenceDrawer in order to auto set usage type on drag and drop
public interface IAtomInstancer { }
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 31198ba3f24bc6c49955d939f8ac29c9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: