473 lines
21 KiB
C#
473 lines
21 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
using UnityEditor;
|
|
using UnityEditorInternal;
|
|
using UnityEngine;
|
|
using Object = UnityEngine.Object;
|
|
|
|
// VolFx © NullTale - https://x.com/NullTale
|
|
namespace VolFx.Tools.Editor
|
|
{
|
|
[CustomPropertyDrawer(typeof(SoCollection<>), true)]
|
|
public class SoCollectionDrawer : PropertyDrawer
|
|
{
|
|
private ReorderableList m_List;
|
|
|
|
// =======================================================================
|
|
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
|
{
|
|
try
|
|
{
|
|
var list = _getList(property);
|
|
|
|
var headerRect = position;
|
|
headerRect.height = EditorGUIUtility.singleLineHeight;
|
|
property.isExpanded = EditorGUI.Foldout(headerRect, property.isExpanded, GUIContent.none, true);
|
|
|
|
if (property.isExpanded == false)
|
|
{
|
|
ReorderableList.defaultBehaviours.DrawHeaderBackground(headerRect);
|
|
headerRect.xMin += 6f;
|
|
headerRect.xMax -= 6f;
|
|
headerRect.height -= 2f;
|
|
headerRect.y += 1f;
|
|
list.drawHeaderCallback.Invoke(headerRect);
|
|
}
|
|
else
|
|
{
|
|
list.DoList(position);
|
|
list.GetHeight();
|
|
}
|
|
|
|
}
|
|
catch
|
|
{
|
|
// pass
|
|
}
|
|
}
|
|
|
|
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
|
{
|
|
try
|
|
{
|
|
if (property.isExpanded == false)
|
|
return EditorGUIUtility.singleLineHeight;
|
|
|
|
return _getList(property).GetHeight();
|
|
}
|
|
catch
|
|
{
|
|
return EditorGUI.GetPropertyHeight(property);
|
|
}
|
|
}
|
|
|
|
// =======================================================================
|
|
private ReorderableList _getList(SerializedProperty property)
|
|
{
|
|
var bug = false;
|
|
try
|
|
{
|
|
if (m_List != null)
|
|
{
|
|
if (m_List.count < 0)
|
|
bug = true;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
bug = true;
|
|
}
|
|
|
|
if (m_List == null || bug)
|
|
{
|
|
var propertyList = property.FindPropertyRelative("m_List");
|
|
var collectionType = propertyList.GetSerializedValue<object>().GetType().GetGenericArguments().First();
|
|
var formatAtr = fieldInfo.GetCustomAttribute<SocFormatAttribute>(true);
|
|
|
|
// setup types, create list
|
|
var types = new List<Type>();
|
|
if (attribute is SocTypesAttribute attr)
|
|
types.AddRange(attr.Types);
|
|
|
|
if (types.IsEmpty())
|
|
types.Add(collectionType);
|
|
|
|
types.RemoveAll(n => n.IsAbstract);
|
|
|
|
m_List = new ReorderableList(propertyList.serializedObject, propertyList, true, true, true, true);
|
|
m_List.drawElementCallback = (rect, index, isActive, isFocused) =>
|
|
{
|
|
rect.width -= 30;
|
|
rect.x += 30 * 0.5f;
|
|
|
|
var element = propertyList.GetArrayElementAtIndex(index);
|
|
|
|
var hasValue = element.objectReferenceValue != null;
|
|
var isInner = element.objectReferenceValue != null && element.serializedObject.GetNestedAssets().Contains(element.objectReferenceValue);
|
|
|
|
// ref
|
|
using (new UtilsEditor.DisablingScope(isInner))
|
|
{
|
|
EditorGUI.BeginChangeCheck();
|
|
var picked = EditorGUI.ObjectField(rect.WithHeight(EditorGUIUtility.singleLineHeight), new GUIContent(" "), element.objectReferenceValue, types.FirstOrDefault() ?? collectionType, false);
|
|
if (EditorGUI.EndChangeCheck())
|
|
{
|
|
if (picked == null || _containtsName(picked.name) == false)
|
|
element.objectReferenceValue = picked;
|
|
else
|
|
Debug.Log($"Can't add object {picked} with duplicated name {picked.name}");
|
|
}
|
|
}
|
|
|
|
|
|
if (hasValue)
|
|
element.isExpanded = EditorGUI.Foldout(rect.WithHeight(EditorGUIUtility.singleLineHeight).WithWidth(5), element.isExpanded, GUIContent.none, toggleOnLabelClick: true);
|
|
|
|
// name
|
|
if (isInner)
|
|
{
|
|
var elementSO = element.objectReferenceValue as ScriptableObject;
|
|
if (elementSO != null)
|
|
{
|
|
// name must be unique
|
|
var newName = EditorGUI.DelayedTextField(rect.Label(), elementSO.name);
|
|
if (newName != elementSO.name && _containtsName(newName) == false)
|
|
{
|
|
elementSO.name = newName;
|
|
_delayedUpdate();
|
|
|
|
// -----------------------------------------------------------------------
|
|
async void _delayedUpdate()
|
|
{
|
|
await Task.Yield();
|
|
AssetDatabase.SaveAssets();
|
|
}
|
|
}
|
|
|
|
element.DrawObjectReference(rect);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
using (new UtilsEditor.DisablingScope())
|
|
EditorGUI.TextField(rect.Label(), element.objectReferenceValue?.name ?? string.Empty);
|
|
|
|
element.DrawObjectReference(rect);
|
|
}
|
|
|
|
// ===================================
|
|
bool _containtsName(string name)
|
|
{
|
|
return propertyList.GetList().Where(n => n.objectReferenceValue != null).Any(n => n.objectReferenceValue.name == name);
|
|
}
|
|
};
|
|
m_List.onRemoveCallback = list =>
|
|
{
|
|
var element = propertyList.GetArrayElementAtIndex(list.index);
|
|
|
|
var isNested = element.objectReferenceValue != null && element.serializedObject.GetNestedAssets().Contains(element.objectReferenceValue);
|
|
var hasValue = element.objectReferenceValue != null;
|
|
var obj = element.objectReferenceValue;
|
|
|
|
var doNotRemove = Event.current.modifiers == EventModifiers.Shift || isNested == false || hasValue == false
|
|
|| propertyList.GetList().Select(n => n.objectReferenceValue).Count(n => n == element.objectReferenceValue) > 1;
|
|
|
|
if (doNotRemove == false)
|
|
{
|
|
AssetDatabase.RemoveObjectFromAsset(obj);
|
|
Object.DestroyImmediate(element.objectReferenceValue);
|
|
element.objectReferenceValue = null;
|
|
#if !UNITY_2022_3_OR_NEWER
|
|
AssetDatabase.SaveAssets();
|
|
#endif
|
|
}
|
|
|
|
var index = list.index;
|
|
propertyList.DeleteArrayElementAtIndex(index);
|
|
};
|
|
m_List.onAddCallback = list =>
|
|
{
|
|
// collect options
|
|
var options = new List<KeyValuePair<string, Action>>();
|
|
var shiftHeld = Event.current.modifiers == EventModifiers.Shift;
|
|
|
|
// empty object option
|
|
if (shiftHeld)
|
|
options.Add(new KeyValuePair<string, Action>("Add Empty", () =>
|
|
{
|
|
propertyList.arraySize++;
|
|
propertyList.GetArrayElementAtIndex(propertyList.arraySize - 1).objectReferenceValue = null;
|
|
propertyList.serializedObject.ApplyModifiedProperties();
|
|
}));
|
|
|
|
// options from types
|
|
options.AddRange(types.Select(type => new KeyValuePair<string, Action>(type.Name, () => _createElementOfType(type))));
|
|
|
|
// if shift held add from existing and derived
|
|
if (shiftHeld)
|
|
{
|
|
|
|
var ofType = types.IsEmpty() ? TypeCache.GetTypesDerivedFrom(collectionType)
|
|
.Where(type => type.IsAbstract == false && type.IsGenericTypeDefinition == false)
|
|
.Except(new[] { collectionType })
|
|
.ToArray() : types.ToArray();
|
|
|
|
var subAssets = _getAssets(ofType)
|
|
.Except(_getElements(ofType))
|
|
.Select(so => new KeyValuePair<string, Action>((string.IsNullOrEmpty(so.name) ? "_" : so.name), () => _addExistingElement(so)))
|
|
.OrderBy(n => n.Key)
|
|
.ToList();
|
|
|
|
if (subAssets.Count > 0)
|
|
{
|
|
if (options.IsEmpty() == false)
|
|
options.Add(new KeyValuePair<string, Action>(string.Empty, null));
|
|
options.AddRange(subAssets);
|
|
}
|
|
|
|
}
|
|
if (shiftHeld || types.IsEmpty())
|
|
{
|
|
var derived = TypeCache.GetTypesDerivedFrom(collectionType)
|
|
.Where(type => type.IsAbstract == false && type.IsGenericTypeDefinition == false && type.HasAttribute<SocIgnoreAttribute>() == false)
|
|
.Except(new[] { collectionType })
|
|
.Select(type => new KeyValuePair<string, Action>(type.Name, () => _createElementOfType(type)))
|
|
.ToList();
|
|
|
|
if (derived.Count > 0)
|
|
{
|
|
if (options.IsEmpty() == false)
|
|
options.Add(new KeyValuePair<string, Action>(string.Empty, null));
|
|
options.AddRange(derived);
|
|
}
|
|
}
|
|
|
|
// apply unique filter to the final result
|
|
var uniqueAtr = fieldInfo.GetCustomAttribute<SocUniqueAttribute>(true);
|
|
if (uniqueAtr != null)
|
|
{
|
|
for (var n = 0; n < list.count; n++)
|
|
{
|
|
var item = propertyList.GetArrayElementAtIndex(n).objectReferenceValue;
|
|
if (item == null)
|
|
continue;
|
|
|
|
if (uniqueAtr._except.Contains(item.GetType()))
|
|
continue;
|
|
|
|
var typeName = item.GetType().Name;
|
|
var toRemove = options.FirstOrDefault(n => n.Key == typeName);
|
|
options.Remove(toRemove);
|
|
}
|
|
}
|
|
|
|
// check real options count
|
|
if (options.Count == 1)
|
|
{
|
|
options.First().Value.Invoke();
|
|
}
|
|
else
|
|
{
|
|
var menu = new GenericMenu();
|
|
foreach (var option in options)
|
|
{
|
|
if (string.IsNullOrEmpty(option.Key))
|
|
menu.AddSeparator(string.Empty);
|
|
else
|
|
menu.AddItem(new GUIContent(_format(option.Key)), false, option.Value.Invoke);
|
|
}
|
|
|
|
menu.ShowAsContext();
|
|
}
|
|
|
|
// ===================================
|
|
void _addExistingElement(object element)
|
|
{
|
|
propertyList.arraySize++;
|
|
propertyList.GetArrayElementAtIndex(propertyList.arraySize - 1).objectReferenceValue = (Object)element;
|
|
propertyList.serializedObject.ApplyModifiedProperties();
|
|
}
|
|
|
|
IEnumerable<ScriptableObject> _getElements(Type[] ofType)
|
|
{
|
|
var result = new List<ScriptableObject>(propertyList.arraySize);
|
|
|
|
for (var n = 0; n < propertyList.arraySize; n++)
|
|
{
|
|
var el = propertyList.GetArrayElementAtIndex(n);
|
|
if (ofType.Contains(el.objectReferenceValue.GetType()) == false)
|
|
continue;
|
|
|
|
result.Add(el.objectReferenceValue as ScriptableObject);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
IEnumerable<ScriptableObject> _getAssets(Type[] ofType)
|
|
{
|
|
return AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(propertyList.serializedObject.targetObject))
|
|
.Except(new []{propertyList.serializedObject.targetObject})
|
|
.OfType<ScriptableObject>()
|
|
.Where(n => ofType.Contains(n.GetType()))
|
|
.ToList();
|
|
}
|
|
};
|
|
m_List.drawHeaderCallback = rect =>
|
|
{
|
|
var style = EditorStyles.label;
|
|
if (Event.current.modifiers == EventModifiers.Shift)
|
|
{
|
|
style = new GUIStyle(EditorStyles.label);
|
|
style.normal.textColor = Color.yellow;
|
|
}
|
|
EditorGUI.LabelField(rect, new GUIContent(property.displayName, "Hold shift to see all options"), style);
|
|
|
|
if (Event.current.type == EventType.MouseDown && Event.current.button == 0 && rect.Contains(Event.current.mousePosition))
|
|
{
|
|
property.isExpanded = !property.isExpanded;
|
|
}
|
|
|
|
if (Event.current.type == EventType.MouseDown && Event.current.button == 1 && rect.Contains(Event.current.mousePosition))
|
|
{
|
|
var menu = new GenericMenu();
|
|
menu.AddItem(new GUIContent("Sort Alphabetic"), false, () =>
|
|
{
|
|
var objects = propertyList.GetList().Select(n => n.objectReferenceValue).OrderBy(n => n.name).ToList();
|
|
propertyList.SetList(objects);
|
|
propertyList.serializedObject.ApplyModifiedProperties();
|
|
});
|
|
menu.AddItem(new GUIContent("Add All Unique"), false, () =>
|
|
{
|
|
var set = _collectionTypes();
|
|
var toCreate = types.IsEmpty() ? TypeCache.GetTypesDerivedFrom(collectionType)
|
|
.Where(type => type.IsAbstract == false && type.IsGenericTypeDefinition == false && set.Contains(type) == false)
|
|
.Except(new[] { collectionType })
|
|
.ToList() : types.ToList();
|
|
|
|
var uniqueAtr = fieldInfo.GetCustomAttribute<SocUniqueAttribute>(true);
|
|
if (uniqueAtr != null)
|
|
{
|
|
foreach (var type in uniqueAtr._except)
|
|
toCreate.Remove(type);
|
|
}
|
|
|
|
foreach (var type in toCreate)
|
|
_createElementOfType(type);
|
|
|
|
// -----------------------------------------------------------------------
|
|
HashSet<Type> _collectionTypes()
|
|
{
|
|
var result = new HashSet<Type>(propertyList.arraySize);
|
|
|
|
for (var n = 0; n < propertyList.arraySize; n++)
|
|
{
|
|
var el = propertyList.GetArrayElementAtIndex(n);
|
|
if (el.objectReferenceValue == null)
|
|
continue;
|
|
result.Add(el.objectReferenceValue?.GetType());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
});
|
|
menu.ShowAsContext();
|
|
}
|
|
};
|
|
m_List.elementHeightCallback = index =>
|
|
{
|
|
if (propertyList.arraySize == 0)
|
|
return 0;
|
|
|
|
return propertyList.GetArrayElementAtIndex(index).GetObjectReferenceHeight();
|
|
};
|
|
|
|
// =======================================================================
|
|
void _createElementOfType(object type)
|
|
{
|
|
propertyList.arraySize ++;
|
|
|
|
var element = ScriptableObject.CreateInstance((Type)type);
|
|
var varName = _unique(_format(element.GetType().Name));
|
|
|
|
// create
|
|
element.name = varName;
|
|
AssetDatabase.AddObjectToAsset(element, propertyList.serializedObject.targetObject);
|
|
#if !UNITY_2022_3_OR_NEWER
|
|
AssetDatabase.SaveAssets();
|
|
#endif
|
|
propertyList.GetArrayElementAtIndex(propertyList.arraySize - 1).objectReferenceValue = element;
|
|
|
|
propertyList.serializedObject.ApplyModifiedProperties();
|
|
|
|
// -----------------------------------------------------------------------
|
|
string _unique(string nameIn)
|
|
{
|
|
var letter = 0;
|
|
|
|
while (true)
|
|
{
|
|
var finalName = nameIn + $" {_letter(letter ++)}";
|
|
if (_contains(finalName) == false)
|
|
return finalName;
|
|
}
|
|
|
|
static string _letter(int index)
|
|
{
|
|
const string letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
|
|
var value = "";
|
|
|
|
if (index >= letters.Length)
|
|
value += letters[index / letters.Length - 1];
|
|
|
|
value += letters[index % letters.Length];
|
|
|
|
return value;
|
|
}
|
|
|
|
bool _contains(string objName)
|
|
{
|
|
for (var n = 0; n < propertyList.arraySize; n++)
|
|
{
|
|
var e = propertyList.GetArrayElementAtIndex(n);
|
|
if (e.objectReferenceValue == null)
|
|
continue;
|
|
|
|
if (e.objectReferenceValue.name == objName)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
string _format(string varName)
|
|
{
|
|
if (formatAtr != null)
|
|
{
|
|
var regex = new Regex(formatAtr._regexClear);
|
|
varName = regex.Replace(varName, "");
|
|
varName = string.Format(varName, formatAtr._format);
|
|
if (formatAtr._nicify)
|
|
varName = ObjectNames.NicifyVariableName(varName);
|
|
}
|
|
else
|
|
{
|
|
varName = ObjectNames.NicifyVariableName(varName);
|
|
}
|
|
|
|
return varName;
|
|
}
|
|
}
|
|
|
|
return m_List;
|
|
}
|
|
|
|
}
|
|
} |