// This code is part of the Fungus library (https://github.com/snozbot/fungus) // It is released for free under the MIT open source license (https://github.com/snozbot/fungus/blob/master/LICENSE) using UnityEditor; using UnityEditorInternal; using UnityEngine; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor.Callbacks; namespace Fungus { [CustomEditor (typeof(LuaBindings))] public class LuaBindingsEditor : Editor { protected ReorderableList boundObjectsList; protected SerializedProperty allLuaEnvironmentsProp; protected SerializedProperty luaEnvironmentProp; protected SerializedProperty tableNameProp; protected SerializedProperty registerTypesProp; protected SerializedProperty boundObjectsProp; protected SerializedProperty showInheritedProp; protected string bindingHelpItem = ""; protected string bindingHelpDetail = ""; protected virtual void OnEnable() { allLuaEnvironmentsProp = serializedObject.FindProperty("allEnvironments"); luaEnvironmentProp = serializedObject.FindProperty("luaEnvironment"); tableNameProp = serializedObject.FindProperty("tableName"); registerTypesProp = serializedObject.FindProperty("registerTypes"); boundObjectsProp = serializedObject.FindProperty("boundObjects"); showInheritedProp = serializedObject.FindProperty("showInherited"); CreateBoundObjectsList(); } protected void CreateBoundObjectsList() { boundObjectsList = new ReorderableList(serializedObject, boundObjectsProp, true, true, true, true); boundObjectsList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => { var element = boundObjectsList.serializedProperty.GetArrayElementAtIndex(index); rect.y += 2; float widthA = rect.width * 0.25f; float widthB = rect.width * 0.75f * 0.5f; float widthC = rect.width * 0.75f * 0.5f; SerializedProperty keyProp = element.FindPropertyRelative("key"); SerializedProperty objectProp = element.FindPropertyRelative("obj"); EditorGUI.BeginChangeCheck(); EditorGUI.PropertyField( new Rect(rect.x, rect.y, widthA - 5, EditorGUIUtility.singleLineHeight), keyProp, GUIContent.none); if (EditorGUI.EndChangeCheck()) { // Force the key to be a valid Lua variable name var luaBindings = target as LuaBindings; keyProp.stringValue = GetUniqueKey(luaBindings, keyProp.stringValue, index); } EditorGUI.BeginChangeCheck(); EditorGUI.PropertyField( new Rect(rect.x + widthA, rect.y, widthB - 5, EditorGUIUtility.singleLineHeight), objectProp, GUIContent.none); if (EditorGUI.EndChangeCheck()) { // Use the object name as the key string keyName = objectProp.objectReferenceValue.name; var luaBindings = target as LuaBindings; element.FindPropertyRelative("key").stringValue = GetUniqueKey(luaBindings, keyName.ToLower(), index); // Auto select any Flowchart component in the object GameObject go = objectProp.objectReferenceValue as GameObject; if (go != null) { Component flowchart = go.GetComponent("Fungus.Flowchart"); if (flowchart != null) { SerializedProperty componentProp = element.FindPropertyRelative("component"); componentProp.objectReferenceValue = flowchart; } } } if (objectProp.objectReferenceValue != null) { GameObject go = objectProp.objectReferenceValue as GameObject; if (go != null) { SerializedProperty componentProp = element.FindPropertyRelative("component"); int selected = 0; List<string> options = new List<string>(); options.Add("<GameObject>"); int count = 1; Component[] componentList = go.GetComponents<Component>(); foreach (Component component in componentList) { if (componentProp.objectReferenceValue == component) { selected = count; } if (component == null || component.GetType() == null) { // Missing script? continue; } string componentName = component.GetType().ToString().Replace("UnityEngine.", ""); options.Add(componentName); count++; } int i = EditorGUI.Popup( new Rect(rect.x + widthA + widthB, rect.y, widthC, EditorGUIUtility.singleLineHeight), selected, options.ToArray()); if (i == 0) { componentProp.objectReferenceValue = null; } else { componentProp.objectReferenceValue = componentList[i - 1]; } } } boundObjectsList.onAddCallback = (ReorderableList l) => { // Add a new item. This copies last item in the list, so clear new items values. boundObjectsProp.InsertArrayElementAtIndex(boundObjectsProp.arraySize); SerializedProperty newItem = boundObjectsProp.GetArrayElementAtIndex(boundObjectsProp.arraySize - 1); newItem.FindPropertyRelative("key").stringValue = ""; newItem.FindPropertyRelative("obj").objectReferenceValue = null; newItem.FindPropertyRelative("component").objectReferenceValue = null; }; }; boundObjectsList.drawHeaderCallback = (Rect rect) => { float widthA = rect.width * 0.25f; float widthB = rect.width * 0.75f * 0.5f; float widthC = rect.width * 0.75f * 0.5f; EditorGUI.LabelField(new Rect(rect.x+ 12, rect.y, widthA, EditorGUIUtility.singleLineHeight), "Key"); EditorGUI.LabelField(new Rect(rect.x + widthA + 12, rect.y, widthB, EditorGUIUtility.singleLineHeight), "Object"); EditorGUI.LabelField(new Rect(rect.x + widthA + widthB + 6, rect.y, widthC, EditorGUIUtility.singleLineHeight), "Component"); }; } public override void OnInspectorGUI() { serializedObject.Update(); EditorGUILayout.PropertyField(allLuaEnvironmentsProp); if (!allLuaEnvironmentsProp.boolValue) { EditorGUILayout.PropertyField(luaEnvironmentProp); } EditorGUILayout.PropertyField(tableNameProp); EditorGUILayout.PropertyField(registerTypesProp); EditorGUILayout.LabelField("Object Bindings"); boundObjectsList.DoLayoutList(); EditorGUILayout.Space(); ShowBindingMemberInfo(); // Update the bound types on every tick to make sure they're up to date. // This could be a bit heavy on performance if the bound object list is long, but // I couldn't get it to work reliably by only updating when the list has changed. // This only happens when inspecting a Fungus Bindings component so I think it'll be ok. PopulateBoundTypes(target as LuaBindings, serializedObject); serializedObject.ApplyModifiedProperties(); } protected virtual void ShowBindingMemberInfo() { List<string> items = new List<string>(); items.Add("<Select a binding member>"); List<string> details = new List<string>(); details.Add(""); var luaBindings = target as LuaBindings; foreach (BoundObject boundObject in luaBindings.BoundObjects) { UnityEngine.Object inspectObject = boundObject.obj; if (boundObject.component != null) { inspectObject = boundObject.component; } // Ignore empty entries if (inspectObject == null || boundObject.key.Length == 0) { continue; } BindingFlags flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public; if (!showInheritedProp.boolValue) { flags |= BindingFlags.DeclaredOnly; } // Show info for fields and properties MemberInfo[] memberInfos = inspectObject.GetType().GetMembers(flags); foreach (MemberInfo memberInfo in memberInfos) { if (memberInfo.MemberType != MemberTypes.Field && memberInfo.MemberType != MemberTypes.Property) { continue; } string item = boundObject.key + "/" + memberInfo.Name; items.Add(item); string detail = ""; detail += "Binding Key: " + boundObject.key + "\n"; detail += "Bound Object: " + boundObject.obj.name + "\n"; if (boundObject.component != null) { detail += "Bound Component: " + boundObject.component.GetType() + "\n"; } if (memberInfo.MemberType == MemberTypes.Field) { detail += "Field Name: " + memberInfo.Name + "\n"; detail += "Field Type: " + (memberInfo as FieldInfo).FieldType; } else if (memberInfo.MemberType == MemberTypes.Property) { detail += "Property Name: " + memberInfo.Name + "\n"; detail += "Property Type: " + (memberInfo as PropertyInfo).PropertyType; } details.Add(detail); } // Show info for methods foreach (MemberInfo memberInfo in memberInfos) { if (memberInfo.MemberType != MemberTypes.Method) { continue; } MethodInfo methodInfo = memberInfo as MethodInfo; if (methodInfo.IsGenericMethod) { continue; } // Build list of parameters string param = ""; foreach (ParameterInfo pi in methodInfo.GetParameters() ) { if (param == "") { param += pi.Name; } else { param += ", " + pi.Name; } } param = "(" + param + ")"; string item = boundObject.key + "/" + methodInfo.Name + param; items.Add(item); string detail = ""; detail += "Binding Key: " + boundObject.key + "\n"; detail += "Bound Object: " + boundObject.obj.name + "\n"; if (boundObject.component != null) { detail += "Bound Component: " + boundObject.component.GetType() + "\n"; } detail += "Method Name: " + methodInfo.Name + "\n"; foreach (ParameterInfo pi in methodInfo.GetParameters() ) { detail += "Parameter: " + pi.Name + " (" + pi.ParameterType.ToString() + ")\n"; } detail += "Return Type : " + methodInfo.ReturnType.ToString(); details.Add(detail); } } EditorGUILayout.BeginHorizontal(); EditorGUILayout.PrefixLabel(new GUIContent("Member Info", "Display member info for the selected binding")); GUILayout.FlexibleSpace(); // Display the popup list of bound members int selectedIndex = EditorGUILayout.Popup(0, items.ToArray()); if (selectedIndex > 0) { bindingHelpItem = items[selectedIndex].Replace("/", "."); bindingHelpDetail = details[selectedIndex]; } EditorGUILayout.EndHorizontal(); EditorGUILayout.PropertyField(showInheritedProp); // Show help info for currently selected item if (bindingHelpItem != "") { EditorGUILayout.HelpBox(bindingHelpDetail, MessageType.Info); EditorGUILayout.PrefixLabel(new GUIContent("Lua Usage", "A copyable example of how this item could be used in a Lua script.")); GUIStyle textAreaStyle = EditorStyles.textField; textAreaStyle.wordWrap = true; EditorGUILayout.TextArea(bindingHelpItem, textAreaStyle, GUILayout.MinHeight(EditorGUIUtility.singleLineHeight * 3)); } } /// <summary> /// Returns a new binding key that is guaranteed to be a valid Lua variable name and /// not to clash with any existing binding in the list. /// </summary> protected virtual string GetUniqueKey(LuaBindings luaBindings, string originalKey, int ignoreIndex = -1) { string baseKey = originalKey; // Only letters and digits allowed char[] arr = baseKey.Where(c => (char.IsLetterOrDigit(c) || c == '_')).ToArray(); baseKey = new string(arr); // No leading digits allowed baseKey = baseKey.TrimStart('0','1','2','3','4','5','6','7','8','9'); // No empty keys allowed if (baseKey.Length == 0) { baseKey = "object"; } // Build a hash of all keys currently in use HashSet<string> keyhash = new HashSet<string>(); for (int i = 0; i < luaBindings.BoundObjects.Count; ++i) { if (i == ignoreIndex) { continue; } keyhash.Add(luaBindings.BoundObjects[i].key); } // Append a suffix to make the key unique string key = baseKey; int suffix = 0; while (keyhash.Contains(key)) { suffix++; key = baseKey + suffix; } return key; } [DidReloadScripts()] protected static void DidReloadScripts() { var luaBindingsList = GameObject.FindObjectsOfType<LuaBindings>(); foreach (var luaBindings in luaBindingsList) { SerializedObject so = new SerializedObject(luaBindings); so.Update(); PopulateBoundTypes(luaBindings, so); so.ApplyModifiedProperties(); } } /// <summary> /// Update the list of bound types on the LuaBindings object. /// </summary> protected static void PopulateBoundTypes(LuaBindings luaBindings, SerializedObject so) { // Use a temp HashSet to store the list of types. // The final list is stored as a list of type strings. HashSet<System.Type> typeSet = new HashSet<System.Type>(); foreach (BoundObject boundObject in luaBindings.BoundObjects) { if (boundObject.obj == null) { continue; } AddAllSubTypes(typeSet, boundObject.obj.GetType()); if (boundObject.component != null) { AddAllSubTypes(typeSet, boundObject.component.GetType()); } } // Store the final list of types in the luaBindings object SerializedProperty boundTypesProp = so.FindProperty("boundTypes"); boundTypesProp.ClearArray(); int index = 0; foreach (System.Type t in typeSet) { boundTypesProp.InsertArrayElementAtIndex(index); SerializedProperty element = boundTypesProp.GetArrayElementAtIndex(index); element.stringValue = t.AssemblyQualifiedName; index++; } } /// <summary> /// Adds the type to the set of types, and then uses reflection to add /// all public fields, properties and methods to the set of types. /// </summary> protected static void AddAllSubTypes(HashSet<System.Type> typeSet, System.Type t) { AddSubType(typeSet, t); MemberInfo[] memberInfos = t.GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); foreach (MemberInfo memberInfo in memberInfos) { if (memberInfo.MemberType == MemberTypes.Field) { FieldInfo fieldInfo = memberInfo as FieldInfo; AddSubType(typeSet, fieldInfo.FieldType); } else if (memberInfo.MemberType == MemberTypes.Property) { PropertyInfo propertyInfo = memberInfo as PropertyInfo; AddSubType(typeSet, propertyInfo.PropertyType); } else if (memberInfo.MemberType == MemberTypes.Method) { MethodInfo methodInfo = memberInfo as MethodInfo; if (methodInfo.IsGenericMethod) { continue; } if (methodInfo.ReturnType != typeof(void)) { AddSubType(typeSet, methodInfo.ReturnType); } foreach (ParameterInfo pi in methodInfo.GetParameters() ) { AddSubType(typeSet, pi.ParameterType); } } } } /// <summary> /// Adds a single type to the type set. /// IEnumerable and IEnumerator types are handled specially. /// </summary> protected static void AddSubType(HashSet<System.Type> typeSet, System.Type t) { // MoonSharp handles IEnumerator and IEnumerable types automatically, so just // register the generic type used. if (typeof(IEnumerable).IsAssignableFrom(t) || typeof(IEnumerator).IsAssignableFrom(t)) { System.Type[] genericArguments = t.GetGenericArguments(); if (genericArguments.Count() == 1) { System.Type containedType = genericArguments[0]; // The generic type could itself be an IEnumerator or IEnumerable, so // recursively check for this case. AddSubType(typeSet, containedType); } } else if (t != typeof(System.Object)) { // Non-IEnumerable/IEnumerator types will be registered. typeSet.Add(t); } } } }