diff --git a/EasyEventEditor.cs b/EasyEventEditor.cs index 505b123..95e3df7 100755 --- a/EasyEventEditor.cs +++ b/EasyEventEditor.cs @@ -1,1602 +1,1611 @@ -/** - * MIT License - * - * Copyright (c) 2019 Merlin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -/** - * Script to make working with objects that have Unity persistent events easier. - * - * Allows five things that the default Unity event editor does not: - * - * 1. Allows reordering of events. If you want to reorder events in the default Unity editor, you need to delete events and recreate them in the desired order - * 2. Gives easy access to private methods and properties on the target object. Usually you'd otherwise need to edit the event in debug view to add private references. - * 3. Gives access to multiple components of the same type on the same object - * 4. Gives an Invoke button to execute the event in editor for debugging and testing - * 5. Adds hotkeys to event operations - */ - -#if UNITY_EDITOR - -using UnityEditor; -using UnityEditorInternal; -using UnityEngine; -using UnityEngine.Events; -using System.Reflection; -using System.Collections; -using System.Collections.Generic; -using System.Text; -using System.Linq; - -namespace Merlin -{ - -[InitializeOnLoad] -public class EasyEventEditorHandler -{ - private const string eeeOverrideEventDrawerKey = "EEE.overrideEventDrawer"; - private const string eeeShowPrivateMembersKey = "EEE.showPrivateMembers"; - private const string eeeShowInvokeFieldKey = "EEE.showInvokeField"; - private const string eeeDisplayArgumentTypeKey = "EEE.displayArgumentType"; - private const string eeeGroupSameComponentTypeKey = "EEE.groupSameComponentType"; - private const string eeeUseHotkeys = "EEE.usehotkeys"; - - private static bool patchApplied = false; - private static FieldInfo internalDrawerTypeMap = null; - private static System.Type attributeUtilityType = null; - - public class EEESettings - { - public bool overrideEventDrawer; - public bool showPrivateMembers; - public bool showInvokeField; - public bool displayArgumentType; - public bool groupSameComponentType; - public bool useHotkeys; - } - - // https://stackoverflow.com/questions/12898282/type-gettype-not-working - public static System.Type FindTypeInAllAssemblies(string qualifiedTypeName) - { - System.Type t = System.Type.GetType(qualifiedTypeName); - - if (t != null) - { - return t; - } - else - { - foreach (System.Reflection.Assembly asm in System.AppDomain.CurrentDomain.GetAssemblies()) - { - t = asm.GetType(qualifiedTypeName); - if (t != null) - return t; - } - - return null; - } - } - - static EasyEventEditorHandler() - { - EditorApplication.update += OnEditorUpdate; - } - - static void OnEditorUpdate() - { - ApplyEventPropertyDrawerPatch(); - } - - [UnityEditor.Callbacks.DidReloadScripts] - private static void OnScriptsReloaded() - { - ApplyEventPropertyDrawerPatch(); - } - - internal static FieldInfo GetDrawerTypeMap() - { - // We already have the map so skip all the reflection - if (internalDrawerTypeMap != null) - { - return internalDrawerTypeMap; - } - - System.Type scriptAttributeUtilityType = FindTypeInAllAssemblies("UnityEditor.ScriptAttributeUtility"); - - if (scriptAttributeUtilityType == null) - { - Debug.LogError("Could not find ScriptAttributeUtility in assemblies!"); - return null; - } - - // Save for later in case we need to lookup the function to populate the attributes - attributeUtilityType = scriptAttributeUtilityType; - - FieldInfo info = scriptAttributeUtilityType.GetField("s_DrawerTypeForType", BindingFlags.NonPublic | BindingFlags.Static); - - if (info == null) - { - Debug.LogError("Could not find drawer type map!"); - return null; - } - - internalDrawerTypeMap = info; - - return internalDrawerTypeMap; - } - - private static void ClearPropertyCaches() - { - if (attributeUtilityType == null) - { - Debug.LogError("UnityEditor.ScriptAttributeUtility type is null! Make sure you have called GetDrawerTypeMap() to ensure this is cached!"); - return; - } - - // Nuke handle caches so they can find our modified drawer - MethodInfo clearCacheFunc = attributeUtilityType.GetMethod("ClearGlobalCache", BindingFlags.NonPublic | BindingFlags.Static); - - if (clearCacheFunc == null) - { - Debug.LogError("Could not find cache clear method!"); - return; - } - - clearCacheFunc.Invoke(null, new object[] { }); - - FieldInfo currentCacheField = attributeUtilityType.GetField("s_CurrentCache", BindingFlags.NonPublic | BindingFlags.Static); - - if (currentCacheField == null) - { - Debug.LogError("Could not find CurrentCache field!"); - return; - } - - object currentCacheValue = currentCacheField.GetValue(null); - - if (currentCacheValue != null) - { - MethodInfo clearMethod = currentCacheValue.GetType().GetMethod("Clear", BindingFlags.Public | BindingFlags.Instance); - - if (clearMethod == null) - { - Debug.LogError("Could not find clear function for current cache!"); - return; - } - - clearMethod.Invoke(currentCacheValue, new object[] { }); - } - - System.Type inspectorWindowType = FindTypeInAllAssemblies("UnityEditor.InspectorWindow"); - - if (inspectorWindowType == null) - { - Debug.LogError("Could not find inspector window type!"); - return; - } - - FieldInfo trackerField = inspectorWindowType.GetField("m_Tracker", BindingFlags.NonPublic | BindingFlags.Instance); - FieldInfo propertyHandleCacheField = typeof(Editor).GetField("m_PropertyHandlerCache", BindingFlags.NonPublic | BindingFlags.Instance); - - if (trackerField == null || propertyHandleCacheField == null) - { - Debug.LogError("Could not find tracker field!"); - return; - } - - //FieldInfo trackerEditorsField = trackerField.GetType().GetField("") - - System.Type propertyHandlerCacheType = FindTypeInAllAssemblies("UnityEditor.PropertyHandlerCache"); - - if (propertyHandlerCacheType == null) - { - Debug.LogError("Could not find type of PropertyHandlerCache"); - return; - } - - // Secondary nuke because Unity is great and keeps a cached copy of the events for every Editor in addition to a global cache we cleared earlier. - EditorWindow[] editorWindows = Resources.FindObjectsOfTypeAll(); - - foreach (EditorWindow editor in editorWindows) - { - if (editor.GetType() == inspectorWindowType || editor.GetType().IsSubclassOf(inspectorWindowType)) - { - ActiveEditorTracker activeEditorTracker = trackerField.GetValue(editor) as ActiveEditorTracker; - - if (activeEditorTracker != null) - { - foreach (Editor activeEditor in activeEditorTracker.activeEditors) - { - if (activeEditor != null) - { - propertyHandleCacheField.SetValue(activeEditor, System.Activator.CreateInstance(propertyHandlerCacheType)); - activeEditor.Repaint(); // Force repaint to get updated drawing of property - } - } - } - } - } - } - - // Applies patch to Unity's builtin tracking for Drawers to redirect any drawers for Unity Events to our EasyEventDrawer instead. - private static void ApplyEventDrawerPatch(bool enableOverride) - { - // Call here to find the scriptAttributeUtilityType in case it's needed for when overrides are disabled - FieldInfo drawerTypeMap = GetDrawerTypeMap(); - - if (enableOverride) - { - System.Type[] mapArgs = drawerTypeMap.FieldType.GetGenericArguments(); - - System.Type keyType = mapArgs[0]; - System.Type valType = mapArgs[1]; - - if (keyType == null || valType == null) - { - Debug.LogError("Could not retrieve dictionary types!"); - return; - } - - FieldInfo drawerField = valType.GetField("drawer", BindingFlags.Public | BindingFlags.Instance); - FieldInfo typeField = valType.GetField("type", BindingFlags.Public | BindingFlags.Instance); - - if (drawerField == null || typeField == null) - { - Debug.LogError("Could not retrieve dictionary value fields!"); - return; - } - - IDictionary drawerTypeMapDict = drawerTypeMap.GetValue(null) as IDictionary; - - if (drawerTypeMapDict == null) - { - MethodInfo popAttributesFunc = attributeUtilityType.GetMethod("BuildDrawerTypeForTypeDictionary", BindingFlags.NonPublic | BindingFlags.Static); - - if (popAttributesFunc == null) - { - Debug.LogError("Could not populate attributes for override!"); - return; - } - - popAttributesFunc.Invoke(null, new object[] { }); - - // Try again now that this should be populated - drawerTypeMapDict = drawerTypeMap.GetValue(null) as IDictionary; - if (drawerTypeMapDict == null) - { - Debug.LogError("Could not get dictionary for drawer types!"); - return; - } - } - - // Replace EventDrawer handles with our custom drawer - List keysToRecreate = new List(); - - foreach (DictionaryEntry entry in drawerTypeMapDict) - { - System.Type drawerType = (System.Type)drawerField.GetValue(entry.Value); - - if (drawerType.Name == "UnityEventDrawer") - { - keysToRecreate.Add(entry.Key); - } - } - - foreach (object keyToKill in keysToRecreate) - { - drawerTypeMapDict.Remove(keyToKill); - } - - // Recreate these key-value pairs since they are structs - foreach (object keyToRecreate in keysToRecreate) - { - object newValMapping = System.Activator.CreateInstance(valType); - typeField.SetValue(newValMapping, (System.Type)keyToRecreate); - drawerField.SetValue(newValMapping, typeof(EasyEventEditorDrawer)); - - drawerTypeMapDict.Add(keyToRecreate, newValMapping); - } - } - else - { - MethodInfo popAttributesFunc = attributeUtilityType.GetMethod("BuildDrawerTypeForTypeDictionary", BindingFlags.NonPublic | BindingFlags.Static); - - if (popAttributesFunc == null) - { - Debug.LogError("Could not populate attributes for override!"); - return; - } - - // Just force the editor to repopulate the drawers without nuking afterwards. - popAttributesFunc.Invoke(null, new object[] { }); - } - - // Clear caches to force event drawers to refresh immediately. - ClearPropertyCaches(); - } - - public static void ApplyEventPropertyDrawerPatch(bool forceApply = false) - { - EEESettings settings = GetEditorSettings(); - - if (!patchApplied || forceApply) - { - ApplyEventDrawerPatch(settings.overrideEventDrawer); - patchApplied = true; - } - } - - public static EEESettings GetEditorSettings() - { - EEESettings settings = new EEESettings - { - overrideEventDrawer = EditorPrefs.GetBool(eeeOverrideEventDrawerKey, true), - showPrivateMembers = EditorPrefs.GetBool(eeeShowPrivateMembersKey, true), - showInvokeField = EditorPrefs.GetBool(eeeShowInvokeFieldKey, true), - displayArgumentType = EditorPrefs.GetBool(eeeDisplayArgumentTypeKey, true), - groupSameComponentType = EditorPrefs.GetBool(eeeGroupSameComponentTypeKey, false), - useHotkeys = EditorPrefs.GetBool(eeeUseHotkeys, true), - }; - - return settings; - } - - public static void SetEditorSettings(EEESettings settings) - { - EditorPrefs.SetBool(eeeOverrideEventDrawerKey, settings.overrideEventDrawer); - EditorPrefs.SetBool(eeeShowPrivateMembersKey, settings.showPrivateMembers); - EditorPrefs.SetBool(eeeShowInvokeFieldKey, settings.showInvokeField); - EditorPrefs.SetBool(eeeDisplayArgumentTypeKey, settings.displayArgumentType); - EditorPrefs.SetBool(eeeGroupSameComponentTypeKey, settings.groupSameComponentType); - EditorPrefs.SetBool(eeeUseHotkeys, settings.useHotkeys); - } -} - -internal class SettingsGUIContent -{ - private static GUIContent enableToggleGuiContent = new GUIContent("Enable Easy Event Editor", "Replaces the default Unity event editing context with EEE"); - private static GUIContent enablePrivateMembersGuiContent = new GUIContent("Show private properties and methods", "Exposes private/internal/obsolete properties and methods to the function list on events"); - private static GUIContent showInvokeFieldGuiContent = new GUIContent("Show invoke button on events", "Gives you a button on events that can be clicked to execute all functions on a given event"); - private static GUIContent displayArgumentTypeContent = new GUIContent("Display argument type on function name", "Shows the argument that a function takes on the function header"); - private static GUIContent groupSameComponentTypeContent = new GUIContent("Do not group components of the same type", "If you have multiple components of the same type on one object, show all components. Unity hides duplicate components by default."); - private static GUIContent useHotkeys = new GUIContent("Use hotkeys", "Adds common Unity hotkeys to event editor that operate on the currently selected event. The commands are Add (CTRL+A), Copy, Paste, Cut, Delete, and Duplicate"); - - public static void DrawSettingsButtons(EasyEventEditorHandler.EEESettings settings) - { - EditorGUI.indentLevel += 1; - - settings.overrideEventDrawer = EditorGUILayout.ToggleLeft(enableToggleGuiContent, settings.overrideEventDrawer); - - EditorGUI.BeginDisabledGroup(!settings.overrideEventDrawer); - - settings.showPrivateMembers = EditorGUILayout.ToggleLeft(enablePrivateMembersGuiContent, settings.showPrivateMembers); - settings.showInvokeField = EditorGUILayout.ToggleLeft(showInvokeFieldGuiContent, settings.showInvokeField); - settings.displayArgumentType = EditorGUILayout.ToggleLeft(displayArgumentTypeContent, settings.displayArgumentType); - settings.groupSameComponentType = !EditorGUILayout.ToggleLeft(groupSameComponentTypeContent, !settings.groupSameComponentType); - settings.useHotkeys = EditorGUILayout.ToggleLeft(useHotkeys, settings.useHotkeys); - - EditorGUI.EndDisabledGroup(); - EditorGUI.indentLevel -= 1; - } -} - -#if UNITY_2018_3_OR_NEWER -// Use the new settings provider class instead so we don't need to add extra stuff to the Edit menu -// Using the IMGUI method -static class EasyEventEditorSettingsProvider -{ - [SettingsProvider] - public static SettingsProvider CreateSettingsProvider() - { - var provider = new SettingsProvider("Preferences/Easy Event Editor", SettingsScope.User) - { - label = "Easy Event Editor", - - guiHandler = (searchContext) => - { - EasyEventEditorHandler.EEESettings settings = EasyEventEditorHandler.GetEditorSettings(); - - EditorGUI.BeginChangeCheck(); - SettingsGUIContent.DrawSettingsButtons(settings); - - if (EditorGUI.EndChangeCheck()) - { - EasyEventEditorHandler.SetEditorSettings(settings); - EasyEventEditorHandler.ApplyEventPropertyDrawerPatch(true); - } - - }, - - keywords = new HashSet(new[] { "Easy", "Event", "Editor", "Delegate", "VRChat", "EEE" }) - }; - - return provider; - } -} -#else -public class EasyEventEditorSettings : EditorWindow -{ - [MenuItem("Edit/Easy Event Editor Settings")] - static void Init() - { - EasyEventEditorSettings window = GetWindow(false, "EEE Settings"); - window.minSize = new Vector2(350, 150); - window.maxSize = new Vector2(350, 150); - window.Show(); - } - - private void OnGUI() - { - EditorGUILayout.Space(); - EditorGUILayout.LabelField("Easy Event Editor Settings", EditorStyles.boldLabel); - - EditorGUILayout.Space(); - - EasyEventEditorHandler.EEESettings settings = EasyEventEditorHandler.GetEditorSettings(); - - EditorGUI.BeginChangeCheck(); - SettingsGUIContent.DrawSettingsButtons(settings); - - if (EditorGUI.EndChangeCheck()) - { - EasyEventEditorHandler.SetEditorSettings(settings); - EasyEventEditorHandler.ApplyEventPropertyDrawerPatch(true); - } - } -} -#endif - -// Drawer that gets patched in over Unity's default event drawer -public class EasyEventEditorDrawer : PropertyDrawer -{ - class DrawerState - { - public ReorderableList reorderableList; - public int lastSelectedIndex; - - // Invoke field tracking - public string currentInvokeStrArg = ""; - public int currentInvokeIntArg = 0; - public float currentInvokeFloatArg = 0f; - public bool currentInvokeBoolArg = false; - public Object currentInvokeObjectArg = null; - } - - class FunctionData - { - public FunctionData(SerializedProperty listener, Object target = null, MethodInfo method = null, PersistentListenerMode mode = PersistentListenerMode.EventDefined) - { - listenerElement = listener; - targetObject = target; - targetMethod = method; - listenerMode = mode; - } - - public SerializedProperty listenerElement; - public Object targetObject; - public MethodInfo targetMethod; - public PersistentListenerMode listenerMode; - } - - Dictionary drawerStates = new Dictionary(); - - DrawerState currentState; - string currentLabelText; - SerializedProperty currentProperty; - SerializedProperty listenerArray; - - UnityEventBase dummyEvent; - MethodInfo cachedFindMethodInfo = null; - static EasyEventEditorHandler.EEESettings cachedSettings; - -#if UNITY_2018_4_OR_NEWER - private static UnityEventBase GetDummyEventStep(string propertyPath, System.Type propertyType, BindingFlags bindingFlags) - { - UnityEventBase dummyEvent = null; - - while (propertyPath.Length > 0) - { - if (propertyPath.StartsWith(".")) - propertyPath = propertyPath.Substring(1); - - string[] splitPath = propertyPath.Split(new char[] { '.' }, 2); - - FieldInfo newField = propertyType.GetField(splitPath[0], bindingFlags); - - if (newField == null) - break; - - propertyType = newField.FieldType; - if (propertyType.IsArray) - { - propertyType = propertyType.GetElementType(); - } - else if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(List<>)) - { - propertyType = propertyType.GetGenericArguments()[0]; - } - - if (splitPath.Length == 1) - break; - - propertyPath = splitPath[1]; - if (propertyPath.StartsWith("Array.data[")) - propertyPath = propertyPath.Split(new char[] { ']' }, 2)[1]; - } - - if (propertyType.IsSubclassOf(typeof(UnityEventBase))) - dummyEvent = System.Activator.CreateInstance(propertyType) as UnityEventBase; - - return dummyEvent; - } - - private static UnityEventBase GetDummyEvent(SerializedProperty property) - { - Object targetObject = property.serializedObject.targetObject; - if (targetObject == null) - return new UnityEvent(); - - UnityEventBase dummyEvent = null; - System.Type targetType = targetObject.GetType(); - BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; - - do - { - dummyEvent = GetDummyEventStep(property.propertyPath, targetType, bindingFlags); - bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic; - targetType = targetType.BaseType; - } while (dummyEvent == null && targetType != null); - - return dummyEvent ?? new UnityEvent(); - } -#endif - - private void PrepareState(SerializedProperty propertyForState) - { - DrawerState state; - - if (!drawerStates.TryGetValue(propertyForState.propertyPath, out state)) - { - state = new DrawerState(); - - SerializedProperty persistentListeners = propertyForState.FindPropertyRelative("m_PersistentCalls.m_Calls"); - - // The fun thing is that if Unity just made the first bool arg true internally, this whole thing would be unnecessary. - state.reorderableList = new ReorderableList(propertyForState.serializedObject, persistentListeners, true, true, true, true); - state.reorderableList.elementHeight = 43; // todo: actually find proper constant for this. - state.reorderableList.drawHeaderCallback += DrawHeaderCallback; - state.reorderableList.drawElementCallback += DrawElementCallback; - state.reorderableList.onSelectCallback += SelectCallback; - state.reorderableList.onRemoveCallback += ReorderCallback; - state.reorderableList.onAddCallback += AddEventListener; - state.reorderableList.onRemoveCallback += RemoveCallback; - - state.lastSelectedIndex = 0; - - drawerStates.Add(propertyForState.propertyPath, state); - } - - currentProperty = propertyForState; - - currentState = state; - currentState.reorderableList.index = currentState.lastSelectedIndex; - listenerArray = state.reorderableList.serializedProperty; - - // Setup dummy event -#if UNITY_2018_4_OR_NEWER - dummyEvent = GetDummyEvent(propertyForState); -#else - string eventTypeName = currentProperty.FindPropertyRelative("m_TypeName").stringValue; - System.Type eventType = EasyEventEditorHandler.FindTypeInAllAssemblies(eventTypeName); - if (eventType == null) - dummyEvent = new UnityEvent(); - else - dummyEvent = System.Activator.CreateInstance(eventType) as UnityEventBase; -#endif - - cachedSettings = EasyEventEditorHandler.GetEditorSettings(); - } - - private void HandleKeyboardShortcuts() - { - if (!cachedSettings.useHotkeys) - return; - - Event currentEvent = Event.current; - - if (!currentState.reorderableList.HasKeyboardControl()) - return; - - if (currentEvent.type == EventType.ValidateCommand) - { - if (currentEvent.commandName == "Copy" || - currentEvent.commandName == "Paste" || - currentEvent.commandName == "Cut" || - currentEvent.commandName == "Duplicate" || - currentEvent.commandName == "Delete" || - currentEvent.commandName == "SoftDelete" || - currentEvent.commandName == "SelectAll") - { - currentEvent.Use(); - } - } - else if (currentEvent.type == EventType.ExecuteCommand) - { - if (currentEvent.commandName == "Copy") - { - HandleCopy(); - currentEvent.Use(); - } - else if (currentEvent.commandName == "Paste") - { - HandlePaste(); - currentEvent.Use(); - } - else if (currentEvent.commandName == "Cut") - { - HandleCut(); - currentEvent.Use(); - } - else if (currentEvent.commandName == "Duplicate") - { - HandleDuplicate(); - currentEvent.Use(); - } - else if (currentEvent.commandName == "Delete" || currentEvent.commandName == "SoftDelete") - { - RemoveCallback(currentState.reorderableList); - currentEvent.Use(); - } - else if (currentEvent.commandName == "SelectAll") // Use Ctrl+A for add, since Ctrl+N isn't usable using command names - { - HandleAdd(); - currentEvent.Use(); - } - } - } - - private class EventClipboardStorage - { - public static SerializedObject CopiedEventProperty; - public static int CopiedEventIndex; - } - - private void HandleCopy() - { - SerializedObject serializedEvent = new SerializedObject(listenerArray.GetArrayElementAtIndex(currentState.reorderableList.index).serializedObject.targetObject); - - EventClipboardStorage.CopiedEventProperty = serializedEvent; - EventClipboardStorage.CopiedEventIndex = currentState.reorderableList.index; - } - - private void HandlePaste() - { - if (EventClipboardStorage.CopiedEventProperty == null) - return; - - SerializedProperty iterator = EventClipboardStorage.CopiedEventProperty.GetIterator(); - - if (iterator == null) - return; - - while (iterator.NextVisible(true)) - { - if (iterator != null && iterator.name == "m_PersistentCalls") - { - iterator = iterator.FindPropertyRelative("m_Calls"); - break; - } - } - - if (iterator.arraySize < (EventClipboardStorage.CopiedEventIndex + 1)) - return; - - SerializedProperty sourceProperty = iterator.GetArrayElementAtIndex(EventClipboardStorage.CopiedEventIndex); - - if (sourceProperty == null) - return; - - int targetArrayIdx = currentState.reorderableList.count > 0 ? currentState.reorderableList.index : 0; - currentState.reorderableList.serializedProperty.InsertArrayElementAtIndex(targetArrayIdx); - - SerializedProperty targetProperty = currentState.reorderableList.serializedProperty.GetArrayElementAtIndex((currentState.reorderableList.count > 0 ? currentState.reorderableList.index : 0) + 1); - ResetEventState(targetProperty); - - targetProperty.FindPropertyRelative("m_CallState").enumValueIndex = sourceProperty.FindPropertyRelative("m_CallState").enumValueIndex; - targetProperty.FindPropertyRelative("m_Target").objectReferenceValue = sourceProperty.FindPropertyRelative("m_Target").objectReferenceValue; - targetProperty.FindPropertyRelative("m_MethodName").stringValue = sourceProperty.FindPropertyRelative("m_MethodName").stringValue; - targetProperty.FindPropertyRelative("m_Mode").enumValueIndex = sourceProperty.FindPropertyRelative("m_Mode").enumValueIndex; - - SerializedProperty targetArgs = targetProperty.FindPropertyRelative("m_Arguments"); - SerializedProperty sourceArgs = sourceProperty.FindPropertyRelative("m_Arguments"); - - targetArgs.FindPropertyRelative("m_IntArgument").intValue = sourceArgs.FindPropertyRelative("m_IntArgument").intValue; - targetArgs.FindPropertyRelative("m_FloatArgument").floatValue = sourceArgs.FindPropertyRelative("m_FloatArgument").floatValue; - targetArgs.FindPropertyRelative("m_BoolArgument").boolValue = sourceArgs.FindPropertyRelative("m_BoolArgument").boolValue; - targetArgs.FindPropertyRelative("m_StringArgument").stringValue = sourceArgs.FindPropertyRelative("m_StringArgument").stringValue; - targetArgs.FindPropertyRelative("m_ObjectArgument").objectReferenceValue = sourceArgs.FindPropertyRelative("m_ObjectArgument").objectReferenceValue; - targetArgs.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName").stringValue = sourceArgs.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName").stringValue; - - currentState.reorderableList.index++; - currentState.lastSelectedIndex++; - - targetProperty.serializedObject.ApplyModifiedProperties(); - } - - private void HandleCut() - { - HandleCopy(); - RemoveCallback(currentState.reorderableList); - } - - private void HandleDuplicate() - { - if (currentState.reorderableList.count == 0) - return; - - SerializedProperty listProperty = currentState.reorderableList.serializedProperty; - - SerializedProperty eventProperty = listProperty.GetArrayElementAtIndex(currentState.reorderableList.index); - - eventProperty.DuplicateCommand(); - - currentState.reorderableList.index++; - currentState.lastSelectedIndex++; - } - - private void HandleAdd() - { - int targetIdx = currentState.reorderableList.count > 0 ? currentState.reorderableList.index : 0; - currentState.reorderableList.serializedProperty.InsertArrayElementAtIndex(targetIdx); - - SerializedProperty eventProperty = currentState.reorderableList.serializedProperty.GetArrayElementAtIndex(currentState.reorderableList.index + 1); - ResetEventState(eventProperty); - - currentState.reorderableList.index++; - currentState.lastSelectedIndex++; - } - - public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) - { - currentLabelText = label.text; - PrepareState(property); - - HandleKeyboardShortcuts(); - - if (dummyEvent == null) - return; - - if (currentState.reorderableList != null) - { - int oldIndent = EditorGUI.indentLevel; - EditorGUI.indentLevel = 0; - currentState.reorderableList.DoList(position); - EditorGUI.indentLevel = oldIndent; - } - } - - static void InvokeOnTargetEvents(MethodInfo method, object[] targets, object argValue) - { - foreach (object target in targets) - { - if (argValue != null) - method.Invoke(target, new object[] { argValue }); - else - method.Invoke(target, new object[] { }); - } - } - - void DrawInvokeField(Rect position, float headerStartOffset) - { - Rect buttonPos = position; - buttonPos.height *= 0.9f; - buttonPos.width = 51; - buttonPos.x += headerStartOffset + 2; - - Rect textPos = buttonPos; - textPos.x += 6; - textPos.width -= 12; - - Rect inputFieldPos = position; - inputFieldPos.height = buttonPos.height; - inputFieldPos.width = position.width - buttonPos.width - 3 - headerStartOffset; - inputFieldPos.x = buttonPos.x + buttonPos.width + 2; - inputFieldPos.y += 1; - - Rect inputFieldTextPlaceholder = inputFieldPos; - - System.Type[] eventInvokeArgs = GetEventParams(dummyEvent); - - GUIStyle textStyle = EditorStyles.miniLabel; - textStyle.alignment = TextAnchor.MiddleLeft; - - MethodInfo invokeMethod = InvokeFindMethod("Invoke", dummyEvent, dummyEvent, PersistentListenerMode.EventDefined); - FieldInfo serializedField = currentProperty.serializedObject.targetObject.GetType().GetField(currentProperty.name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); - - object[] invokeTargets = currentProperty.serializedObject.targetObjects.Select(target => target == null || serializedField == null ? null : serializedField.GetValue(target)).Where(f => f != null).ToArray(); - - EditorGUI.BeginDisabledGroup(invokeTargets.Length == 0 || invokeMethod == null); - - bool executeInvoke = GUI.Button(buttonPos, "", EditorStyles.miniButton); - GUI.Label(textPos, "Invoke"/* + " (" + string.Join(", ", eventInvokeArgs.Select(e => e.Name).ToArray()) + ")"*/, textStyle); - - if (eventInvokeArgs.Length > 0) - { - System.Type argType = eventInvokeArgs[0]; - - if (argType == typeof(string)) - { - currentState.currentInvokeStrArg = EditorGUI.TextField(inputFieldPos, currentState.currentInvokeStrArg); - - // Draw placeholder text - if (currentState.currentInvokeStrArg.Length == 0) - { - GUIStyle placeholderLabelStyle = EditorStyles.centeredGreyMiniLabel; - placeholderLabelStyle.alignment = TextAnchor.UpperLeft; - - GUI.Label(inputFieldTextPlaceholder, "String argument...", placeholderLabelStyle); - } - - if (executeInvoke) - InvokeOnTargetEvents(invokeMethod, invokeTargets, currentState.currentInvokeStrArg); - } - else if (argType == typeof(int)) - { - currentState.currentInvokeIntArg = EditorGUI.IntField(inputFieldPos, currentState.currentInvokeIntArg); - - if (executeInvoke) - InvokeOnTargetEvents(invokeMethod, invokeTargets, currentState.currentInvokeIntArg); - } - else if (argType == typeof(float)) - { - currentState.currentInvokeFloatArg = EditorGUI.FloatField(inputFieldPos, currentState.currentInvokeFloatArg); - - if (executeInvoke) - InvokeOnTargetEvents(invokeMethod, invokeTargets, currentState.currentInvokeFloatArg); - } - else if (argType == typeof(bool)) - { - currentState.currentInvokeBoolArg = EditorGUI.Toggle(inputFieldPos, currentState.currentInvokeBoolArg); - - if (executeInvoke) - InvokeOnTargetEvents(invokeMethod, invokeTargets, currentState.currentInvokeBoolArg); - } - else if (argType == typeof(Object)) - { - currentState.currentInvokeObjectArg = EditorGUI.ObjectField(inputFieldPos, currentState.currentInvokeObjectArg, argType, true); - - if (executeInvoke) - invokeMethod.Invoke(currentProperty.serializedObject.targetObject, new object[] { currentState.currentInvokeObjectArg }); - } - } - else if (executeInvoke) // No input arg - { - InvokeOnTargetEvents(invokeMethod, invokeTargets, null); - } - - EditorGUI.EndDisabledGroup(); - } - - public override float GetPropertyHeight(SerializedProperty property, GUIContent label) - { - PrepareState(property); - - float height = 0f; - if (currentState.reorderableList != null) - height = currentState.reorderableList.GetHeight(); - - return height; - } - - MethodInfo InvokeFindMethod(string functionName, object targetObject, UnityEventBase eventObject, PersistentListenerMode listenerMode, System.Type argType = null) - { - MethodInfo findMethod = cachedFindMethodInfo; - - if (findMethod == null) - { - // Rather not reinvent the wheel considering this function calls different functions depending on the number of args the event has... - findMethod = eventObject.GetType().GetMethod("FindMethod", BindingFlags.NonPublic | BindingFlags.Instance, null, - new System.Type[] { - typeof(string), - typeof(object), - typeof(PersistentListenerMode), - typeof(System.Type) - }, - null); - - cachedFindMethodInfo = findMethod; - } - - if (findMethod == null) - { - Debug.LogError("Could not find FindMethod function!"); - return null; - } - - return findMethod.Invoke(eventObject, new object[] { functionName, targetObject, listenerMode, argType }) as MethodInfo; - } - - System.Type[] GetEventParams(UnityEventBase eventIn) - { - MethodInfo methodInfo = InvokeFindMethod("Invoke", eventIn, eventIn, PersistentListenerMode.EventDefined); - return methodInfo.GetParameters().Select(x => x.ParameterType).ToArray(); - } - - string GetEventParamsStr(UnityEventBase eventIn) - { - StringBuilder builder = new StringBuilder(); - System.Type[] methodTypes = GetEventParams(eventIn); - - builder.Append("("); - builder.Append(string.Join(", ", methodTypes.Select(val => val.Name).ToArray())); - builder.Append(")"); - - return builder.ToString(); - } - - string GetFunctionArgStr(string functionName, object targetObject, PersistentListenerMode listenerMode, System.Type argType = null) - { - MethodInfo methodInfo = InvokeFindMethod(functionName, targetObject, dummyEvent, listenerMode, argType); - - if (methodInfo == null) - return ""; - - ParameterInfo[] parameterInfos = methodInfo.GetParameters(); - if (parameterInfos.Length == 0) - return ""; - - return GetTypeName(parameterInfos[0].ParameterType); - } - - void DrawHeaderCallback(Rect headerRect) - { - // We need to know where to position the invoke field based on the length of the title in the UI - GUIContent headerTitle = new GUIContent(string.IsNullOrEmpty(currentLabelText) ? "Event" : currentLabelText + " " + GetEventParamsStr(dummyEvent)); - float headerStartOffset = EditorStyles.label.CalcSize(headerTitle).x; - - GUI.Label(headerRect, headerTitle); - - if (cachedSettings.showInvokeField) - DrawInvokeField(headerRect, headerStartOffset); - } - - Rect[] GetElementRects(Rect rect) - { - Rect[] rects = new Rect[4]; - - rect.height = EditorGUIUtility.singleLineHeight; - rect.y += 2; - - // enabled field - rects[0] = rect; - rects[0].width *= 0.3f; - - // game object field - rects[1] = rects[0]; - rects[1].x += 1; - rects[1].width -= 2; - rects[1].y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; - - // function field - rects[2] = rect; - rects[2].xMin = rects[1].xMax + 5; - - // argument field - rects[3] = rects[2]; - rects[3].y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; - - return rects; - } - - string GetFunctionDisplayName(SerializedProperty objectProperty, SerializedProperty methodProperty, PersistentListenerMode listenerMode, System.Type argType, bool showArg) - { - string methodNameOut = "No Function"; - - if (objectProperty.objectReferenceValue == null || methodProperty.stringValue == "") - return methodNameOut; - - MethodInfo methodInfo = InvokeFindMethod(methodProperty.stringValue, objectProperty.objectReferenceValue, dummyEvent, listenerMode, argType); - string funcName = methodProperty.stringValue.StartsWith("set_") ? methodProperty.stringValue.Substring(4) : methodProperty.stringValue; - - if (methodInfo == null) - { - methodNameOut = string.Format("", objectProperty.objectReferenceValue.GetType().Name.ToString(), funcName); - return methodNameOut; - } - - string objectTypeName = objectProperty.objectReferenceValue.GetType().Name; - Component objectComponent = objectProperty.objectReferenceValue as Component; - - if (!cachedSettings.groupSameComponentType && objectComponent != null) - { - System.Type objectType = objectProperty.objectReferenceValue.GetType(); - - Component[] components = objectComponent.GetComponents(objectType); - - if (components.Length > 1) - { - int componentID = 0; - for (int i = 0; i < components.Length; i++) - { - if (components[i] == objectComponent) - { - componentID = i + 1; - break; - } - } - - objectTypeName += string.Format("({0})", componentID); - } - } - - if (showArg) - { - string functionArgStr = GetFunctionArgStr(methodProperty.stringValue, objectProperty.objectReferenceValue, listenerMode, argType); - methodNameOut = string.Format("{0}.{1} ({2})", objectTypeName, funcName, functionArgStr); - } - else - { - methodNameOut = string.Format("{0}.{1}", objectTypeName, funcName); - } - - - return methodNameOut; - } - - System.Type[] GetTypeForListenerMode(PersistentListenerMode listenerMode) - { - switch (listenerMode) - { - case PersistentListenerMode.EventDefined: - case PersistentListenerMode.Void: - return new System.Type[] { }; - case PersistentListenerMode.Object: - return new System.Type[] { typeof(Object) }; - case PersistentListenerMode.Int: - return new System.Type[] { typeof(int) }; - case PersistentListenerMode.Float: - return new System.Type[] { typeof(float) }; - case PersistentListenerMode.String: - return new System.Type[] { typeof(string) }; - case PersistentListenerMode.Bool: - return new System.Type[] { typeof(bool) }; - } - - return new System.Type[] { }; - } - - void FindValidMethods(Object targetObject, PersistentListenerMode listenerMode, List methodInfos, System.Type[] customArgTypes = null) - { - System.Type objectType = targetObject.GetType(); - - System.Type[] argTypes; - - if (listenerMode == PersistentListenerMode.EventDefined && customArgTypes != null) - argTypes = customArgTypes; - else - argTypes = GetTypeForListenerMode(listenerMode); - - List foundMethods = new List(); - - // For some reason BindingFlags.FlattenHierarchy does not seem to work, so we manually traverse the base types instead - while (objectType != null) - { - MethodInfo[] foundMethodsOnType = objectType.GetMethods(BindingFlags.Public | (cachedSettings.showPrivateMembers ? BindingFlags.NonPublic : BindingFlags.Default) | BindingFlags.Instance); - - foundMethods.AddRange(foundMethodsOnType); - - objectType = objectType.BaseType; - } - - foreach (MethodInfo methodInfo in foundMethods) - { - // Sadly we can only use functions with void return type since C# throws an error - if (methodInfo.ReturnType != typeof(void)) - continue; - - ParameterInfo[] methodParams = methodInfo.GetParameters(); - if (methodParams.Length != argTypes.Length) - continue; - - bool isValidParamMatch = true; - for (int i = 0; i < methodParams.Length; i++) - { - if (!methodParams[i].ParameterType.IsAssignableFrom(argTypes[i])/* && (argTypes[i] != typeof(int) || !methodParams[i].ParameterType.IsEnum)*/) - { - isValidParamMatch = false; - } - if (listenerMode == PersistentListenerMode.Object && argTypes[i].IsAssignableFrom(methodParams[i].ParameterType)) - { - isValidParamMatch = true; - } - } - - if (!isValidParamMatch) - continue; - - if (!cachedSettings.showPrivateMembers && methodInfo.GetCustomAttributes(typeof(System.ObsoleteAttribute), true).Length > 0) - continue; - - - FunctionData foundMethodData = new FunctionData(null, targetObject, methodInfo, listenerMode); - - methodInfos.Add(foundMethodData); - } - } - - string GetTypeName(System.Type typeToName) - { - if (typeToName == typeof(float)) - return "float"; - if (typeToName == typeof(bool)) - return "bool"; - if (typeToName == typeof(int)) - return "int"; - if (typeToName == typeof(string)) - return "string"; - - return typeToName.Name; - } - - void AddFunctionToMenu(string contentPath, SerializedProperty elementProperty, FunctionData methodData, GenericMenu menu, int componentCount, bool dynamicCall = false) - { - string functionName = (methodData.targetMethod.Name.StartsWith("set_") ? methodData.targetMethod.Name.Substring(4) : methodData.targetMethod.Name); - string argStr = string.Join(", ", methodData.targetMethod.GetParameters().Select(param => GetTypeName(param.ParameterType)).ToArray()); - - if (dynamicCall) // Cut out the args from the dynamic variation to match Unity, and the menu item won't be created if it's not unique. - { - contentPath += functionName; - } - else - { - if (methodData.targetMethod.Name.StartsWith("set_")) // If it's a property add the arg before the name - { - contentPath += argStr + " " + functionName; - } - else - { - contentPath += functionName + " (" + argStr + ")"; // Add arguments - } - } - - if (!methodData.targetMethod.IsPublic) - contentPath += " " + (methodData.targetMethod.IsPrivate ? "" : ""); - - if (methodData.targetMethod.GetCustomAttributes(typeof(System.ObsoleteAttribute), true).Length > 0) - contentPath += " "; - - methodData.listenerElement = elementProperty; - - SerializedProperty serializedTargetObject = elementProperty.FindPropertyRelative("m_Target"); - SerializedProperty serializedMethodName = elementProperty.FindPropertyRelative("m_MethodName"); - SerializedProperty serializedMode = elementProperty.FindPropertyRelative("m_Mode"); - - bool itemOn = serializedTargetObject.objectReferenceValue == methodData.targetObject && - serializedMethodName.stringValue == methodData.targetMethod.Name && - serializedMode.enumValueIndex == (int)methodData.listenerMode; - - menu.AddItem(new GUIContent(contentPath), itemOn, SetEventFunctionCallback, methodData); - } - - void BuildMenuForObject(Object targetObject, SerializedProperty elementProperty, GenericMenu menu, int componentCount = 0) - { - List methodInfos = new List(); - string contentPath = targetObject.GetType().Name + (componentCount > 0 ? string.Format("({0})", componentCount) : "") + "/"; - - FindValidMethods(targetObject, PersistentListenerMode.Void, methodInfos); - FindValidMethods(targetObject, PersistentListenerMode.Int, methodInfos); - FindValidMethods(targetObject, PersistentListenerMode.Float, methodInfos); - FindValidMethods(targetObject, PersistentListenerMode.String, methodInfos); - FindValidMethods(targetObject, PersistentListenerMode.Bool, methodInfos); - FindValidMethods(targetObject, PersistentListenerMode.Object, methodInfos); - - methodInfos = methodInfos.OrderBy(method1 => method1.targetMethod.Name.StartsWith("set_") ? 0 : 1).ThenBy((method1) => method1.targetMethod.Name).ToList(); - - // Get event args to determine if we can do a pass through of the arg to the parameter - System.Type[] eventArgs = dummyEvent.GetType().GetMethod("Invoke").GetParameters().Select(p => p.ParameterType).ToArray(); - - bool dynamicBinding = false; - - if (eventArgs.Length > 0) - { - List dynamicMethodInfos = new List(); - FindValidMethods(targetObject, PersistentListenerMode.EventDefined, dynamicMethodInfos, eventArgs); - - if (dynamicMethodInfos.Count > 0) - { - dynamicMethodInfos = dynamicMethodInfos.OrderBy(m => m.targetMethod.Name.StartsWith("set") ? 0 : 1).ThenBy(m => m.targetMethod.Name).ToList(); - - dynamicBinding = true; - - // Add dynamic header - menu.AddDisabledItem(new GUIContent(contentPath + string.Format("Dynamic {0}", GetTypeName(eventArgs[0])))); - menu.AddSeparator(contentPath); - - foreach (FunctionData dynamicMethod in dynamicMethodInfos) - { - AddFunctionToMenu(contentPath, elementProperty, dynamicMethod, menu, 0, true); - } - } - } - - // Add static header if we have dynamic bindings - if (dynamicBinding) - { - menu.AddDisabledItem(new GUIContent(contentPath + "Static Parameters")); - menu.AddSeparator(contentPath); - } - - foreach (FunctionData method in methodInfos) - { - AddFunctionToMenu(contentPath, elementProperty, method, menu, componentCount); - } - } - - class ComponentTypeCount - { - public int TotalCount = 0; - public int CurrentCount = 1; - } - - GenericMenu BuildPopupMenu(Object targetObj, SerializedProperty elementProperty, System.Type objectArgType) - { - GenericMenu menu = new GenericMenu(); - - string currentMethodName = elementProperty.FindPropertyRelative("m_MethodName").stringValue; - - menu.AddItem(new GUIContent("No Function"), string.IsNullOrEmpty(currentMethodName), ClearEventFunctionCallback, new FunctionData(elementProperty)); - menu.AddSeparator(""); - - if (targetObj is Component) - { - targetObj = (targetObj as Component).gameObject; - } - else if (!(targetObj is GameObject)) - { - // Function menu for asset objects and such - BuildMenuForObject(targetObj, elementProperty, menu); - return menu; - } - - // GameObject menu - BuildMenuForObject(targetObj, elementProperty, menu); - - Component[] components = (targetObj as GameObject).GetComponents(); - Dictionary componentTypeCounts = new Dictionary(); - - // Only get the first instance of each component type - if (cachedSettings.groupSameComponentType) - { - components = components.GroupBy(comp => comp.GetType()).Select(group => group.First()).ToArray(); - } - else // Otherwise we need to know if there are multiple components of a given type before we start going through the components since we only need numbers on component types with multiple instances. - { - foreach (Component component in components) - { - ComponentTypeCount typeCount; - if (!componentTypeCounts.TryGetValue(component.GetType(), out typeCount)) - { - typeCount = new ComponentTypeCount(); - componentTypeCounts.Add(component.GetType(), typeCount); - } - - typeCount.TotalCount++; - } - - } - - foreach (Component component in components) - { - int componentCount = 0; - - if (!cachedSettings.groupSameComponentType) - { - ComponentTypeCount typeCount = componentTypeCounts[component.GetType()]; - if (typeCount.TotalCount > 1) - componentCount = typeCount.CurrentCount++; - } - - BuildMenuForObject(component, elementProperty, menu, componentCount); - } - - return menu; - } - - // Where the event data actually gets added when you choose a function - static void SetEventFunctionCallback(object functionUserData) - { - FunctionData functionData = functionUserData as FunctionData; - - SerializedProperty serializedElement = functionData.listenerElement; - - SerializedProperty serializedTarget = serializedElement.FindPropertyRelative("m_Target"); - SerializedProperty serializedMethodName = serializedElement.FindPropertyRelative("m_MethodName"); - SerializedProperty serializedArgs = serializedElement.FindPropertyRelative("m_Arguments"); - SerializedProperty serializedMode = serializedElement.FindPropertyRelative("m_Mode"); - - SerializedProperty serializedArgAssembly = serializedArgs.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName"); - SerializedProperty serializedArgObjectValue = serializedArgs.FindPropertyRelative("m_ObjectArgument"); - - serializedTarget.objectReferenceValue = functionData.targetObject; - serializedMethodName.stringValue = functionData.targetMethod.Name; - serializedMode.enumValueIndex = (int)functionData.listenerMode; - - if (functionData.listenerMode == PersistentListenerMode.Object) - { - ParameterInfo[] methodParams = functionData.targetMethod.GetParameters(); - if (methodParams.Length == 1 && typeof(Object).IsAssignableFrom(methodParams[0].ParameterType)) - serializedArgAssembly.stringValue = methodParams[0].ParameterType.AssemblyQualifiedName; - else - serializedArgAssembly.stringValue = typeof(Object).AssemblyQualifiedName; - } - else - { - serializedArgAssembly.stringValue = typeof(Object).AssemblyQualifiedName; - serializedArgObjectValue.objectReferenceValue = null; - } - - System.Type argType = EasyEventEditorHandler.FindTypeInAllAssemblies(serializedArgAssembly.stringValue); - if (!typeof(Object).IsAssignableFrom(argType) || !argType.IsInstanceOfType(serializedArgObjectValue.objectReferenceValue)) - serializedArgObjectValue.objectReferenceValue = null; - - functionData.listenerElement.serializedObject.ApplyModifiedProperties(); - } - - static void ClearEventFunctionCallback(object functionUserData) - { - FunctionData functionData = functionUserData as FunctionData; - - functionData.listenerElement.FindPropertyRelative("m_Mode").enumValueIndex = (int)PersistentListenerMode.Void; - functionData.listenerElement.FindPropertyRelative("m_MethodName").stringValue = null; - functionData.listenerElement.serializedObject.ApplyModifiedProperties(); - } - - void DrawElementCallback(Rect rect, int index, bool active, bool focused) - { - SerializedProperty element = listenerArray.GetArrayElementAtIndex(index); - - rect.y++; - Rect[] rects = GetElementRects(rect); - - Rect enabledRect = rects[0]; - Rect gameObjectRect = rects[1]; - Rect functionRect = rects[2]; - Rect argRect = rects[3]; - - SerializedProperty serializedCallState = element.FindPropertyRelative("m_CallState"); - SerializedProperty serializedMode = element.FindPropertyRelative("m_Mode"); - SerializedProperty serializedArgs = element.FindPropertyRelative("m_Arguments"); - SerializedProperty serializedTarget = element.FindPropertyRelative("m_Target"); - SerializedProperty serializedMethod = element.FindPropertyRelative("m_MethodName"); - - Color oldColor = GUI.backgroundColor; - GUI.backgroundColor = Color.white; - - EditorGUI.PropertyField(enabledRect, serializedCallState, GUIContent.none); - - EditorGUI.BeginChangeCheck(); - - Object oldTargetObject = serializedTarget.objectReferenceValue; - - GUI.Box(gameObjectRect, GUIContent.none); - EditorGUI.PropertyField(gameObjectRect, serializedTarget, GUIContent.none); - if (EditorGUI.EndChangeCheck()) - { - Object newTargetObject = serializedTarget.objectReferenceValue; - - // Attempt to maintain the function pointer and component pointer if someone changes the target object and it has the correct component type on it. - if (oldTargetObject != null && newTargetObject != null) - { - if (oldTargetObject.GetType() != newTargetObject.GetType()) // If not an asset, if it is an asset and the same type we don't do anything - { - // If these are Unity components then the game object that they are attached to may have multiple copies of the same component type so attempt to match the count - if (typeof(Component).IsAssignableFrom(oldTargetObject.GetType()) && newTargetObject.GetType() == typeof(GameObject)) - { - GameObject oldParentObject = ((Component)oldTargetObject).gameObject; - GameObject newParentObject = (GameObject)newTargetObject; - - Component[] oldComponentList = oldParentObject.GetComponents(oldTargetObject.GetType()); - - int componentLocationOffset = 0; - for (int i = 0; i < oldComponentList.Length; ++i) - { - if (oldComponentList[i] == oldTargetObject) - break; - - if (oldComponentList[i].GetType() == oldTargetObject.GetType()) // Only take exact matches for component type since I don't want to do redo the reflection to find the methods at the moment. - componentLocationOffset++; - } - - Component[] newComponentList = newParentObject.GetComponents(oldTargetObject.GetType()); - - int newComponentIndex = 0; - int componentCount = -1; - for (int i = 0; i < newComponentList.Length; ++i) - { - if (componentCount == componentLocationOffset) - break; - - if (newComponentList[i].GetType() == oldTargetObject.GetType()) - { - newComponentIndex = i; - componentCount++; - } - } - - if (newComponentList.Length > 0 && newComponentList[newComponentIndex].GetType() == oldTargetObject.GetType()) - { - serializedTarget.objectReferenceValue = newComponentList[newComponentIndex]; - } - else - { - serializedMethod.stringValue = null; - } - } - else - { - serializedMethod.stringValue = null; - } - } - } - else - { - serializedMethod.stringValue = null; - } - } - - PersistentListenerMode mode = (PersistentListenerMode)serializedMode.enumValueIndex; - - SerializedProperty argument; - if (serializedTarget.objectReferenceValue == null || string.IsNullOrEmpty(serializedMethod.stringValue)) - mode = PersistentListenerMode.Void; - - switch (mode) - { - case PersistentListenerMode.Object: - case PersistentListenerMode.String: - case PersistentListenerMode.Bool: - case PersistentListenerMode.Float: - argument = serializedArgs.FindPropertyRelative("m_" + System.Enum.GetName(typeof(PersistentListenerMode), mode) + "Argument"); - break; - default: - argument = serializedArgs.FindPropertyRelative("m_IntArgument"); - break; - } - - string argTypeName = serializedArgs.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName").stringValue; - System.Type argType = typeof(Object); - if (!string.IsNullOrEmpty(argTypeName)) - argType = EasyEventEditorHandler.FindTypeInAllAssemblies(argTypeName) ?? typeof (Object); - - if (mode == PersistentListenerMode.Object) - { - EditorGUI.BeginChangeCheck(); - Object result = EditorGUI.ObjectField(argRect, GUIContent.none, argument.objectReferenceValue, argType, true); - if (EditorGUI.EndChangeCheck()) - argument.objectReferenceValue = result; - } - else if (mode != PersistentListenerMode.Void && mode != PersistentListenerMode.EventDefined) - EditorGUI.PropertyField(argRect, argument, GUIContent.none); - - EditorGUI.BeginDisabledGroup(serializedTarget.objectReferenceValue == null); - { - EditorGUI.BeginProperty(functionRect, GUIContent.none, serializedMethod); - - GUIContent buttonContent; - - if (EditorGUI.showMixedValue) - { - buttonContent = new GUIContent("\u2014", "Mixed Values"); - } - else - { - if (serializedTarget.objectReferenceValue == null || string.IsNullOrEmpty(serializedMethod.stringValue)) - { - buttonContent = new GUIContent("No Function"); - } - else - { - buttonContent = new GUIContent(GetFunctionDisplayName(serializedTarget, serializedMethod, mode, argType, cachedSettings.displayArgumentType)); - } - } - - if (GUI.Button(functionRect, buttonContent, EditorStyles.popup)) - { - BuildPopupMenu(serializedTarget.objectReferenceValue, element, argType).DropDown(functionRect); - } - - EditorGUI.EndProperty(); - } - EditorGUI.EndDisabledGroup(); - } - - void SelectCallback(ReorderableList list) - { - currentState.lastSelectedIndex = list.index; - } - - void ReorderCallback(ReorderableList list) - { - currentState.lastSelectedIndex = list.index; - } - - void AddEventListener(ReorderableList list) - { - if (listenerArray.hasMultipleDifferentValues) - { - foreach (Object targetObj in listenerArray.serializedObject.targetObjects) - { - SerializedObject tempSerializedObject = new SerializedObject(targetObj); - SerializedProperty listenerArrayProperty = tempSerializedObject.FindProperty(listenerArray.propertyPath); - listenerArrayProperty.arraySize += 1; - tempSerializedObject.ApplyModifiedProperties(); - } - - listenerArray.serializedObject.SetIsDifferentCacheDirty(); - listenerArray.serializedObject.Update(); - list.index = list.serializedProperty.arraySize - 1; - } - else - { - ReorderableList.defaultBehaviours.DoAddButton(list); - } - - currentState.lastSelectedIndex = list.index; - - // Init default state - SerializedProperty serialiedListener = listenerArray.GetArrayElementAtIndex(list.index); - ResetEventState(serialiedListener); - } - - void ResetEventState(SerializedProperty serialiedListener) - { - SerializedProperty serializedCallState = serialiedListener.FindPropertyRelative("m_CallState"); - SerializedProperty serializedTarget = serialiedListener.FindPropertyRelative("m_Target"); - SerializedProperty serializedMethodName = serialiedListener.FindPropertyRelative("m_MethodName"); - SerializedProperty serializedMode = serialiedListener.FindPropertyRelative("m_Mode"); - SerializedProperty serializedArgs = serialiedListener.FindPropertyRelative("m_Arguments"); - - serializedCallState.enumValueIndex = (int)UnityEventCallState.RuntimeOnly; - serializedTarget.objectReferenceValue = null; - serializedMethodName.stringValue = null; - serializedMode.enumValueIndex = (int)PersistentListenerMode.Void; - - serializedArgs.FindPropertyRelative("m_IntArgument").intValue = 0; - serializedArgs.FindPropertyRelative("m_FloatArgument").floatValue = 0f; - serializedArgs.FindPropertyRelative("m_BoolArgument").boolValue = false; - serializedArgs.FindPropertyRelative("m_StringArgument").stringValue = null; - serializedArgs.FindPropertyRelative("m_ObjectArgument").objectReferenceValue = null; - serializedArgs.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName").stringValue = null; - } - - void RemoveCallback(ReorderableList list) - { - if (currentState.reorderableList.count > 0) - { - ReorderableList.defaultBehaviours.DoRemoveButton(list); - currentState.lastSelectedIndex = list.index; - } - } -} - -} // namespace Merlin - -#endif +/** + * MIT License + * + * Copyright (c) 2019 Merlin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * Script to make working with objects that have Unity persistent events easier. + * + * Allows five things that the default Unity event editor does not: + * + * 1. Allows reordering of events. If you want to reorder events in the default Unity editor, you need to delete events and recreate them in the desired order + * 2. Gives easy access to private methods and properties on the target object. Usually you'd otherwise need to edit the event in debug view to add private references. + * 3. Gives access to multiple components of the same type on the same object + * 4. Gives an Invoke button to execute the event in editor for debugging and testing + * 5. Adds hotkeys to event operations + */ + +#if UNITY_EDITOR + +using UnityEditor; +using UnityEditorInternal; +using UnityEngine; +using UnityEngine.Events; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Linq; + +namespace Merlin +{ + +[InitializeOnLoad] +public class EasyEventEditorHandler +{ + private const string eeeOverrideEventDrawerKey = "EEE.overrideEventDrawer"; + private const string eeeShowPrivateMembersKey = "EEE.showPrivateMembers"; + private const string eeeShowInvokeFieldKey = "EEE.showInvokeField"; + private const string eeeDisplayArgumentTypeKey = "EEE.displayArgumentType"; + private const string eeeGroupSameComponentTypeKey = "EEE.groupSameComponentType"; + private const string eeeUseHotkeys = "EEE.usehotkeys"; + + private static bool patchApplied = false; + private static FieldInfo internalDrawerTypeMap = null; + private static System.Type attributeUtilityType = null; + + public class EEESettings + { + public bool overrideEventDrawer; + public bool showPrivateMembers; + public bool showInvokeField; + public bool displayArgumentType; + public bool groupSameComponentType; + public bool useHotkeys; + } + + // https://stackoverflow.com/questions/12898282/type-gettype-not-working + public static System.Type FindTypeInAllAssemblies(string qualifiedTypeName) + { + System.Type t = System.Type.GetType(qualifiedTypeName); + + if (t != null) + { + return t; + } + else + { + foreach (System.Reflection.Assembly asm in System.AppDomain.CurrentDomain.GetAssemblies()) + { + t = asm.GetType(qualifiedTypeName); + if (t != null) + return t; + } + + return null; + } + } + + static EasyEventEditorHandler() + { + EditorApplication.update += OnEditorUpdate; + } + + static void OnEditorUpdate() + { + ApplyEventPropertyDrawerPatch(); + } + + [UnityEditor.Callbacks.DidReloadScripts] + private static void OnScriptsReloaded() + { + ApplyEventPropertyDrawerPatch(); + } + + internal static FieldInfo GetDrawerTypeMap() + { + // We already have the map so skip all the reflection + if (internalDrawerTypeMap != null) + { + return internalDrawerTypeMap; + } + + System.Type scriptAttributeUtilityType = FindTypeInAllAssemblies("UnityEditor.ScriptAttributeUtility"); + + if (scriptAttributeUtilityType == null) + { + Debug.LogError("Could not find ScriptAttributeUtility in assemblies!"); + return null; + } + + // Save for later in case we need to lookup the function to populate the attributes + attributeUtilityType = scriptAttributeUtilityType; + + FieldInfo info = scriptAttributeUtilityType.GetField("s_DrawerTypeForType", BindingFlags.NonPublic | BindingFlags.Static); + + if (info == null) + { + Debug.LogError("Could not find drawer type map!"); + return null; + } + + internalDrawerTypeMap = info; + + return internalDrawerTypeMap; + } + + private static void ClearPropertyCaches() + { + if (attributeUtilityType == null) + { + Debug.LogError("UnityEditor.ScriptAttributeUtility type is null! Make sure you have called GetDrawerTypeMap() to ensure this is cached!"); + return; + } + + // Nuke handle caches so they can find our modified drawer + MethodInfo clearCacheFunc = attributeUtilityType.GetMethod("ClearGlobalCache", BindingFlags.NonPublic | BindingFlags.Static); + + if (clearCacheFunc == null) + { + Debug.LogError("Could not find cache clear method!"); + return; + } + + clearCacheFunc.Invoke(null, new object[] { }); + + FieldInfo currentCacheField = attributeUtilityType.GetField("s_CurrentCache", BindingFlags.NonPublic | BindingFlags.Static); + + if (currentCacheField == null) + { + Debug.LogError("Could not find CurrentCache field!"); + return; + } + + object currentCacheValue = currentCacheField.GetValue(null); + + if (currentCacheValue != null) + { + MethodInfo clearMethod = currentCacheValue.GetType().GetMethod("Clear", BindingFlags.Public | BindingFlags.Instance); + + if (clearMethod == null) + { + Debug.LogError("Could not find clear function for current cache!"); + return; + } + + clearMethod.Invoke(currentCacheValue, new object[] { }); + } + + System.Type inspectorWindowType = FindTypeInAllAssemblies("UnityEditor.InspectorWindow"); + + if (inspectorWindowType == null) + { + Debug.LogError("Could not find inspector window type!"); + return; + } + + FieldInfo trackerField = inspectorWindowType.GetField("m_Tracker", BindingFlags.NonPublic | BindingFlags.Instance); + FieldInfo propertyHandleCacheField = typeof(Editor).GetField("m_PropertyHandlerCache", BindingFlags.NonPublic | BindingFlags.Instance); + + if (trackerField == null || propertyHandleCacheField == null) + { + Debug.LogError("Could not find tracker field!"); + return; + } + + //FieldInfo trackerEditorsField = trackerField.GetType().GetField("") + + System.Type propertyHandlerCacheType = FindTypeInAllAssemblies("UnityEditor.PropertyHandlerCache"); + + if (propertyHandlerCacheType == null) + { + Debug.LogError("Could not find type of PropertyHandlerCache"); + return; + } + + // Secondary nuke because Unity is great and keeps a cached copy of the events for every Editor in addition to a global cache we cleared earlier. + EditorWindow[] editorWindows = Resources.FindObjectsOfTypeAll(); + + foreach (EditorWindow editor in editorWindows) + { + if (editor.GetType() == inspectorWindowType || editor.GetType().IsSubclassOf(inspectorWindowType)) + { + ActiveEditorTracker activeEditorTracker = trackerField.GetValue(editor) as ActiveEditorTracker; + + if (activeEditorTracker != null) + { + foreach (Editor activeEditor in activeEditorTracker.activeEditors) + { + if (activeEditor != null) + { + propertyHandleCacheField.SetValue(activeEditor, System.Activator.CreateInstance(propertyHandlerCacheType)); + activeEditor.Repaint(); // Force repaint to get updated drawing of property + } + } + } + } + } + } + + // Applies patch to Unity's builtin tracking for Drawers to redirect any drawers for Unity Events to our EasyEventDrawer instead. + private static void ApplyEventDrawerPatch(bool enableOverride) + { + // Call here to find the scriptAttributeUtilityType in case it's needed for when overrides are disabled + FieldInfo drawerTypeMap = GetDrawerTypeMap(); + + if (enableOverride) + { + System.Type[] mapArgs = drawerTypeMap.FieldType.GetGenericArguments(); + + System.Type keyType = mapArgs[0]; + System.Type valType = mapArgs[1]; + + if (keyType == null || valType == null) + { + Debug.LogError("Could not retrieve dictionary types!"); + return; + } + + FieldInfo drawerField = valType.GetField("drawer", BindingFlags.Public | BindingFlags.Instance); + FieldInfo typeField = valType.GetField("type", BindingFlags.Public | BindingFlags.Instance); + + if (drawerField == null || typeField == null) + { + Debug.LogError("Could not retrieve dictionary value fields!"); + return; + } + + IDictionary drawerTypeMapDict = drawerTypeMap.GetValue(null) as IDictionary; + + if (drawerTypeMapDict == null) + { + MethodInfo popAttributesFunc = attributeUtilityType.GetMethod("BuildDrawerTypeForTypeDictionary", BindingFlags.NonPublic | BindingFlags.Static); + + if (popAttributesFunc == null) + { + Debug.LogError("Could not populate attributes for override!"); + return; + } + + popAttributesFunc.Invoke(null, new object[] { }); + + // Try again now that this should be populated + drawerTypeMapDict = drawerTypeMap.GetValue(null) as IDictionary; + if (drawerTypeMapDict == null) + { + Debug.LogError("Could not get dictionary for drawer types!"); + return; + } + } + + // Replace EventDrawer handles with our custom drawer + List keysToRecreate = new List(); + + foreach (DictionaryEntry entry in drawerTypeMapDict) + { + System.Type drawerType = (System.Type)drawerField.GetValue(entry.Value); + + if (drawerType.Name == "UnityEventDrawer") + { + keysToRecreate.Add(entry.Key); + } + } + + foreach (object keyToKill in keysToRecreate) + { + drawerTypeMapDict.Remove(keyToKill); + } + + // Recreate these key-value pairs since they are structs + foreach (object keyToRecreate in keysToRecreate) + { + object newValMapping = System.Activator.CreateInstance(valType); + typeField.SetValue(newValMapping, (System.Type)keyToRecreate); + drawerField.SetValue(newValMapping, typeof(EasyEventEditorDrawer)); + + drawerTypeMapDict.Add(keyToRecreate, newValMapping); + } + } + else + { + MethodInfo popAttributesFunc = attributeUtilityType.GetMethod("BuildDrawerTypeForTypeDictionary", BindingFlags.NonPublic | BindingFlags.Static); + + if (popAttributesFunc == null) + { + Debug.LogError("Could not populate attributes for override!"); + return; + } + + // Just force the editor to repopulate the drawers without nuking afterwards. + popAttributesFunc.Invoke(null, new object[] { }); + } + + // Clear caches to force event drawers to refresh immediately. + ClearPropertyCaches(); + } + + public static void ApplyEventPropertyDrawerPatch(bool forceApply = false) + { + EEESettings settings = GetEditorSettings(); + + if (!patchApplied || forceApply) + { + ApplyEventDrawerPatch(settings.overrideEventDrawer); + patchApplied = true; + } + } + + public static EEESettings GetEditorSettings() + { + EEESettings settings = new EEESettings + { + overrideEventDrawer = EditorPrefs.GetBool(eeeOverrideEventDrawerKey, true), + showPrivateMembers = EditorPrefs.GetBool(eeeShowPrivateMembersKey, true), + showInvokeField = EditorPrefs.GetBool(eeeShowInvokeFieldKey, true), + displayArgumentType = EditorPrefs.GetBool(eeeDisplayArgumentTypeKey, true), + groupSameComponentType = EditorPrefs.GetBool(eeeGroupSameComponentTypeKey, false), + useHotkeys = EditorPrefs.GetBool(eeeUseHotkeys, true), + }; + + return settings; + } + + public static void SetEditorSettings(EEESettings settings) + { + EditorPrefs.SetBool(eeeOverrideEventDrawerKey, settings.overrideEventDrawer); + EditorPrefs.SetBool(eeeShowPrivateMembersKey, settings.showPrivateMembers); + EditorPrefs.SetBool(eeeShowInvokeFieldKey, settings.showInvokeField); + EditorPrefs.SetBool(eeeDisplayArgumentTypeKey, settings.displayArgumentType); + EditorPrefs.SetBool(eeeGroupSameComponentTypeKey, settings.groupSameComponentType); + EditorPrefs.SetBool(eeeUseHotkeys, settings.useHotkeys); + } +} + +internal class SettingsGUIContent +{ + private static GUIContent enableToggleGuiContent = new GUIContent("Enable Easy Event Editor", "Replaces the default Unity event editing context with EEE"); + private static GUIContent enablePrivateMembersGuiContent = new GUIContent("Show private properties and methods", "Exposes private/internal/obsolete properties and methods to the function list on events"); + private static GUIContent showInvokeFieldGuiContent = new GUIContent("Show invoke button on events", "Gives you a button on events that can be clicked to execute all functions on a given event"); + private static GUIContent displayArgumentTypeContent = new GUIContent("Display argument type on function name", "Shows the argument that a function takes on the function header"); + private static GUIContent groupSameComponentTypeContent = new GUIContent("Do not group components of the same type", "If you have multiple components of the same type on one object, show all components. Unity hides duplicate components by default."); + private static GUIContent useHotkeys = new GUIContent("Use hotkeys", "Adds common Unity hotkeys to event editor that operate on the currently selected event. The commands are Add (CTRL+A), Copy, Paste, Cut, Delete, and Duplicate"); + + public static void DrawSettingsButtons(EasyEventEditorHandler.EEESettings settings) + { + EditorGUI.indentLevel += 1; + + settings.overrideEventDrawer = EditorGUILayout.ToggleLeft(enableToggleGuiContent, settings.overrideEventDrawer); + + EditorGUI.BeginDisabledGroup(!settings.overrideEventDrawer); + + settings.showPrivateMembers = EditorGUILayout.ToggleLeft(enablePrivateMembersGuiContent, settings.showPrivateMembers); + settings.showInvokeField = EditorGUILayout.ToggleLeft(showInvokeFieldGuiContent, settings.showInvokeField); + settings.displayArgumentType = EditorGUILayout.ToggleLeft(displayArgumentTypeContent, settings.displayArgumentType); + settings.groupSameComponentType = !EditorGUILayout.ToggleLeft(groupSameComponentTypeContent, !settings.groupSameComponentType); + settings.useHotkeys = EditorGUILayout.ToggleLeft(useHotkeys, settings.useHotkeys); + + EditorGUI.EndDisabledGroup(); + EditorGUI.indentLevel -= 1; + } +} + +#if UNITY_2018_3_OR_NEWER +// Use the new settings provider class instead so we don't need to add extra stuff to the Edit menu +// Using the IMGUI method +static class EasyEventEditorSettingsProvider +{ + [SettingsProvider] + public static SettingsProvider CreateSettingsProvider() + { + var provider = new SettingsProvider("Preferences/Easy Event Editor", SettingsScope.User) + { + label = "Easy Event Editor", + + guiHandler = (searchContext) => + { + EasyEventEditorHandler.EEESettings settings = EasyEventEditorHandler.GetEditorSettings(); + + EditorGUI.BeginChangeCheck(); + SettingsGUIContent.DrawSettingsButtons(settings); + + if (EditorGUI.EndChangeCheck()) + { + EasyEventEditorHandler.SetEditorSettings(settings); + EasyEventEditorHandler.ApplyEventPropertyDrawerPatch(true); + } + + }, + + keywords = new HashSet(new[] { "Easy", "Event", "Editor", "Delegate", "VRChat", "EEE" }) + }; + + return provider; + } +} +#else +public class EasyEventEditorSettings : EditorWindow +{ + [MenuItem("Edit/Easy Event Editor Settings")] + static void Init() + { + EasyEventEditorSettings window = GetWindow(false, "EEE Settings"); + window.minSize = new Vector2(350, 150); + window.maxSize = new Vector2(350, 150); + window.Show(); + } + + private void OnGUI() + { + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Easy Event Editor Settings", EditorStyles.boldLabel); + + EditorGUILayout.Space(); + + EasyEventEditorHandler.EEESettings settings = EasyEventEditorHandler.GetEditorSettings(); + + EditorGUI.BeginChangeCheck(); + SettingsGUIContent.DrawSettingsButtons(settings); + + if (EditorGUI.EndChangeCheck()) + { + EasyEventEditorHandler.SetEditorSettings(settings); + EasyEventEditorHandler.ApplyEventPropertyDrawerPatch(true); + } + } +} +#endif + +// Drawer that gets patched in over Unity's default event drawer +public class EasyEventEditorDrawer : PropertyDrawer +{ + class DrawerState + { + public ReorderableList reorderableList; + public int lastSelectedIndex; + + // Invoke field tracking + public string currentInvokeStrArg = ""; + public int currentInvokeIntArg = 0; + public float currentInvokeFloatArg = 0f; + public bool currentInvokeBoolArg = false; + public Object currentInvokeObjectArg = null; + } + + class FunctionData + { + public FunctionData(SerializedProperty listener, Object target = null, MethodInfo method = null, PersistentListenerMode mode = PersistentListenerMode.EventDefined) + { + listenerElement = listener; + targetObject = target; + targetMethod = method; + listenerMode = mode; + } + + public SerializedProperty listenerElement; + public Object targetObject; + public MethodInfo targetMethod; + public PersistentListenerMode listenerMode; + } + + Dictionary drawerStates = new Dictionary(); + + DrawerState currentState; + string currentLabelText; + SerializedProperty currentProperty; + SerializedProperty listenerArray; + + UnityEventBase dummyEvent; + MethodInfo cachedFindMethodInfo = null; + static EasyEventEditorHandler.EEESettings cachedSettings; + +#if UNITY_2018_4_OR_NEWER + private static UnityEventBase GetDummyEventStep(string propertyPath, System.Type propertyType, BindingFlags bindingFlags) + { + UnityEventBase dummyEvent = null; + + while (propertyPath.Length > 0) + { + if (propertyPath.StartsWith(".")) + propertyPath = propertyPath.Substring(1); + + string[] splitPath = propertyPath.Split(new char[] { '.' }, 2); + + FieldInfo newField = propertyType.GetField(splitPath[0], bindingFlags); + + if (newField == null) + break; + + propertyType = newField.FieldType; + if (propertyType.IsArray) + { + propertyType = propertyType.GetElementType(); + } + else if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(List<>)) + { + propertyType = propertyType.GetGenericArguments()[0]; + } + + if (splitPath.Length == 1) + break; + + propertyPath = splitPath[1]; + if (propertyPath.StartsWith("Array.data[")) + propertyPath = propertyPath.Split(new char[] { ']' }, 2)[1]; + } + + if (propertyType.IsSubclassOf(typeof(UnityEventBase))) + dummyEvent = System.Activator.CreateInstance(propertyType) as UnityEventBase; + + return dummyEvent; + } + + private static UnityEventBase GetDummyEvent(SerializedProperty property) + { + Object targetObject = property.serializedObject.targetObject; + if (targetObject == null) + return new UnityEvent(); + + UnityEventBase dummyEvent = null; + System.Type targetType = targetObject.GetType(); + BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + do + { + dummyEvent = GetDummyEventStep(property.propertyPath, targetType, bindingFlags); + bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic; + targetType = targetType.BaseType; + } while (dummyEvent == null && targetType != null); + + return dummyEvent ?? new UnityEvent(); + } +#endif + + private void PrepareState(SerializedProperty propertyForState) + { + DrawerState state; + + if (!drawerStates.TryGetValue(propertyForState.propertyPath, out state)) + { + state = new DrawerState(); + + SerializedProperty persistentListeners = propertyForState.FindPropertyRelative("m_PersistentCalls.m_Calls"); + + // The fun thing is that if Unity just made the first bool arg true internally, this whole thing would be unnecessary. + state.reorderableList = new ReorderableList(propertyForState.serializedObject, persistentListeners, true, true, true, true); + state.reorderableList.elementHeight = 43; // todo: actually find proper constant for this. + state.reorderableList.drawHeaderCallback += DrawHeaderCallback; + state.reorderableList.drawElementCallback += DrawElementCallback; + state.reorderableList.onSelectCallback += SelectCallback; + state.reorderableList.onRemoveCallback += ReorderCallback; + state.reorderableList.onAddCallback += AddEventListener; + state.reorderableList.onRemoveCallback += RemoveCallback; + + state.lastSelectedIndex = 0; + + drawerStates.Add(propertyForState.propertyPath, state); + } + + currentProperty = propertyForState; + + currentState = state; + currentState.reorderableList.index = currentState.lastSelectedIndex; + listenerArray = state.reorderableList.serializedProperty; + + // Setup dummy event +#if UNITY_2018_4_OR_NEWER + dummyEvent = GetDummyEvent(propertyForState); +#else + string eventTypeName = currentProperty.FindPropertyRelative("m_TypeName").stringValue; + System.Type eventType = EasyEventEditorHandler.FindTypeInAllAssemblies(eventTypeName); + if (eventType == null) + dummyEvent = new UnityEvent(); + else + dummyEvent = System.Activator.CreateInstance(eventType) as UnityEventBase; +#endif + + cachedSettings = EasyEventEditorHandler.GetEditorSettings(); + } + + private void HandleKeyboardShortcuts() + { + if (!cachedSettings.useHotkeys) + return; + + Event currentEvent = Event.current; + + if (!currentState.reorderableList.HasKeyboardControl()) + return; + + if (currentEvent.type == EventType.ValidateCommand) + { + if (currentEvent.commandName == "Copy" || + currentEvent.commandName == "Paste" || + currentEvent.commandName == "Cut" || + currentEvent.commandName == "Duplicate" || + currentEvent.commandName == "Delete" || + currentEvent.commandName == "SoftDelete" || + currentEvent.commandName == "SelectAll") + { + currentEvent.Use(); + } + } + else if (currentEvent.type == EventType.ExecuteCommand) + { + if (currentEvent.commandName == "Copy") + { + HandleCopy(); + currentEvent.Use(); + } + else if (currentEvent.commandName == "Paste") + { + HandlePaste(); + currentEvent.Use(); + } + else if (currentEvent.commandName == "Cut") + { + HandleCut(); + currentEvent.Use(); + } + else if (currentEvent.commandName == "Duplicate") + { + HandleDuplicate(); + currentEvent.Use(); + } + else if (currentEvent.commandName == "Delete" || currentEvent.commandName == "SoftDelete") + { + RemoveCallback(currentState.reorderableList); + currentEvent.Use(); + } + else if (currentEvent.commandName == "SelectAll") // Use Ctrl+A for add, since Ctrl+N isn't usable using command names + { + HandleAdd(); + currentEvent.Use(); + } + } + } + + private class EventClipboardStorage + { + public static SerializedObject CopiedEventProperty; + public static int CopiedEventIndex; + } + + private void HandleCopy() + { + SerializedObject serializedEvent = new SerializedObject(listenerArray.GetArrayElementAtIndex(currentState.reorderableList.index).serializedObject.targetObject); + + EventClipboardStorage.CopiedEventProperty = serializedEvent; + EventClipboardStorage.CopiedEventIndex = currentState.reorderableList.index; + } + + private void HandlePaste() + { + if (EventClipboardStorage.CopiedEventProperty == null) + return; + + SerializedProperty iterator = EventClipboardStorage.CopiedEventProperty.GetIterator(); + + if (iterator == null) + return; + + while (iterator.NextVisible(true)) + { + if (iterator != null && iterator.name == "m_PersistentCalls") + { + iterator = iterator.FindPropertyRelative("m_Calls"); + break; + } + } + + if (iterator.arraySize < (EventClipboardStorage.CopiedEventIndex + 1)) + return; + + SerializedProperty sourceProperty = iterator.GetArrayElementAtIndex(EventClipboardStorage.CopiedEventIndex); + + if (sourceProperty == null) + return; + + int targetArrayIdx = currentState.reorderableList.count > 0 ? currentState.reorderableList.index : 0; + currentState.reorderableList.serializedProperty.InsertArrayElementAtIndex(targetArrayIdx); + + SerializedProperty targetProperty = currentState.reorderableList.serializedProperty.GetArrayElementAtIndex((currentState.reorderableList.count > 0 ? currentState.reorderableList.index : 0) + 1); + ResetEventState(targetProperty); + + targetProperty.FindPropertyRelative("m_CallState").enumValueIndex = sourceProperty.FindPropertyRelative("m_CallState").enumValueIndex; + targetProperty.FindPropertyRelative("m_Target").objectReferenceValue = sourceProperty.FindPropertyRelative("m_Target").objectReferenceValue; + targetProperty.FindPropertyRelative("m_MethodName").stringValue = sourceProperty.FindPropertyRelative("m_MethodName").stringValue; + targetProperty.FindPropertyRelative("m_Mode").enumValueIndex = sourceProperty.FindPropertyRelative("m_Mode").enumValueIndex; + + SerializedProperty targetArgs = targetProperty.FindPropertyRelative("m_Arguments"); + SerializedProperty sourceArgs = sourceProperty.FindPropertyRelative("m_Arguments"); + + targetArgs.FindPropertyRelative("m_IntArgument").intValue = sourceArgs.FindPropertyRelative("m_IntArgument").intValue; + targetArgs.FindPropertyRelative("m_FloatArgument").floatValue = sourceArgs.FindPropertyRelative("m_FloatArgument").floatValue; + targetArgs.FindPropertyRelative("m_BoolArgument").boolValue = sourceArgs.FindPropertyRelative("m_BoolArgument").boolValue; + targetArgs.FindPropertyRelative("m_StringArgument").stringValue = sourceArgs.FindPropertyRelative("m_StringArgument").stringValue; + targetArgs.FindPropertyRelative("m_ObjectArgument").objectReferenceValue = sourceArgs.FindPropertyRelative("m_ObjectArgument").objectReferenceValue; + targetArgs.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName").stringValue = sourceArgs.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName").stringValue; + + currentState.reorderableList.index++; + currentState.lastSelectedIndex++; + + targetProperty.serializedObject.ApplyModifiedProperties(); + } + + private void HandleCut() + { + HandleCopy(); + RemoveCallback(currentState.reorderableList); + } + + private void HandleDuplicate() + { + if (currentState.reorderableList.count == 0) + return; + + SerializedProperty listProperty = currentState.reorderableList.serializedProperty; + + SerializedProperty eventProperty = listProperty.GetArrayElementAtIndex(currentState.reorderableList.index); + + eventProperty.DuplicateCommand(); + + currentState.reorderableList.index++; + currentState.lastSelectedIndex++; + } + + private void HandleAdd() + { + int targetIdx = currentState.reorderableList.count > 0 ? currentState.reorderableList.index : 0; + currentState.reorderableList.serializedProperty.InsertArrayElementAtIndex(targetIdx); + + SerializedProperty eventProperty = currentState.reorderableList.serializedProperty.GetArrayElementAtIndex(currentState.reorderableList.index + 1); + ResetEventState(eventProperty); + + currentState.reorderableList.index++; + currentState.lastSelectedIndex++; + } + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + currentLabelText = label.text; + PrepareState(property); + + HandleKeyboardShortcuts(); + + if (dummyEvent == null) + return; + + if (currentState.reorderableList != null) + { + int oldIndent = EditorGUI.indentLevel; + EditorGUI.indentLevel = 0; + currentState.reorderableList.DoList(position); + EditorGUI.indentLevel = oldIndent; + } + } + + static void InvokeOnTargetEvents(MethodInfo method, object[] targets, object argValue) + { + foreach (object target in targets) + { + if (argValue != null) + method.Invoke(target, new object[] { argValue }); + else + method.Invoke(target, new object[] { }); + } + } + + void DrawInvokeField(Rect position, float headerStartOffset) + { + Rect buttonPos = position; + buttonPos.height *= 0.9f; + buttonPos.width = 51; + buttonPos.x += headerStartOffset + 2; + + Rect textPos = buttonPos; + textPos.x += 6; + textPos.width -= 12; + + Rect inputFieldPos = position; + inputFieldPos.height = buttonPos.height; + inputFieldPos.width = position.width - buttonPos.width - 3 - headerStartOffset; + inputFieldPos.x = buttonPos.x + buttonPos.width + 2; + inputFieldPos.y += 1; + + Rect inputFieldTextPlaceholder = inputFieldPos; + + System.Type[] eventInvokeArgs = GetEventParams(dummyEvent); + + GUIStyle textStyle = EditorStyles.miniLabel; + textStyle.alignment = TextAnchor.MiddleLeft; + + MethodInfo invokeMethod = InvokeFindMethod("Invoke", dummyEvent, dummyEvent, PersistentListenerMode.EventDefined); + FieldInfo serializedField = currentProperty.serializedObject.targetObject.GetType().GetField(currentProperty.name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + + object[] invokeTargets = currentProperty.serializedObject.targetObjects.Select(target => target == null || serializedField == null ? null : serializedField.GetValue(target)).Where(f => f != null).ToArray(); + + EditorGUI.BeginDisabledGroup(invokeTargets.Length == 0 || invokeMethod == null); + + bool executeInvoke = GUI.Button(buttonPos, "", EditorStyles.miniButton); + GUI.Label(textPos, "Invoke"/* + " (" + string.Join(", ", eventInvokeArgs.Select(e => e.Name).ToArray()) + ")"*/, textStyle); + + if (eventInvokeArgs.Length > 0) + { + System.Type argType = eventInvokeArgs[0]; + + if (argType == typeof(string)) + { + currentState.currentInvokeStrArg = EditorGUI.TextField(inputFieldPos, currentState.currentInvokeStrArg); + + // Draw placeholder text + if (currentState.currentInvokeStrArg.Length == 0) + { + GUIStyle placeholderLabelStyle = EditorStyles.centeredGreyMiniLabel; + placeholderLabelStyle.alignment = TextAnchor.UpperLeft; + + GUI.Label(inputFieldTextPlaceholder, "String argument...", placeholderLabelStyle); + } + + if (executeInvoke) + InvokeOnTargetEvents(invokeMethod, invokeTargets, currentState.currentInvokeStrArg); + } + else if (argType == typeof(int)) + { + currentState.currentInvokeIntArg = EditorGUI.IntField(inputFieldPos, currentState.currentInvokeIntArg); + + if (executeInvoke) + InvokeOnTargetEvents(invokeMethod, invokeTargets, currentState.currentInvokeIntArg); + } + else if (argType == typeof(float)) + { + currentState.currentInvokeFloatArg = EditorGUI.FloatField(inputFieldPos, currentState.currentInvokeFloatArg); + + if (executeInvoke) + InvokeOnTargetEvents(invokeMethod, invokeTargets, currentState.currentInvokeFloatArg); + } + else if (argType == typeof(bool)) + { + currentState.currentInvokeBoolArg = EditorGUI.Toggle(inputFieldPos, currentState.currentInvokeBoolArg); + + if (executeInvoke) + InvokeOnTargetEvents(invokeMethod, invokeTargets, currentState.currentInvokeBoolArg); + } + else if (argType == typeof(Object)) + { + currentState.currentInvokeObjectArg = EditorGUI.ObjectField(inputFieldPos, currentState.currentInvokeObjectArg, argType, true); + + if (executeInvoke) + invokeMethod.Invoke(currentProperty.serializedObject.targetObject, new object[] { currentState.currentInvokeObjectArg }); + } + } + else if (executeInvoke) // No input arg + { + InvokeOnTargetEvents(invokeMethod, invokeTargets, null); + } + + EditorGUI.EndDisabledGroup(); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + PrepareState(property); + + float height = 0f; + if (currentState.reorderableList != null) + height = currentState.reorderableList.GetHeight(); + + return height; + } + + MethodInfo InvokeFindMethod(string functionName, object targetObject, UnityEventBase eventObject, PersistentListenerMode listenerMode, System.Type argType = null) + { + MethodInfo findMethod = cachedFindMethodInfo; + + if (findMethod == null) + { + // Rather not reinvent the wheel considering this function calls different functions depending on the number of args the event has... + // Unity 2020.1 changed the function signature for the FindMethod method (the second parameter is a Type instead of an object) + findMethod = eventObject.GetType().GetMethod("FindMethod", BindingFlags.NonPublic | BindingFlags.Instance, null, + new System.Type[] { + typeof(string), +#if UNITY_2020_1_OR_NEWER + typeof(System.Type), +#else + typeof(object), +#endif + typeof(PersistentListenerMode), + typeof(System.Type) + }, + null); + + cachedFindMethodInfo = findMethod; + } + + if (findMethod == null) + { + Debug.LogError("Could not find FindMethod function!"); + return null; + } + +#if UNITY_2020_1_OR_NEWER + return findMethod.Invoke(eventObject, new object[] {functionName, targetObject?.GetType(), listenerMode, argType }) as MethodInfo; +#else + return findMethod.Invoke(eventObject, new object[] {functionName, targetObject, listenerMode, argType }) as MethodInfo; +#endif + } + + System.Type[] GetEventParams(UnityEventBase eventIn) + { + MethodInfo methodInfo = InvokeFindMethod("Invoke", eventIn, eventIn, PersistentListenerMode.EventDefined); + return methodInfo.GetParameters().Select(x => x.ParameterType).ToArray(); + } + + string GetEventParamsStr(UnityEventBase eventIn) + { + StringBuilder builder = new StringBuilder(); + System.Type[] methodTypes = GetEventParams(eventIn); + + builder.Append("("); + builder.Append(string.Join(", ", methodTypes.Select(val => val.Name).ToArray())); + builder.Append(")"); + + return builder.ToString(); + } + + string GetFunctionArgStr(string functionName, object targetObject, PersistentListenerMode listenerMode, System.Type argType = null) + { + MethodInfo methodInfo = InvokeFindMethod(functionName, targetObject, dummyEvent, listenerMode, argType); + + if (methodInfo == null) + return ""; + + ParameterInfo[] parameterInfos = methodInfo.GetParameters(); + if (parameterInfos.Length == 0) + return ""; + + return GetTypeName(parameterInfos[0].ParameterType); + } + + void DrawHeaderCallback(Rect headerRect) + { + // We need to know where to position the invoke field based on the length of the title in the UI + GUIContent headerTitle = new GUIContent(string.IsNullOrEmpty(currentLabelText) ? "Event" : currentLabelText + " " + GetEventParamsStr(dummyEvent)); + float headerStartOffset = EditorStyles.label.CalcSize(headerTitle).x; + + GUI.Label(headerRect, headerTitle); + + if (cachedSettings.showInvokeField) + DrawInvokeField(headerRect, headerStartOffset); + } + + Rect[] GetElementRects(Rect rect) + { + Rect[] rects = new Rect[4]; + + rect.height = EditorGUIUtility.singleLineHeight; + rect.y += 2; + + // enabled field + rects[0] = rect; + rects[0].width *= 0.3f; + + // game object field + rects[1] = rects[0]; + rects[1].x += 1; + rects[1].width -= 2; + rects[1].y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; + + // function field + rects[2] = rect; + rects[2].xMin = rects[1].xMax + 5; + + // argument field + rects[3] = rects[2]; + rects[3].y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; + + return rects; + } + + string GetFunctionDisplayName(SerializedProperty objectProperty, SerializedProperty methodProperty, PersistentListenerMode listenerMode, System.Type argType, bool showArg) + { + string methodNameOut = "No Function"; + + if (objectProperty.objectReferenceValue == null || methodProperty.stringValue == "") + return methodNameOut; + + MethodInfo methodInfo = InvokeFindMethod(methodProperty.stringValue, objectProperty.objectReferenceValue, dummyEvent, listenerMode, argType); + string funcName = methodProperty.stringValue.StartsWith("set_") ? methodProperty.stringValue.Substring(4) : methodProperty.stringValue; + + if (methodInfo == null) + { + methodNameOut = string.Format("", objectProperty.objectReferenceValue.GetType().Name.ToString(), funcName); + return methodNameOut; + } + + string objectTypeName = objectProperty.objectReferenceValue.GetType().Name; + Component objectComponent = objectProperty.objectReferenceValue as Component; + + if (!cachedSettings.groupSameComponentType && objectComponent != null) + { + System.Type objectType = objectProperty.objectReferenceValue.GetType(); + + Component[] components = objectComponent.GetComponents(objectType); + + if (components.Length > 1) + { + int componentID = 0; + for (int i = 0; i < components.Length; i++) + { + if (components[i] == objectComponent) + { + componentID = i + 1; + break; + } + } + + objectTypeName += string.Format("({0})", componentID); + } + } + + if (showArg) + { + string functionArgStr = GetFunctionArgStr(methodProperty.stringValue, objectProperty.objectReferenceValue, listenerMode, argType); + methodNameOut = string.Format("{0}.{1} ({2})", objectTypeName, funcName, functionArgStr); + } + else + { + methodNameOut = string.Format("{0}.{1}", objectTypeName, funcName); + } + + + return methodNameOut; + } + + System.Type[] GetTypeForListenerMode(PersistentListenerMode listenerMode) + { + switch (listenerMode) + { + case PersistentListenerMode.EventDefined: + case PersistentListenerMode.Void: + return new System.Type[] { }; + case PersistentListenerMode.Object: + return new System.Type[] { typeof(Object) }; + case PersistentListenerMode.Int: + return new System.Type[] { typeof(int) }; + case PersistentListenerMode.Float: + return new System.Type[] { typeof(float) }; + case PersistentListenerMode.String: + return new System.Type[] { typeof(string) }; + case PersistentListenerMode.Bool: + return new System.Type[] { typeof(bool) }; + } + + return new System.Type[] { }; + } + + void FindValidMethods(Object targetObject, PersistentListenerMode listenerMode, List methodInfos, System.Type[] customArgTypes = null) + { + System.Type objectType = targetObject.GetType(); + + System.Type[] argTypes; + + if (listenerMode == PersistentListenerMode.EventDefined && customArgTypes != null) + argTypes = customArgTypes; + else + argTypes = GetTypeForListenerMode(listenerMode); + + List foundMethods = new List(); + + // For some reason BindingFlags.FlattenHierarchy does not seem to work, so we manually traverse the base types instead + while (objectType != null) + { + MethodInfo[] foundMethodsOnType = objectType.GetMethods(BindingFlags.Public | (cachedSettings.showPrivateMembers ? BindingFlags.NonPublic : BindingFlags.Default) | BindingFlags.Instance); + + foundMethods.AddRange(foundMethodsOnType); + + objectType = objectType.BaseType; + } + + foreach (MethodInfo methodInfo in foundMethods) + { + // Sadly we can only use functions with void return type since C# throws an error + if (methodInfo.ReturnType != typeof(void)) + continue; + + ParameterInfo[] methodParams = methodInfo.GetParameters(); + if (methodParams.Length != argTypes.Length) + continue; + + bool isValidParamMatch = true; + for (int i = 0; i < methodParams.Length; i++) + { + if (!methodParams[i].ParameterType.IsAssignableFrom(argTypes[i])/* && (argTypes[i] != typeof(int) || !methodParams[i].ParameterType.IsEnum)*/) + { + isValidParamMatch = false; + } + if (listenerMode == PersistentListenerMode.Object && argTypes[i].IsAssignableFrom(methodParams[i].ParameterType)) + { + isValidParamMatch = true; + } + } + + if (!isValidParamMatch) + continue; + + if (!cachedSettings.showPrivateMembers && methodInfo.GetCustomAttributes(typeof(System.ObsoleteAttribute), true).Length > 0) + continue; + + + FunctionData foundMethodData = new FunctionData(null, targetObject, methodInfo, listenerMode); + + methodInfos.Add(foundMethodData); + } + } + + string GetTypeName(System.Type typeToName) + { + if (typeToName == typeof(float)) + return "float"; + if (typeToName == typeof(bool)) + return "bool"; + if (typeToName == typeof(int)) + return "int"; + if (typeToName == typeof(string)) + return "string"; + + return typeToName.Name; + } + + void AddFunctionToMenu(string contentPath, SerializedProperty elementProperty, FunctionData methodData, GenericMenu menu, int componentCount, bool dynamicCall = false) + { + string functionName = (methodData.targetMethod.Name.StartsWith("set_") ? methodData.targetMethod.Name.Substring(4) : methodData.targetMethod.Name); + string argStr = string.Join(", ", methodData.targetMethod.GetParameters().Select(param => GetTypeName(param.ParameterType)).ToArray()); + + if (dynamicCall) // Cut out the args from the dynamic variation to match Unity, and the menu item won't be created if it's not unique. + { + contentPath += functionName; + } + else + { + if (methodData.targetMethod.Name.StartsWith("set_")) // If it's a property add the arg before the name + { + contentPath += argStr + " " + functionName; + } + else + { + contentPath += functionName + " (" + argStr + ")"; // Add arguments + } + } + + if (!methodData.targetMethod.IsPublic) + contentPath += " " + (methodData.targetMethod.IsPrivate ? "" : ""); + + if (methodData.targetMethod.GetCustomAttributes(typeof(System.ObsoleteAttribute), true).Length > 0) + contentPath += " "; + + methodData.listenerElement = elementProperty; + + SerializedProperty serializedTargetObject = elementProperty.FindPropertyRelative("m_Target"); + SerializedProperty serializedMethodName = elementProperty.FindPropertyRelative("m_MethodName"); + SerializedProperty serializedMode = elementProperty.FindPropertyRelative("m_Mode"); + + bool itemOn = serializedTargetObject.objectReferenceValue == methodData.targetObject && + serializedMethodName.stringValue == methodData.targetMethod.Name && + serializedMode.enumValueIndex == (int)methodData.listenerMode; + + menu.AddItem(new GUIContent(contentPath), itemOn, SetEventFunctionCallback, methodData); + } + + void BuildMenuForObject(Object targetObject, SerializedProperty elementProperty, GenericMenu menu, int componentCount = 0) + { + List methodInfos = new List(); + string contentPath = targetObject.GetType().Name + (componentCount > 0 ? string.Format("({0})", componentCount) : "") + "/"; + + FindValidMethods(targetObject, PersistentListenerMode.Void, methodInfos); + FindValidMethods(targetObject, PersistentListenerMode.Int, methodInfos); + FindValidMethods(targetObject, PersistentListenerMode.Float, methodInfos); + FindValidMethods(targetObject, PersistentListenerMode.String, methodInfos); + FindValidMethods(targetObject, PersistentListenerMode.Bool, methodInfos); + FindValidMethods(targetObject, PersistentListenerMode.Object, methodInfos); + + methodInfos = methodInfos.OrderBy(method1 => method1.targetMethod.Name.StartsWith("set_") ? 0 : 1).ThenBy((method1) => method1.targetMethod.Name).ToList(); + + // Get event args to determine if we can do a pass through of the arg to the parameter + System.Type[] eventArgs = dummyEvent.GetType().GetMethod("Invoke").GetParameters().Select(p => p.ParameterType).ToArray(); + + bool dynamicBinding = false; + + if (eventArgs.Length > 0) + { + List dynamicMethodInfos = new List(); + FindValidMethods(targetObject, PersistentListenerMode.EventDefined, dynamicMethodInfos, eventArgs); + + if (dynamicMethodInfos.Count > 0) + { + dynamicMethodInfos = dynamicMethodInfos.OrderBy(m => m.targetMethod.Name.StartsWith("set") ? 0 : 1).ThenBy(m => m.targetMethod.Name).ToList(); + + dynamicBinding = true; + + // Add dynamic header + menu.AddDisabledItem(new GUIContent(contentPath + string.Format("Dynamic {0}", GetTypeName(eventArgs[0])))); + menu.AddSeparator(contentPath); + + foreach (FunctionData dynamicMethod in dynamicMethodInfos) + { + AddFunctionToMenu(contentPath, elementProperty, dynamicMethod, menu, 0, true); + } + } + } + + // Add static header if we have dynamic bindings + if (dynamicBinding) + { + menu.AddDisabledItem(new GUIContent(contentPath + "Static Parameters")); + menu.AddSeparator(contentPath); + } + + foreach (FunctionData method in methodInfos) + { + AddFunctionToMenu(contentPath, elementProperty, method, menu, componentCount); + } + } + + class ComponentTypeCount + { + public int TotalCount = 0; + public int CurrentCount = 1; + } + + GenericMenu BuildPopupMenu(Object targetObj, SerializedProperty elementProperty, System.Type objectArgType) + { + GenericMenu menu = new GenericMenu(); + + string currentMethodName = elementProperty.FindPropertyRelative("m_MethodName").stringValue; + + menu.AddItem(new GUIContent("No Function"), string.IsNullOrEmpty(currentMethodName), ClearEventFunctionCallback, new FunctionData(elementProperty)); + menu.AddSeparator(""); + + if (targetObj is Component) + { + targetObj = (targetObj as Component).gameObject; + } + else if (!(targetObj is GameObject)) + { + // Function menu for asset objects and such + BuildMenuForObject(targetObj, elementProperty, menu); + return menu; + } + + // GameObject menu + BuildMenuForObject(targetObj, elementProperty, menu); + + Component[] components = (targetObj as GameObject).GetComponents(); + Dictionary componentTypeCounts = new Dictionary(); + + // Only get the first instance of each component type + if (cachedSettings.groupSameComponentType) + { + components = components.GroupBy(comp => comp.GetType()).Select(group => group.First()).ToArray(); + } + else // Otherwise we need to know if there are multiple components of a given type before we start going through the components since we only need numbers on component types with multiple instances. + { + foreach (Component component in components) + { + ComponentTypeCount typeCount; + if (!componentTypeCounts.TryGetValue(component.GetType(), out typeCount)) + { + typeCount = new ComponentTypeCount(); + componentTypeCounts.Add(component.GetType(), typeCount); + } + + typeCount.TotalCount++; + } + + } + + foreach (Component component in components) + { + int componentCount = 0; + + if (!cachedSettings.groupSameComponentType) + { + ComponentTypeCount typeCount = componentTypeCounts[component.GetType()]; + if (typeCount.TotalCount > 1) + componentCount = typeCount.CurrentCount++; + } + + BuildMenuForObject(component, elementProperty, menu, componentCount); + } + + return menu; + } + + // Where the event data actually gets added when you choose a function + static void SetEventFunctionCallback(object functionUserData) + { + FunctionData functionData = functionUserData as FunctionData; + + SerializedProperty serializedElement = functionData.listenerElement; + + SerializedProperty serializedTarget = serializedElement.FindPropertyRelative("m_Target"); + SerializedProperty serializedMethodName = serializedElement.FindPropertyRelative("m_MethodName"); + SerializedProperty serializedArgs = serializedElement.FindPropertyRelative("m_Arguments"); + SerializedProperty serializedMode = serializedElement.FindPropertyRelative("m_Mode"); + + SerializedProperty serializedArgAssembly = serializedArgs.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName"); + SerializedProperty serializedArgObjectValue = serializedArgs.FindPropertyRelative("m_ObjectArgument"); + + serializedTarget.objectReferenceValue = functionData.targetObject; + serializedMethodName.stringValue = functionData.targetMethod.Name; + serializedMode.enumValueIndex = (int)functionData.listenerMode; + + if (functionData.listenerMode == PersistentListenerMode.Object) + { + ParameterInfo[] methodParams = functionData.targetMethod.GetParameters(); + if (methodParams.Length == 1 && typeof(Object).IsAssignableFrom(methodParams[0].ParameterType)) + serializedArgAssembly.stringValue = methodParams[0].ParameterType.AssemblyQualifiedName; + else + serializedArgAssembly.stringValue = typeof(Object).AssemblyQualifiedName; + } + else + { + serializedArgAssembly.stringValue = typeof(Object).AssemblyQualifiedName; + serializedArgObjectValue.objectReferenceValue = null; + } + + System.Type argType = EasyEventEditorHandler.FindTypeInAllAssemblies(serializedArgAssembly.stringValue); + if (!typeof(Object).IsAssignableFrom(argType) || !argType.IsInstanceOfType(serializedArgObjectValue.objectReferenceValue)) + serializedArgObjectValue.objectReferenceValue = null; + + functionData.listenerElement.serializedObject.ApplyModifiedProperties(); + } + + static void ClearEventFunctionCallback(object functionUserData) + { + FunctionData functionData = functionUserData as FunctionData; + + functionData.listenerElement.FindPropertyRelative("m_Mode").enumValueIndex = (int)PersistentListenerMode.Void; + functionData.listenerElement.FindPropertyRelative("m_MethodName").stringValue = null; + functionData.listenerElement.serializedObject.ApplyModifiedProperties(); + } + + void DrawElementCallback(Rect rect, int index, bool active, bool focused) + { + SerializedProperty element = listenerArray.GetArrayElementAtIndex(index); + + rect.y++; + Rect[] rects = GetElementRects(rect); + + Rect enabledRect = rects[0]; + Rect gameObjectRect = rects[1]; + Rect functionRect = rects[2]; + Rect argRect = rects[3]; + + SerializedProperty serializedCallState = element.FindPropertyRelative("m_CallState"); + SerializedProperty serializedMode = element.FindPropertyRelative("m_Mode"); + SerializedProperty serializedArgs = element.FindPropertyRelative("m_Arguments"); + SerializedProperty serializedTarget = element.FindPropertyRelative("m_Target"); + SerializedProperty serializedMethod = element.FindPropertyRelative("m_MethodName"); + + Color oldColor = GUI.backgroundColor; + GUI.backgroundColor = Color.white; + + EditorGUI.PropertyField(enabledRect, serializedCallState, GUIContent.none); + + EditorGUI.BeginChangeCheck(); + + Object oldTargetObject = serializedTarget.objectReferenceValue; + + GUI.Box(gameObjectRect, GUIContent.none); + EditorGUI.PropertyField(gameObjectRect, serializedTarget, GUIContent.none); + if (EditorGUI.EndChangeCheck()) + { + Object newTargetObject = serializedTarget.objectReferenceValue; + + // Attempt to maintain the function pointer and component pointer if someone changes the target object and it has the correct component type on it. + if (oldTargetObject != null && newTargetObject != null) + { + if (oldTargetObject.GetType() != newTargetObject.GetType()) // If not an asset, if it is an asset and the same type we don't do anything + { + // If these are Unity components then the game object that they are attached to may have multiple copies of the same component type so attempt to match the count + if (typeof(Component).IsAssignableFrom(oldTargetObject.GetType()) && newTargetObject.GetType() == typeof(GameObject)) + { + GameObject oldParentObject = ((Component)oldTargetObject).gameObject; + GameObject newParentObject = (GameObject)newTargetObject; + + Component[] oldComponentList = oldParentObject.GetComponents(oldTargetObject.GetType()); + + int componentLocationOffset = 0; + for (int i = 0; i < oldComponentList.Length; ++i) + { + if (oldComponentList[i] == oldTargetObject) + break; + + if (oldComponentList[i].GetType() == oldTargetObject.GetType()) // Only take exact matches for component type since I don't want to do redo the reflection to find the methods at the moment. + componentLocationOffset++; + } + + Component[] newComponentList = newParentObject.GetComponents(oldTargetObject.GetType()); + + int newComponentIndex = 0; + int componentCount = -1; + for (int i = 0; i < newComponentList.Length; ++i) + { + if (componentCount == componentLocationOffset) + break; + + if (newComponentList[i].GetType() == oldTargetObject.GetType()) + { + newComponentIndex = i; + componentCount++; + } + } + + if (newComponentList.Length > 0 && newComponentList[newComponentIndex].GetType() == oldTargetObject.GetType()) + { + serializedTarget.objectReferenceValue = newComponentList[newComponentIndex]; + } + else + { + serializedMethod.stringValue = null; + } + } + else + { + serializedMethod.stringValue = null; + } + } + } + else + { + serializedMethod.stringValue = null; + } + } + + PersistentListenerMode mode = (PersistentListenerMode)serializedMode.enumValueIndex; + + SerializedProperty argument; + if (serializedTarget.objectReferenceValue == null || string.IsNullOrEmpty(serializedMethod.stringValue)) + mode = PersistentListenerMode.Void; + + switch (mode) + { + case PersistentListenerMode.Object: + case PersistentListenerMode.String: + case PersistentListenerMode.Bool: + case PersistentListenerMode.Float: + argument = serializedArgs.FindPropertyRelative("m_" + System.Enum.GetName(typeof(PersistentListenerMode), mode) + "Argument"); + break; + default: + argument = serializedArgs.FindPropertyRelative("m_IntArgument"); + break; + } + + string argTypeName = serializedArgs.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName").stringValue; + System.Type argType = typeof(Object); + if (!string.IsNullOrEmpty(argTypeName)) + argType = EasyEventEditorHandler.FindTypeInAllAssemblies(argTypeName) ?? typeof (Object); + + if (mode == PersistentListenerMode.Object) + { + EditorGUI.BeginChangeCheck(); + Object result = EditorGUI.ObjectField(argRect, GUIContent.none, argument.objectReferenceValue, argType, true); + if (EditorGUI.EndChangeCheck()) + argument.objectReferenceValue = result; + } + else if (mode != PersistentListenerMode.Void && mode != PersistentListenerMode.EventDefined) + EditorGUI.PropertyField(argRect, argument, GUIContent.none); + + EditorGUI.BeginDisabledGroup(serializedTarget.objectReferenceValue == null); + { + EditorGUI.BeginProperty(functionRect, GUIContent.none, serializedMethod); + + GUIContent buttonContent; + + if (EditorGUI.showMixedValue) + { + buttonContent = new GUIContent("\u2014", "Mixed Values"); + } + else + { + if (serializedTarget.objectReferenceValue == null || string.IsNullOrEmpty(serializedMethod.stringValue)) + { + buttonContent = new GUIContent("No Function"); + } + else + { + buttonContent = new GUIContent(GetFunctionDisplayName(serializedTarget, serializedMethod, mode, argType, cachedSettings.displayArgumentType)); + } + } + + if (GUI.Button(functionRect, buttonContent, EditorStyles.popup)) + { + BuildPopupMenu(serializedTarget.objectReferenceValue, element, argType).DropDown(functionRect); + } + + EditorGUI.EndProperty(); + } + EditorGUI.EndDisabledGroup(); + } + + void SelectCallback(ReorderableList list) + { + currentState.lastSelectedIndex = list.index; + } + + void ReorderCallback(ReorderableList list) + { + currentState.lastSelectedIndex = list.index; + } + + void AddEventListener(ReorderableList list) + { + if (listenerArray.hasMultipleDifferentValues) + { + foreach (Object targetObj in listenerArray.serializedObject.targetObjects) + { + SerializedObject tempSerializedObject = new SerializedObject(targetObj); + SerializedProperty listenerArrayProperty = tempSerializedObject.FindProperty(listenerArray.propertyPath); + listenerArrayProperty.arraySize += 1; + tempSerializedObject.ApplyModifiedProperties(); + } + + listenerArray.serializedObject.SetIsDifferentCacheDirty(); + listenerArray.serializedObject.Update(); + list.index = list.serializedProperty.arraySize - 1; + } + else + { + ReorderableList.defaultBehaviours.DoAddButton(list); + } + + currentState.lastSelectedIndex = list.index; + + // Init default state + SerializedProperty serialiedListener = listenerArray.GetArrayElementAtIndex(list.index); + ResetEventState(serialiedListener); + } + + void ResetEventState(SerializedProperty serialiedListener) + { + SerializedProperty serializedCallState = serialiedListener.FindPropertyRelative("m_CallState"); + SerializedProperty serializedTarget = serialiedListener.FindPropertyRelative("m_Target"); + SerializedProperty serializedMethodName = serialiedListener.FindPropertyRelative("m_MethodName"); + SerializedProperty serializedMode = serialiedListener.FindPropertyRelative("m_Mode"); + SerializedProperty serializedArgs = serialiedListener.FindPropertyRelative("m_Arguments"); + + serializedCallState.enumValueIndex = (int)UnityEventCallState.RuntimeOnly; + serializedTarget.objectReferenceValue = null; + serializedMethodName.stringValue = null; + serializedMode.enumValueIndex = (int)PersistentListenerMode.Void; + + serializedArgs.FindPropertyRelative("m_IntArgument").intValue = 0; + serializedArgs.FindPropertyRelative("m_FloatArgument").floatValue = 0f; + serializedArgs.FindPropertyRelative("m_BoolArgument").boolValue = false; + serializedArgs.FindPropertyRelative("m_StringArgument").stringValue = null; + serializedArgs.FindPropertyRelative("m_ObjectArgument").objectReferenceValue = null; + serializedArgs.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName").stringValue = null; + } + + void RemoveCallback(ReorderableList list) + { + if (currentState.reorderableList.count > 0) + { + ReorderableList.defaultBehaviours.DoRemoveButton(list); + currentState.lastSelectedIndex = list.index; + } + } +} + +} // namespace Merlin + +#endif diff --git a/package.json b/package.json index 1285844..6ec7856 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.merlin.easyeventeditor", "displayName": "Easy Event Editor", - "version": "1.0.3", + "version": "1.0.4", "unity": "2017.4", "description": "Drop in replacement for the default Unity event editor drawer that allows listener reordering and a few other things", "keywords": ["Event", "Editor", "Delegate"],