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().GetType().GetGenericArguments().First(); var formatAtr = fieldInfo.GetCustomAttribute(true); // setup types, create list var types = new List(); 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>(); var shiftHeld = Event.current.modifiers == EventModifiers.Shift; // empty object option if (shiftHeld) options.Add(new KeyValuePair("Add Empty", () => { propertyList.arraySize++; propertyList.GetArrayElementAtIndex(propertyList.arraySize - 1).objectReferenceValue = null; propertyList.serializedObject.ApplyModifiedProperties(); })); // options from types options.AddRange(types.Select(type => new KeyValuePair(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.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.Empty, null)); options.AddRange(subAssets); } } if (shiftHeld || types.IsEmpty()) { var derived = TypeCache.GetTypesDerivedFrom(collectionType) .Where(type => type.IsAbstract == false && type.IsGenericTypeDefinition == false && type.HasAttribute() == false) .Except(new[] { collectionType }) .Select(type => new KeyValuePair(type.Name, () => _createElementOfType(type))) .ToList(); if (derived.Count > 0) { if (options.IsEmpty() == false) options.Add(new KeyValuePair(string.Empty, null)); options.AddRange(derived); } } // apply unique filter to the final result var uniqueAtr = fieldInfo.GetCustomAttribute(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 _getElements(Type[] ofType) { var result = new List(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 _getAssets(Type[] ofType) { return AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(propertyList.serializedObject.targetObject)) .Except(new []{propertyList.serializedObject.targetObject}) .OfType() .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(true); if (uniqueAtr != null) { foreach (var type in uniqueAtr._except) toCreate.Remove(type); } foreach (var type in toCreate) _createElementOfType(type); // ----------------------------------------------------------------------- HashSet _collectionTypes() { var result = new HashSet(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; } } }