forked from Shardstone/trail-into-darkness
149 lines
6.2 KiB
C#
149 lines
6.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Jovian.EncounterSystem;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace Jovian.EncounterSystem.Editor {
|
|
/// <summary>Concrete-type dropdown for <c>[SerializeReference]</c> fields, arrays, and list elements.</summary>
|
|
[CustomPropertyDrawer(typeof(SubclassSelectorAttribute))]
|
|
public class SubclassSelectorDrawer : PropertyDrawer {
|
|
private static readonly Dictionary<Type, Type[]> TypeCache = new();
|
|
|
|
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
|
if(property.propertyType != SerializedPropertyType.ManagedReference) {
|
|
EditorGUI.LabelField(position, label.text, "[SubclassSelector] requires [SerializeReference]");
|
|
return;
|
|
}
|
|
|
|
var baseType = ResolveBaseType(fieldInfo.FieldType);
|
|
var concreteTypes = GetConcreteTypes(baseType);
|
|
var currentType = GetCurrentType(property);
|
|
var currentIndex = Array.IndexOf(concreteTypes, currentType);
|
|
|
|
var names = new string[concreteTypes.Length + 1];
|
|
names[0] = "<None>";
|
|
for(int i = 0; i < concreteTypes.Length; i++) {
|
|
names[i + 1] = concreteTypes[i].Name;
|
|
}
|
|
|
|
var displayLabel = ResolveDisplayLabel(property, label);
|
|
var headerRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
|
|
var labelRect = new Rect(headerRect.x, headerRect.y, EditorGUIUtility.labelWidth, headerRect.height);
|
|
var popupRect = new Rect(headerRect.x + EditorGUIUtility.labelWidth, headerRect.y, headerRect.width - EditorGUIUtility.labelWidth, headerRect.height);
|
|
|
|
EditorGUI.LabelField(labelRect, displayLabel);
|
|
var newIndex = EditorGUI.Popup(popupRect, currentIndex + 1, names);
|
|
if(newIndex != currentIndex + 1) {
|
|
property.managedReferenceValue = newIndex == 0 ? null : Activator.CreateInstance(concreteTypes[newIndex - 1]);
|
|
property.serializedObject.ApplyModifiedProperties();
|
|
}
|
|
|
|
if(property.managedReferenceValue == null) {
|
|
return;
|
|
}
|
|
|
|
EditorGUI.indentLevel++;
|
|
var y = position.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
|
|
var iterator = property.Copy();
|
|
var end = iterator.GetEndProperty();
|
|
if(iterator.NextVisible(true)) {
|
|
while(!SerializedProperty.EqualContents(iterator, end)) {
|
|
var h = EditorGUI.GetPropertyHeight(iterator, true);
|
|
var r = new Rect(position.x, y, position.width, h);
|
|
EditorGUI.PropertyField(r, iterator, true);
|
|
y += h + EditorGUIUtility.standardVerticalSpacing;
|
|
if(!iterator.NextVisible(false)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
EditorGUI.indentLevel--;
|
|
}
|
|
|
|
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
|
|
var height = EditorGUIUtility.singleLineHeight;
|
|
if(property.propertyType != SerializedPropertyType.ManagedReference || property.managedReferenceValue == null) {
|
|
return height;
|
|
}
|
|
|
|
var iterator = property.Copy();
|
|
var end = iterator.GetEndProperty();
|
|
if(iterator.NextVisible(true)) {
|
|
while(!SerializedProperty.EqualContents(iterator, end)) {
|
|
height += EditorGUI.GetPropertyHeight(iterator, true) + EditorGUIUtility.standardVerticalSpacing;
|
|
if(!iterator.NextVisible(false)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return height;
|
|
}
|
|
|
|
private static Type ResolveBaseType(Type fieldType) {
|
|
if(fieldType.IsArray) {
|
|
return fieldType.GetElementType();
|
|
}
|
|
if(fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(List<>)) {
|
|
return fieldType.GetGenericArguments()[0];
|
|
}
|
|
return fieldType;
|
|
}
|
|
|
|
private static Type[] GetConcreteTypes(Type baseType) {
|
|
if(TypeCache.TryGetValue(baseType, out var cached)) {
|
|
return cached;
|
|
}
|
|
|
|
var types = AppDomain.CurrentDomain.GetAssemblies()
|
|
.SelectMany(assembly => {
|
|
try {
|
|
return assembly.GetTypes();
|
|
}
|
|
catch {
|
|
return Array.Empty<Type>();
|
|
}
|
|
})
|
|
.Where(type => baseType.IsAssignableFrom(type)
|
|
&& !type.IsAbstract
|
|
&& !type.IsInterface
|
|
&& !typeof(UnityEngine.Object).IsAssignableFrom(type)
|
|
&& type.GetConstructor(Type.EmptyTypes) != null)
|
|
.OrderBy(type => type.Name)
|
|
.ToArray();
|
|
TypeCache[baseType] = types;
|
|
return types;
|
|
}
|
|
|
|
private static GUIContent ResolveDisplayLabel(SerializedProperty property, GUIContent fallback) {
|
|
var value = property.managedReferenceValue;
|
|
if(value is not IEncounter) {
|
|
return fallback;
|
|
}
|
|
|
|
var definitionProp = value.GetType().GetProperty("EncounterDefinition");
|
|
var definition = definitionProp?.GetValue(value);
|
|
var nameField = definition?.GetType().GetField("name");
|
|
var name = nameField?.GetValue(definition) as string;
|
|
return string.IsNullOrEmpty(name) ? fallback : new GUIContent(name);
|
|
}
|
|
|
|
private static Type GetCurrentType(SerializedProperty property) {
|
|
var full = property.managedReferenceFullTypename;
|
|
if(string.IsNullOrEmpty(full)) {
|
|
return null;
|
|
}
|
|
|
|
var space = full.IndexOf(' ');
|
|
if(space < 0) {
|
|
return null;
|
|
}
|
|
|
|
var assembly = full.Substring(0, space);
|
|
var typeName = full.Substring(space + 1);
|
|
return Type.GetType($"{typeName}, {assembly}");
|
|
}
|
|
}
|
|
}
|