├── EasyEventEditor.asmdef ├── EasyEventEditor.asmdef.meta ├── EasyEventEditor.cs ├── EasyEventEditor.cs.meta ├── LICENSE ├── LICENSE.meta ├── README.md ├── README.md.meta ├── package.json └── package.json.meta /EasyEventEditor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "EasyEventEditor", 3 | "references": [], 4 | "optionalUnityReferences": [], 5 | "includePlatforms": [ 6 | "Editor" 7 | ], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": false, 10 | "overrideReferences": false, 11 | "precompiledReferences": [], 12 | "autoReferenced": true, 13 | "defineConstraints": [] 14 | } -------------------------------------------------------------------------------- /EasyEventEditor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 522921964670914419e1da5ef6500807 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /EasyEventEditor.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * 4 | * Copyright (c) 2019 Merlin 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | /** 26 | * Script to make working with objects that have Unity persistent events easier. 27 | * 28 | * Allows five things that the default Unity event editor does not: 29 | * 30 | * 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 31 | * 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. 32 | * 3. Gives access to multiple components of the same type on the same object 33 | * 4. Gives an Invoke button to execute the event in editor for debugging and testing 34 | * 5. Adds hotkeys to event operations 35 | */ 36 | 37 | #if UNITY_EDITOR 38 | 39 | using UnityEditor; 40 | using UnityEditorInternal; 41 | using UnityEngine; 42 | using UnityEngine.Events; 43 | using System.Reflection; 44 | using System.Collections; 45 | using System.Collections.Generic; 46 | using System.Text; 47 | using System.Linq; 48 | 49 | namespace Merlin 50 | { 51 | 52 | [InitializeOnLoad] 53 | public class EasyEventEditorHandler 54 | { 55 | private const string eeeOverrideEventDrawerKey = "EEE.overrideEventDrawer"; 56 | private const string eeeShowPrivateMembersKey = "EEE.showPrivateMembers"; 57 | private const string eeeShowInvokeFieldKey = "EEE.showInvokeField"; 58 | private const string eeeDisplayArgumentTypeKey = "EEE.displayArgumentType"; 59 | private const string eeeGroupSameComponentTypeKey = "EEE.groupSameComponentType"; 60 | private const string eeeUseHotkeys = "EEE.usehotkeys"; 61 | 62 | private static bool patchApplied = false; 63 | private static FieldInfo internalDrawerTypeMap = null; 64 | private static System.Type attributeUtilityType = null; 65 | 66 | public class EEESettings 67 | { 68 | public bool overrideEventDrawer; 69 | public bool showPrivateMembers; 70 | public bool showInvokeField; 71 | public bool displayArgumentType; 72 | public bool groupSameComponentType; 73 | public bool useHotkeys; 74 | } 75 | 76 | // https://stackoverflow.com/questions/12898282/type-gettype-not-working 77 | public static System.Type FindTypeInAllAssemblies(string qualifiedTypeName) 78 | { 79 | System.Type t = System.Type.GetType(qualifiedTypeName); 80 | 81 | if (t != null) 82 | { 83 | return t; 84 | } 85 | else 86 | { 87 | foreach (System.Reflection.Assembly asm in System.AppDomain.CurrentDomain.GetAssemblies()) 88 | { 89 | t = asm.GetType(qualifiedTypeName); 90 | if (t != null) 91 | return t; 92 | } 93 | 94 | return null; 95 | } 96 | } 97 | 98 | static EasyEventEditorHandler() 99 | { 100 | EditorApplication.update += OnEditorUpdate; 101 | } 102 | 103 | static void OnEditorUpdate() 104 | { 105 | ApplyEventPropertyDrawerPatch(); 106 | } 107 | 108 | [UnityEditor.Callbacks.DidReloadScripts] 109 | private static void OnScriptsReloaded() 110 | { 111 | ApplyEventPropertyDrawerPatch(); 112 | } 113 | 114 | internal static FieldInfo GetDrawerTypeMap() 115 | { 116 | // We already have the map so skip all the reflection 117 | if (internalDrawerTypeMap != null) 118 | { 119 | return internalDrawerTypeMap; 120 | } 121 | 122 | System.Type scriptAttributeUtilityType = FindTypeInAllAssemblies("UnityEditor.ScriptAttributeUtility"); 123 | 124 | if (scriptAttributeUtilityType == null) 125 | { 126 | Debug.LogError("Could not find ScriptAttributeUtility in assemblies!"); 127 | return null; 128 | } 129 | 130 | // Save for later in case we need to lookup the function to populate the attributes 131 | attributeUtilityType = scriptAttributeUtilityType; 132 | 133 | FieldInfo info = scriptAttributeUtilityType.GetField("s_DrawerTypeForType", BindingFlags.NonPublic | BindingFlags.Static); 134 | 135 | if (info == null) 136 | { 137 | Debug.LogError("Could not find drawer type map!"); 138 | return null; 139 | } 140 | 141 | internalDrawerTypeMap = info; 142 | 143 | return internalDrawerTypeMap; 144 | } 145 | 146 | private static void ClearPropertyCaches() 147 | { 148 | if (attributeUtilityType == null) 149 | { 150 | Debug.LogError("UnityEditor.ScriptAttributeUtility type is null! Make sure you have called GetDrawerTypeMap() to ensure this is cached!"); 151 | return; 152 | } 153 | 154 | // Nuke handle caches so they can find our modified drawer 155 | MethodInfo clearCacheFunc = attributeUtilityType.GetMethod("ClearGlobalCache", BindingFlags.NonPublic | BindingFlags.Static); 156 | 157 | if (clearCacheFunc == null) 158 | { 159 | Debug.LogError("Could not find cache clear method!"); 160 | return; 161 | } 162 | 163 | clearCacheFunc.Invoke(null, new object[] { }); 164 | 165 | FieldInfo currentCacheField = attributeUtilityType.GetField("s_CurrentCache", BindingFlags.NonPublic | BindingFlags.Static); 166 | 167 | if (currentCacheField == null) 168 | { 169 | Debug.LogError("Could not find CurrentCache field!"); 170 | return; 171 | } 172 | 173 | object currentCacheValue = currentCacheField.GetValue(null); 174 | 175 | if (currentCacheValue != null) 176 | { 177 | MethodInfo clearMethod = currentCacheValue.GetType().GetMethod("Clear", BindingFlags.Public | BindingFlags.Instance); 178 | 179 | if (clearMethod == null) 180 | { 181 | Debug.LogError("Could not find clear function for current cache!"); 182 | return; 183 | } 184 | 185 | clearMethod.Invoke(currentCacheValue, new object[] { }); 186 | } 187 | 188 | System.Type inspectorWindowType = FindTypeInAllAssemblies("UnityEditor.InspectorWindow"); 189 | 190 | if (inspectorWindowType == null) 191 | { 192 | Debug.LogError("Could not find inspector window type!"); 193 | return; 194 | } 195 | 196 | FieldInfo trackerField = inspectorWindowType.GetField("m_Tracker", BindingFlags.NonPublic | BindingFlags.Instance); 197 | FieldInfo propertyHandleCacheField = typeof(Editor).GetField("m_PropertyHandlerCache", BindingFlags.NonPublic | BindingFlags.Instance); 198 | 199 | if (trackerField == null || propertyHandleCacheField == null) 200 | { 201 | Debug.LogError("Could not find tracker field!"); 202 | return; 203 | } 204 | 205 | //FieldInfo trackerEditorsField = trackerField.GetType().GetField("") 206 | 207 | System.Type propertyHandlerCacheType = FindTypeInAllAssemblies("UnityEditor.PropertyHandlerCache"); 208 | 209 | if (propertyHandlerCacheType == null) 210 | { 211 | Debug.LogError("Could not find type of PropertyHandlerCache"); 212 | return; 213 | } 214 | 215 | // 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. 216 | EditorWindow[] editorWindows = Resources.FindObjectsOfTypeAll(); 217 | 218 | foreach (EditorWindow editor in editorWindows) 219 | { 220 | if (editor.GetType() == inspectorWindowType || editor.GetType().IsSubclassOf(inspectorWindowType)) 221 | { 222 | ActiveEditorTracker activeEditorTracker = trackerField.GetValue(editor) as ActiveEditorTracker; 223 | 224 | if (activeEditorTracker != null) 225 | { 226 | foreach (Editor activeEditor in activeEditorTracker.activeEditors) 227 | { 228 | if (activeEditor != null) 229 | { 230 | propertyHandleCacheField.SetValue(activeEditor, System.Activator.CreateInstance(propertyHandlerCacheType)); 231 | activeEditor.Repaint(); // Force repaint to get updated drawing of property 232 | } 233 | } 234 | } 235 | } 236 | } 237 | } 238 | 239 | // Applies patch to Unity's builtin tracking for Drawers to redirect any drawers for Unity Events to our EasyEventDrawer instead. 240 | private static void ApplyEventDrawerPatch(bool enableOverride) 241 | { 242 | // Call here to find the scriptAttributeUtilityType in case it's needed for when overrides are disabled 243 | FieldInfo drawerTypeMap = GetDrawerTypeMap(); 244 | 245 | if (enableOverride) 246 | { 247 | System.Type[] mapArgs = drawerTypeMap.FieldType.GetGenericArguments(); 248 | 249 | System.Type keyType = mapArgs[0]; 250 | System.Type valType = mapArgs[1]; 251 | 252 | if (keyType == null || valType == null) 253 | { 254 | Debug.LogError("Could not retrieve dictionary types!"); 255 | return; 256 | } 257 | 258 | FieldInfo drawerField = valType.GetField("drawer", BindingFlags.Public | BindingFlags.Instance); 259 | FieldInfo typeField = valType.GetField("type", BindingFlags.Public | BindingFlags.Instance); 260 | 261 | if (drawerField == null || typeField == null) 262 | { 263 | Debug.LogError("Could not retrieve dictionary value fields!"); 264 | return; 265 | } 266 | 267 | IDictionary drawerTypeMapDict = drawerTypeMap.GetValue(null) as IDictionary; 268 | 269 | if (drawerTypeMapDict == null) 270 | { 271 | MethodInfo popAttributesFunc = attributeUtilityType.GetMethod("BuildDrawerTypeForTypeDictionary", BindingFlags.NonPublic | BindingFlags.Static); 272 | 273 | if (popAttributesFunc == null) 274 | { 275 | Debug.LogError("Could not populate attributes for override!"); 276 | return; 277 | } 278 | 279 | popAttributesFunc.Invoke(null, new object[] { }); 280 | 281 | // Try again now that this should be populated 282 | drawerTypeMapDict = drawerTypeMap.GetValue(null) as IDictionary; 283 | if (drawerTypeMapDict == null) 284 | { 285 | Debug.LogError("Could not get dictionary for drawer types!"); 286 | return; 287 | } 288 | } 289 | 290 | // Replace EventDrawer handles with our custom drawer 291 | List keysToRecreate = new List(); 292 | 293 | foreach (DictionaryEntry entry in drawerTypeMapDict) 294 | { 295 | System.Type drawerType = (System.Type)drawerField.GetValue(entry.Value); 296 | 297 | if (drawerType.Name == "UnityEventDrawer") 298 | { 299 | keysToRecreate.Add(entry.Key); 300 | } 301 | } 302 | 303 | foreach (object keyToKill in keysToRecreate) 304 | { 305 | drawerTypeMapDict.Remove(keyToKill); 306 | } 307 | 308 | // Recreate these key-value pairs since they are structs 309 | foreach (object keyToRecreate in keysToRecreate) 310 | { 311 | object newValMapping = System.Activator.CreateInstance(valType); 312 | typeField.SetValue(newValMapping, (System.Type)keyToRecreate); 313 | drawerField.SetValue(newValMapping, typeof(EasyEventEditorDrawer)); 314 | 315 | drawerTypeMapDict.Add(keyToRecreate, newValMapping); 316 | } 317 | } 318 | else 319 | { 320 | MethodInfo popAttributesFunc = attributeUtilityType.GetMethod("BuildDrawerTypeForTypeDictionary", BindingFlags.NonPublic | BindingFlags.Static); 321 | 322 | if (popAttributesFunc == null) 323 | { 324 | Debug.LogError("Could not populate attributes for override!"); 325 | return; 326 | } 327 | 328 | // Just force the editor to repopulate the drawers without nuking afterwards. 329 | popAttributesFunc.Invoke(null, new object[] { }); 330 | } 331 | 332 | // Clear caches to force event drawers to refresh immediately. 333 | ClearPropertyCaches(); 334 | } 335 | 336 | public static void ApplyEventPropertyDrawerPatch(bool forceApply = false) 337 | { 338 | EEESettings settings = GetEditorSettings(); 339 | 340 | if (!patchApplied || forceApply) 341 | { 342 | ApplyEventDrawerPatch(settings.overrideEventDrawer); 343 | patchApplied = true; 344 | } 345 | } 346 | 347 | public static EEESettings GetEditorSettings() 348 | { 349 | EEESettings settings = new EEESettings 350 | { 351 | overrideEventDrawer = EditorPrefs.GetBool(eeeOverrideEventDrawerKey, true), 352 | showPrivateMembers = EditorPrefs.GetBool(eeeShowPrivateMembersKey, true), 353 | showInvokeField = EditorPrefs.GetBool(eeeShowInvokeFieldKey, true), 354 | displayArgumentType = EditorPrefs.GetBool(eeeDisplayArgumentTypeKey, true), 355 | groupSameComponentType = EditorPrefs.GetBool(eeeGroupSameComponentTypeKey, false), 356 | useHotkeys = EditorPrefs.GetBool(eeeUseHotkeys, true), 357 | }; 358 | 359 | return settings; 360 | } 361 | 362 | public static void SetEditorSettings(EEESettings settings) 363 | { 364 | EditorPrefs.SetBool(eeeOverrideEventDrawerKey, settings.overrideEventDrawer); 365 | EditorPrefs.SetBool(eeeShowPrivateMembersKey, settings.showPrivateMembers); 366 | EditorPrefs.SetBool(eeeShowInvokeFieldKey, settings.showInvokeField); 367 | EditorPrefs.SetBool(eeeDisplayArgumentTypeKey, settings.displayArgumentType); 368 | EditorPrefs.SetBool(eeeGroupSameComponentTypeKey, settings.groupSameComponentType); 369 | EditorPrefs.SetBool(eeeUseHotkeys, settings.useHotkeys); 370 | } 371 | } 372 | 373 | internal class SettingsGUIContent 374 | { 375 | private static GUIContent enableToggleGuiContent = new GUIContent("Enable Easy Event Editor", "Replaces the default Unity event editing context with EEE"); 376 | private static GUIContent enablePrivateMembersGuiContent = new GUIContent("Show private properties and methods", "Exposes private/internal/obsolete properties and methods to the function list on events"); 377 | 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"); 378 | private static GUIContent displayArgumentTypeContent = new GUIContent("Display argument type on function name", "Shows the argument that a function takes on the function header"); 379 | 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."); 380 | 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"); 381 | 382 | public static void DrawSettingsButtons(EasyEventEditorHandler.EEESettings settings) 383 | { 384 | EditorGUI.indentLevel += 1; 385 | 386 | settings.overrideEventDrawer = EditorGUILayout.ToggleLeft(enableToggleGuiContent, settings.overrideEventDrawer); 387 | 388 | EditorGUI.BeginDisabledGroup(!settings.overrideEventDrawer); 389 | 390 | settings.showPrivateMembers = EditorGUILayout.ToggleLeft(enablePrivateMembersGuiContent, settings.showPrivateMembers); 391 | settings.showInvokeField = EditorGUILayout.ToggleLeft(showInvokeFieldGuiContent, settings.showInvokeField); 392 | settings.displayArgumentType = EditorGUILayout.ToggleLeft(displayArgumentTypeContent, settings.displayArgumentType); 393 | settings.groupSameComponentType = !EditorGUILayout.ToggleLeft(groupSameComponentTypeContent, !settings.groupSameComponentType); 394 | settings.useHotkeys = EditorGUILayout.ToggleLeft(useHotkeys, settings.useHotkeys); 395 | 396 | EditorGUI.EndDisabledGroup(); 397 | EditorGUI.indentLevel -= 1; 398 | } 399 | } 400 | 401 | #if UNITY_2018_3_OR_NEWER 402 | // Use the new settings provider class instead so we don't need to add extra stuff to the Edit menu 403 | // Using the IMGUI method 404 | static class EasyEventEditorSettingsProvider 405 | { 406 | [SettingsProvider] 407 | public static SettingsProvider CreateSettingsProvider() 408 | { 409 | var provider = new SettingsProvider("Preferences/Easy Event Editor", SettingsScope.User) 410 | { 411 | label = "Easy Event Editor", 412 | 413 | guiHandler = (searchContext) => 414 | { 415 | EasyEventEditorHandler.EEESettings settings = EasyEventEditorHandler.GetEditorSettings(); 416 | 417 | EditorGUI.BeginChangeCheck(); 418 | SettingsGUIContent.DrawSettingsButtons(settings); 419 | 420 | if (EditorGUI.EndChangeCheck()) 421 | { 422 | EasyEventEditorHandler.SetEditorSettings(settings); 423 | EasyEventEditorHandler.ApplyEventPropertyDrawerPatch(true); 424 | } 425 | 426 | }, 427 | 428 | keywords = new HashSet(new[] { "Easy", "Event", "Editor", "Delegate", "VRChat", "EEE" }) 429 | }; 430 | 431 | return provider; 432 | } 433 | } 434 | #else 435 | public class EasyEventEditorSettings : EditorWindow 436 | { 437 | [MenuItem("Edit/Easy Event Editor Settings")] 438 | static void Init() 439 | { 440 | EasyEventEditorSettings window = GetWindow(false, "EEE Settings"); 441 | window.minSize = new Vector2(350, 150); 442 | window.maxSize = new Vector2(350, 150); 443 | window.Show(); 444 | } 445 | 446 | private void OnGUI() 447 | { 448 | EditorGUILayout.Space(); 449 | EditorGUILayout.LabelField("Easy Event Editor Settings", EditorStyles.boldLabel); 450 | 451 | EditorGUILayout.Space(); 452 | 453 | EasyEventEditorHandler.EEESettings settings = EasyEventEditorHandler.GetEditorSettings(); 454 | 455 | EditorGUI.BeginChangeCheck(); 456 | SettingsGUIContent.DrawSettingsButtons(settings); 457 | 458 | if (EditorGUI.EndChangeCheck()) 459 | { 460 | EasyEventEditorHandler.SetEditorSettings(settings); 461 | EasyEventEditorHandler.ApplyEventPropertyDrawerPatch(true); 462 | } 463 | } 464 | } 465 | #endif 466 | 467 | // Drawer that gets patched in over Unity's default event drawer 468 | public class EasyEventEditorDrawer : PropertyDrawer 469 | { 470 | class DrawerState 471 | { 472 | public ReorderableList reorderableList; 473 | public int lastSelectedIndex; 474 | 475 | // Invoke field tracking 476 | public string currentInvokeStrArg = ""; 477 | public int currentInvokeIntArg = 0; 478 | public float currentInvokeFloatArg = 0f; 479 | public bool currentInvokeBoolArg = false; 480 | public Object currentInvokeObjectArg = null; 481 | } 482 | 483 | class FunctionData 484 | { 485 | public FunctionData(SerializedProperty listener, Object target = null, MethodInfo method = null, PersistentListenerMode mode = PersistentListenerMode.EventDefined) 486 | { 487 | listenerElement = listener; 488 | targetObject = target; 489 | targetMethod = method; 490 | listenerMode = mode; 491 | } 492 | 493 | public SerializedProperty listenerElement; 494 | public Object targetObject; 495 | public MethodInfo targetMethod; 496 | public PersistentListenerMode listenerMode; 497 | } 498 | 499 | Dictionary drawerStates = new Dictionary(); 500 | 501 | DrawerState currentState; 502 | string currentLabelText; 503 | SerializedProperty currentProperty; 504 | SerializedProperty listenerArray; 505 | 506 | UnityEventBase dummyEvent; 507 | MethodInfo cachedFindMethodInfo = null; 508 | static EasyEventEditorHandler.EEESettings cachedSettings; 509 | 510 | #if UNITY_2018_4_OR_NEWER 511 | private static UnityEventBase GetDummyEventStep(string propertyPath, System.Type propertyType, BindingFlags bindingFlags) 512 | { 513 | UnityEventBase dummyEvent = null; 514 | 515 | while (propertyPath.Length > 0) 516 | { 517 | if (propertyPath.StartsWith(".")) 518 | propertyPath = propertyPath.Substring(1); 519 | 520 | string[] splitPath = propertyPath.Split(new char[] { '.' }, 2); 521 | 522 | FieldInfo newField = propertyType.GetField(splitPath[0], bindingFlags); 523 | 524 | if (newField == null) 525 | break; 526 | 527 | propertyType = newField.FieldType; 528 | if (propertyType.IsArray) 529 | { 530 | propertyType = propertyType.GetElementType(); 531 | } 532 | else if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(List<>)) 533 | { 534 | propertyType = propertyType.GetGenericArguments()[0]; 535 | } 536 | 537 | if (splitPath.Length == 1) 538 | break; 539 | 540 | propertyPath = splitPath[1]; 541 | if (propertyPath.StartsWith("Array.data[")) 542 | propertyPath = propertyPath.Split(new char[] { ']' }, 2)[1]; 543 | } 544 | 545 | if (propertyType.IsSubclassOf(typeof(UnityEventBase))) 546 | dummyEvent = System.Activator.CreateInstance(propertyType) as UnityEventBase; 547 | 548 | return dummyEvent; 549 | } 550 | 551 | private static UnityEventBase GetDummyEvent(SerializedProperty property) 552 | { 553 | Object targetObject = property.serializedObject.targetObject; 554 | if (targetObject == null) 555 | return new UnityEvent(); 556 | 557 | UnityEventBase dummyEvent = null; 558 | System.Type targetType = targetObject.GetType(); 559 | BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; 560 | 561 | do 562 | { 563 | dummyEvent = GetDummyEventStep(property.propertyPath, targetType, bindingFlags); 564 | bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic; 565 | targetType = targetType.BaseType; 566 | } while (dummyEvent == null && targetType != null); 567 | 568 | return dummyEvent ?? new UnityEvent(); 569 | } 570 | #endif 571 | 572 | private void PrepareState(SerializedProperty propertyForState) 573 | { 574 | DrawerState state; 575 | 576 | if (!drawerStates.TryGetValue(propertyForState.propertyPath, out state)) 577 | { 578 | state = new DrawerState(); 579 | 580 | SerializedProperty persistentListeners = propertyForState.FindPropertyRelative("m_PersistentCalls.m_Calls"); 581 | 582 | // The fun thing is that if Unity just made the first bool arg true internally, this whole thing would be unnecessary. 583 | state.reorderableList = new ReorderableList(propertyForState.serializedObject, persistentListeners, true, true, true, true); 584 | state.reorderableList.elementHeight = 43; // todo: actually find proper constant for this. 585 | state.reorderableList.drawHeaderCallback += DrawHeaderCallback; 586 | state.reorderableList.drawElementCallback += DrawElementCallback; 587 | state.reorderableList.onSelectCallback += SelectCallback; 588 | state.reorderableList.onRemoveCallback += ReorderCallback; 589 | state.reorderableList.onAddCallback += AddEventListener; 590 | state.reorderableList.onRemoveCallback += RemoveCallback; 591 | 592 | state.lastSelectedIndex = 0; 593 | 594 | drawerStates.Add(propertyForState.propertyPath, state); 595 | } 596 | 597 | currentProperty = propertyForState; 598 | 599 | currentState = state; 600 | currentState.reorderableList.index = currentState.lastSelectedIndex; 601 | listenerArray = state.reorderableList.serializedProperty; 602 | 603 | // Setup dummy event 604 | #if UNITY_2018_4_OR_NEWER 605 | dummyEvent = GetDummyEvent(propertyForState); 606 | #else 607 | string eventTypeName = currentProperty.FindPropertyRelative("m_TypeName").stringValue; 608 | System.Type eventType = EasyEventEditorHandler.FindTypeInAllAssemblies(eventTypeName); 609 | if (eventType == null) 610 | dummyEvent = new UnityEvent(); 611 | else 612 | dummyEvent = System.Activator.CreateInstance(eventType) as UnityEventBase; 613 | #endif 614 | 615 | cachedSettings = EasyEventEditorHandler.GetEditorSettings(); 616 | } 617 | 618 | private void HandleKeyboardShortcuts() 619 | { 620 | if (!cachedSettings.useHotkeys) 621 | return; 622 | 623 | Event currentEvent = Event.current; 624 | 625 | if (!currentState.reorderableList.HasKeyboardControl()) 626 | return; 627 | 628 | if (currentEvent.type == EventType.ValidateCommand) 629 | { 630 | if (currentEvent.commandName == "Copy" || 631 | currentEvent.commandName == "Paste" || 632 | currentEvent.commandName == "Cut" || 633 | currentEvent.commandName == "Duplicate" || 634 | currentEvent.commandName == "Delete" || 635 | currentEvent.commandName == "SoftDelete" || 636 | currentEvent.commandName == "SelectAll") 637 | { 638 | currentEvent.Use(); 639 | } 640 | } 641 | else if (currentEvent.type == EventType.ExecuteCommand) 642 | { 643 | if (currentEvent.commandName == "Copy") 644 | { 645 | HandleCopy(); 646 | currentEvent.Use(); 647 | } 648 | else if (currentEvent.commandName == "Paste") 649 | { 650 | HandlePaste(); 651 | currentEvent.Use(); 652 | } 653 | else if (currentEvent.commandName == "Cut") 654 | { 655 | HandleCut(); 656 | currentEvent.Use(); 657 | } 658 | else if (currentEvent.commandName == "Duplicate") 659 | { 660 | HandleDuplicate(); 661 | currentEvent.Use(); 662 | } 663 | else if (currentEvent.commandName == "Delete" || currentEvent.commandName == "SoftDelete") 664 | { 665 | RemoveCallback(currentState.reorderableList); 666 | currentEvent.Use(); 667 | } 668 | else if (currentEvent.commandName == "SelectAll") // Use Ctrl+A for add, since Ctrl+N isn't usable using command names 669 | { 670 | HandleAdd(); 671 | currentEvent.Use(); 672 | } 673 | } 674 | } 675 | 676 | private class EventClipboardStorage 677 | { 678 | public static SerializedObject CopiedEventProperty; 679 | public static int CopiedEventIndex; 680 | } 681 | 682 | private void HandleCopy() 683 | { 684 | SerializedObject serializedEvent = new SerializedObject(listenerArray.GetArrayElementAtIndex(currentState.reorderableList.index).serializedObject.targetObject); 685 | 686 | EventClipboardStorage.CopiedEventProperty = serializedEvent; 687 | EventClipboardStorage.CopiedEventIndex = currentState.reorderableList.index; 688 | } 689 | 690 | private void HandlePaste() 691 | { 692 | if (EventClipboardStorage.CopiedEventProperty == null) 693 | return; 694 | 695 | SerializedProperty iterator = EventClipboardStorage.CopiedEventProperty.GetIterator(); 696 | 697 | if (iterator == null) 698 | return; 699 | 700 | while (iterator.NextVisible(true)) 701 | { 702 | if (iterator != null && iterator.name == "m_PersistentCalls") 703 | { 704 | iterator = iterator.FindPropertyRelative("m_Calls"); 705 | break; 706 | } 707 | } 708 | 709 | if (iterator.arraySize < (EventClipboardStorage.CopiedEventIndex + 1)) 710 | return; 711 | 712 | SerializedProperty sourceProperty = iterator.GetArrayElementAtIndex(EventClipboardStorage.CopiedEventIndex); 713 | 714 | if (sourceProperty == null) 715 | return; 716 | 717 | int targetArrayIdx = currentState.reorderableList.count > 0 ? currentState.reorderableList.index : 0; 718 | currentState.reorderableList.serializedProperty.InsertArrayElementAtIndex(targetArrayIdx); 719 | 720 | SerializedProperty targetProperty = currentState.reorderableList.serializedProperty.GetArrayElementAtIndex((currentState.reorderableList.count > 0 ? currentState.reorderableList.index : 0) + 1); 721 | ResetEventState(targetProperty); 722 | 723 | targetProperty.FindPropertyRelative("m_CallState").enumValueIndex = sourceProperty.FindPropertyRelative("m_CallState").enumValueIndex; 724 | targetProperty.FindPropertyRelative("m_Target").objectReferenceValue = sourceProperty.FindPropertyRelative("m_Target").objectReferenceValue; 725 | targetProperty.FindPropertyRelative("m_MethodName").stringValue = sourceProperty.FindPropertyRelative("m_MethodName").stringValue; 726 | targetProperty.FindPropertyRelative("m_Mode").enumValueIndex = sourceProperty.FindPropertyRelative("m_Mode").enumValueIndex; 727 | 728 | SerializedProperty targetArgs = targetProperty.FindPropertyRelative("m_Arguments"); 729 | SerializedProperty sourceArgs = sourceProperty.FindPropertyRelative("m_Arguments"); 730 | 731 | targetArgs.FindPropertyRelative("m_IntArgument").intValue = sourceArgs.FindPropertyRelative("m_IntArgument").intValue; 732 | targetArgs.FindPropertyRelative("m_FloatArgument").floatValue = sourceArgs.FindPropertyRelative("m_FloatArgument").floatValue; 733 | targetArgs.FindPropertyRelative("m_BoolArgument").boolValue = sourceArgs.FindPropertyRelative("m_BoolArgument").boolValue; 734 | targetArgs.FindPropertyRelative("m_StringArgument").stringValue = sourceArgs.FindPropertyRelative("m_StringArgument").stringValue; 735 | targetArgs.FindPropertyRelative("m_ObjectArgument").objectReferenceValue = sourceArgs.FindPropertyRelative("m_ObjectArgument").objectReferenceValue; 736 | targetArgs.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName").stringValue = sourceArgs.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName").stringValue; 737 | 738 | currentState.reorderableList.index++; 739 | currentState.lastSelectedIndex++; 740 | 741 | targetProperty.serializedObject.ApplyModifiedProperties(); 742 | } 743 | 744 | private void HandleCut() 745 | { 746 | HandleCopy(); 747 | RemoveCallback(currentState.reorderableList); 748 | } 749 | 750 | private void HandleDuplicate() 751 | { 752 | if (currentState.reorderableList.count == 0) 753 | return; 754 | 755 | SerializedProperty listProperty = currentState.reorderableList.serializedProperty; 756 | 757 | SerializedProperty eventProperty = listProperty.GetArrayElementAtIndex(currentState.reorderableList.index); 758 | 759 | eventProperty.DuplicateCommand(); 760 | 761 | currentState.reorderableList.index++; 762 | currentState.lastSelectedIndex++; 763 | } 764 | 765 | private void HandleAdd() 766 | { 767 | int targetIdx = currentState.reorderableList.count > 0 ? currentState.reorderableList.index : 0; 768 | currentState.reorderableList.serializedProperty.InsertArrayElementAtIndex(targetIdx); 769 | 770 | SerializedProperty eventProperty = currentState.reorderableList.serializedProperty.GetArrayElementAtIndex(currentState.reorderableList.index + 1); 771 | ResetEventState(eventProperty); 772 | 773 | currentState.reorderableList.index++; 774 | currentState.lastSelectedIndex++; 775 | } 776 | 777 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) 778 | { 779 | currentLabelText = label.text; 780 | PrepareState(property); 781 | 782 | HandleKeyboardShortcuts(); 783 | 784 | if (dummyEvent == null) 785 | return; 786 | 787 | if (currentState.reorderableList != null) 788 | { 789 | int oldIndent = EditorGUI.indentLevel; 790 | EditorGUI.indentLevel = 0; 791 | currentState.reorderableList.DoList(position); 792 | EditorGUI.indentLevel = oldIndent; 793 | } 794 | } 795 | 796 | static void InvokeOnTargetEvents(MethodInfo method, object[] targets, object argValue) 797 | { 798 | foreach (object target in targets) 799 | { 800 | if (argValue != null) 801 | method.Invoke(target, new object[] { argValue }); 802 | else 803 | method.Invoke(target, new object[] { }); 804 | } 805 | } 806 | 807 | void DrawInvokeField(Rect position, float headerStartOffset) 808 | { 809 | Rect buttonPos = position; 810 | buttonPos.height *= 0.9f; 811 | buttonPos.width = 51; 812 | buttonPos.x += headerStartOffset + 2; 813 | 814 | Rect textPos = buttonPos; 815 | textPos.x += 6; 816 | textPos.width -= 12; 817 | 818 | Rect inputFieldPos = position; 819 | inputFieldPos.height = buttonPos.height; 820 | inputFieldPos.width = position.width - buttonPos.width - 3 - headerStartOffset; 821 | inputFieldPos.x = buttonPos.x + buttonPos.width + 2; 822 | inputFieldPos.y += 1; 823 | 824 | Rect inputFieldTextPlaceholder = inputFieldPos; 825 | 826 | System.Type[] eventInvokeArgs = GetEventParams(dummyEvent); 827 | 828 | GUIStyle textStyle = EditorStyles.miniLabel; 829 | textStyle.alignment = TextAnchor.MiddleLeft; 830 | 831 | MethodInfo invokeMethod = InvokeFindMethod("Invoke", dummyEvent, dummyEvent, PersistentListenerMode.EventDefined); 832 | FieldInfo serializedField = currentProperty.serializedObject.targetObject.GetType().GetField(currentProperty.name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); 833 | 834 | object[] invokeTargets = currentProperty.serializedObject.targetObjects.Select(target => target == null || serializedField == null ? null : serializedField.GetValue(target)).Where(f => f != null).ToArray(); 835 | 836 | EditorGUI.BeginDisabledGroup(invokeTargets.Length == 0 || invokeMethod == null); 837 | 838 | bool executeInvoke = GUI.Button(buttonPos, "", EditorStyles.miniButton); 839 | GUI.Label(textPos, "Invoke"/* + " (" + string.Join(", ", eventInvokeArgs.Select(e => e.Name).ToArray()) + ")"*/, textStyle); 840 | 841 | if (eventInvokeArgs.Length > 0) 842 | { 843 | System.Type argType = eventInvokeArgs[0]; 844 | 845 | if (argType == typeof(string)) 846 | { 847 | currentState.currentInvokeStrArg = EditorGUI.TextField(inputFieldPos, currentState.currentInvokeStrArg); 848 | 849 | // Draw placeholder text 850 | if (currentState.currentInvokeStrArg.Length == 0) 851 | { 852 | GUIStyle placeholderLabelStyle = EditorStyles.centeredGreyMiniLabel; 853 | placeholderLabelStyle.alignment = TextAnchor.UpperLeft; 854 | 855 | GUI.Label(inputFieldTextPlaceholder, "String argument...", placeholderLabelStyle); 856 | } 857 | 858 | if (executeInvoke) 859 | InvokeOnTargetEvents(invokeMethod, invokeTargets, currentState.currentInvokeStrArg); 860 | } 861 | else if (argType == typeof(int)) 862 | { 863 | currentState.currentInvokeIntArg = EditorGUI.IntField(inputFieldPos, currentState.currentInvokeIntArg); 864 | 865 | if (executeInvoke) 866 | InvokeOnTargetEvents(invokeMethod, invokeTargets, currentState.currentInvokeIntArg); 867 | } 868 | else if (argType == typeof(float)) 869 | { 870 | currentState.currentInvokeFloatArg = EditorGUI.FloatField(inputFieldPos, currentState.currentInvokeFloatArg); 871 | 872 | if (executeInvoke) 873 | InvokeOnTargetEvents(invokeMethod, invokeTargets, currentState.currentInvokeFloatArg); 874 | } 875 | else if (argType == typeof(bool)) 876 | { 877 | currentState.currentInvokeBoolArg = EditorGUI.Toggle(inputFieldPos, currentState.currentInvokeBoolArg); 878 | 879 | if (executeInvoke) 880 | InvokeOnTargetEvents(invokeMethod, invokeTargets, currentState.currentInvokeBoolArg); 881 | } 882 | else if (argType == typeof(Object)) 883 | { 884 | currentState.currentInvokeObjectArg = EditorGUI.ObjectField(inputFieldPos, currentState.currentInvokeObjectArg, argType, true); 885 | 886 | if (executeInvoke) 887 | invokeMethod.Invoke(currentProperty.serializedObject.targetObject, new object[] { currentState.currentInvokeObjectArg }); 888 | } 889 | } 890 | else if (executeInvoke) // No input arg 891 | { 892 | InvokeOnTargetEvents(invokeMethod, invokeTargets, null); 893 | } 894 | 895 | EditorGUI.EndDisabledGroup(); 896 | } 897 | 898 | public override float GetPropertyHeight(SerializedProperty property, GUIContent label) 899 | { 900 | PrepareState(property); 901 | 902 | float height = 0f; 903 | if (currentState.reorderableList != null) 904 | height = currentState.reorderableList.GetHeight(); 905 | 906 | return height; 907 | } 908 | 909 | MethodInfo InvokeFindMethod(string functionName, object targetObject, UnityEventBase eventObject, PersistentListenerMode listenerMode, System.Type argType = null) 910 | { 911 | MethodInfo findMethod = cachedFindMethodInfo; 912 | 913 | if (findMethod == null) 914 | { 915 | // Rather not reinvent the wheel considering this function calls different functions depending on the number of args the event has... 916 | // Unity 2020.1 changed the function signature for the FindMethod method (the second parameter is a Type instead of an object) 917 | findMethod = eventObject.GetType().GetMethod("FindMethod", BindingFlags.NonPublic | BindingFlags.Instance, null, 918 | new System.Type[] { 919 | typeof(string), 920 | #if UNITY_2020_1_OR_NEWER 921 | typeof(System.Type), 922 | #else 923 | typeof(object), 924 | #endif 925 | typeof(PersistentListenerMode), 926 | typeof(System.Type) 927 | }, 928 | null); 929 | 930 | cachedFindMethodInfo = findMethod; 931 | } 932 | 933 | if (findMethod == null) 934 | { 935 | Debug.LogError("Could not find FindMethod function!"); 936 | return null; 937 | } 938 | 939 | #if UNITY_2020_1_OR_NEWER 940 | return findMethod.Invoke(eventObject, new object[] {functionName, targetObject?.GetType(), listenerMode, argType }) as MethodInfo; 941 | #else 942 | return findMethod.Invoke(eventObject, new object[] {functionName, targetObject, listenerMode, argType }) as MethodInfo; 943 | #endif 944 | } 945 | 946 | System.Type[] GetEventParams(UnityEventBase eventIn) 947 | { 948 | MethodInfo methodInfo = InvokeFindMethod("Invoke", eventIn, eventIn, PersistentListenerMode.EventDefined); 949 | return methodInfo.GetParameters().Select(x => x.ParameterType).ToArray(); 950 | } 951 | 952 | string GetEventParamsStr(UnityEventBase eventIn) 953 | { 954 | StringBuilder builder = new StringBuilder(); 955 | System.Type[] methodTypes = GetEventParams(eventIn); 956 | 957 | builder.Append("("); 958 | builder.Append(string.Join(", ", methodTypes.Select(val => val.Name).ToArray())); 959 | builder.Append(")"); 960 | 961 | return builder.ToString(); 962 | } 963 | 964 | string GetFunctionArgStr(string functionName, object targetObject, PersistentListenerMode listenerMode, System.Type argType = null) 965 | { 966 | MethodInfo methodInfo = InvokeFindMethod(functionName, targetObject, dummyEvent, listenerMode, argType); 967 | 968 | if (methodInfo == null) 969 | return ""; 970 | 971 | ParameterInfo[] parameterInfos = methodInfo.GetParameters(); 972 | if (parameterInfos.Length == 0) 973 | return ""; 974 | 975 | return GetTypeName(parameterInfos[0].ParameterType); 976 | } 977 | 978 | void DrawHeaderCallback(Rect headerRect) 979 | { 980 | // We need to know where to position the invoke field based on the length of the title in the UI 981 | GUIContent headerTitle = new GUIContent(string.IsNullOrEmpty(currentLabelText) ? "Event" : currentLabelText + " " + GetEventParamsStr(dummyEvent)); 982 | float headerStartOffset = EditorStyles.label.CalcSize(headerTitle).x; 983 | 984 | GUI.Label(headerRect, headerTitle); 985 | 986 | if (cachedSettings.showInvokeField) 987 | DrawInvokeField(headerRect, headerStartOffset); 988 | } 989 | 990 | Rect[] GetElementRects(Rect rect) 991 | { 992 | Rect[] rects = new Rect[4]; 993 | 994 | rect.height = EditorGUIUtility.singleLineHeight; 995 | rect.y += 2; 996 | 997 | // enabled field 998 | rects[0] = rect; 999 | rects[0].width *= 0.3f; 1000 | 1001 | // game object field 1002 | rects[1] = rects[0]; 1003 | rects[1].x += 1; 1004 | rects[1].width -= 2; 1005 | rects[1].y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; 1006 | 1007 | // function field 1008 | rects[2] = rect; 1009 | rects[2].xMin = rects[1].xMax + 5; 1010 | 1011 | // argument field 1012 | rects[3] = rects[2]; 1013 | rects[3].y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; 1014 | 1015 | return rects; 1016 | } 1017 | 1018 | string GetFunctionDisplayName(SerializedProperty objectProperty, SerializedProperty methodProperty, PersistentListenerMode listenerMode, System.Type argType, bool showArg) 1019 | { 1020 | string methodNameOut = "No Function"; 1021 | 1022 | if (objectProperty.objectReferenceValue == null || methodProperty.stringValue == "") 1023 | return methodNameOut; 1024 | 1025 | MethodInfo methodInfo = InvokeFindMethod(methodProperty.stringValue, objectProperty.objectReferenceValue, dummyEvent, listenerMode, argType); 1026 | string funcName = methodProperty.stringValue.StartsWith("set_") ? methodProperty.stringValue.Substring(4) : methodProperty.stringValue; 1027 | 1028 | if (methodInfo == null) 1029 | { 1030 | methodNameOut = string.Format("", objectProperty.objectReferenceValue.GetType().Name.ToString(), funcName); 1031 | return methodNameOut; 1032 | } 1033 | 1034 | string objectTypeName = objectProperty.objectReferenceValue.GetType().Name; 1035 | Component objectComponent = objectProperty.objectReferenceValue as Component; 1036 | 1037 | if (!cachedSettings.groupSameComponentType && objectComponent != null) 1038 | { 1039 | System.Type objectType = objectProperty.objectReferenceValue.GetType(); 1040 | 1041 | Component[] components = objectComponent.GetComponents(objectType); 1042 | 1043 | if (components.Length > 1) 1044 | { 1045 | int componentID = 0; 1046 | for (int i = 0; i < components.Length; i++) 1047 | { 1048 | if (components[i] == objectComponent) 1049 | { 1050 | componentID = i + 1; 1051 | break; 1052 | } 1053 | } 1054 | 1055 | objectTypeName += string.Format("({0})", componentID); 1056 | } 1057 | } 1058 | 1059 | if (showArg) 1060 | { 1061 | string functionArgStr = GetFunctionArgStr(methodProperty.stringValue, objectProperty.objectReferenceValue, listenerMode, argType); 1062 | methodNameOut = string.Format("{0}.{1} ({2})", objectTypeName, funcName, functionArgStr); 1063 | } 1064 | else 1065 | { 1066 | methodNameOut = string.Format("{0}.{1}", objectTypeName, funcName); 1067 | } 1068 | 1069 | 1070 | return methodNameOut; 1071 | } 1072 | 1073 | System.Type[] GetTypeForListenerMode(PersistentListenerMode listenerMode) 1074 | { 1075 | switch (listenerMode) 1076 | { 1077 | case PersistentListenerMode.EventDefined: 1078 | case PersistentListenerMode.Void: 1079 | return new System.Type[] { }; 1080 | case PersistentListenerMode.Object: 1081 | return new System.Type[] { typeof(Object) }; 1082 | case PersistentListenerMode.Int: 1083 | return new System.Type[] { typeof(int) }; 1084 | case PersistentListenerMode.Float: 1085 | return new System.Type[] { typeof(float) }; 1086 | case PersistentListenerMode.String: 1087 | return new System.Type[] { typeof(string) }; 1088 | case PersistentListenerMode.Bool: 1089 | return new System.Type[] { typeof(bool) }; 1090 | } 1091 | 1092 | return new System.Type[] { }; 1093 | } 1094 | 1095 | void FindValidMethods(Object targetObject, PersistentListenerMode listenerMode, List methodInfos, System.Type[] customArgTypes = null) 1096 | { 1097 | System.Type objectType = targetObject.GetType(); 1098 | 1099 | System.Type[] argTypes; 1100 | 1101 | if (listenerMode == PersistentListenerMode.EventDefined && customArgTypes != null) 1102 | argTypes = customArgTypes; 1103 | else 1104 | argTypes = GetTypeForListenerMode(listenerMode); 1105 | 1106 | List foundMethods = new List(); 1107 | 1108 | // For some reason BindingFlags.FlattenHierarchy does not seem to work, so we manually traverse the base types instead 1109 | while (objectType != null) 1110 | { 1111 | MethodInfo[] foundMethodsOnType = objectType.GetMethods(BindingFlags.Public | (cachedSettings.showPrivateMembers ? BindingFlags.NonPublic : BindingFlags.Default) | BindingFlags.Instance); 1112 | 1113 | foundMethods.AddRange(foundMethodsOnType); 1114 | 1115 | objectType = objectType.BaseType; 1116 | } 1117 | 1118 | foreach (MethodInfo methodInfo in foundMethods) 1119 | { 1120 | // Sadly we can only use functions with void return type since C# throws an error 1121 | if (methodInfo.ReturnType != typeof(void)) 1122 | continue; 1123 | 1124 | ParameterInfo[] methodParams = methodInfo.GetParameters(); 1125 | if (methodParams.Length != argTypes.Length) 1126 | continue; 1127 | 1128 | bool isValidParamMatch = true; 1129 | for (int i = 0; i < methodParams.Length; i++) 1130 | { 1131 | if (!methodParams[i].ParameterType.IsAssignableFrom(argTypes[i])/* && (argTypes[i] != typeof(int) || !methodParams[i].ParameterType.IsEnum)*/) 1132 | { 1133 | isValidParamMatch = false; 1134 | } 1135 | if (listenerMode == PersistentListenerMode.Object && argTypes[i].IsAssignableFrom(methodParams[i].ParameterType)) 1136 | { 1137 | isValidParamMatch = true; 1138 | } 1139 | } 1140 | 1141 | if (!isValidParamMatch) 1142 | continue; 1143 | 1144 | if (!cachedSettings.showPrivateMembers && methodInfo.GetCustomAttributes(typeof(System.ObsoleteAttribute), true).Length > 0) 1145 | continue; 1146 | 1147 | 1148 | FunctionData foundMethodData = new FunctionData(null, targetObject, methodInfo, listenerMode); 1149 | 1150 | methodInfos.Add(foundMethodData); 1151 | } 1152 | } 1153 | 1154 | string GetTypeName(System.Type typeToName) 1155 | { 1156 | if (typeToName == typeof(float)) 1157 | return "float"; 1158 | if (typeToName == typeof(bool)) 1159 | return "bool"; 1160 | if (typeToName == typeof(int)) 1161 | return "int"; 1162 | if (typeToName == typeof(string)) 1163 | return "string"; 1164 | 1165 | return typeToName.Name; 1166 | } 1167 | 1168 | void AddFunctionToMenu(string contentPath, SerializedProperty elementProperty, FunctionData methodData, GenericMenu menu, int componentCount, bool dynamicCall = false) 1169 | { 1170 | string functionName = (methodData.targetMethod.Name.StartsWith("set_") ? methodData.targetMethod.Name.Substring(4) : methodData.targetMethod.Name); 1171 | string argStr = string.Join(", ", methodData.targetMethod.GetParameters().Select(param => GetTypeName(param.ParameterType)).ToArray()); 1172 | 1173 | 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. 1174 | { 1175 | contentPath += functionName; 1176 | } 1177 | else 1178 | { 1179 | if (methodData.targetMethod.Name.StartsWith("set_")) // If it's a property add the arg before the name 1180 | { 1181 | contentPath += argStr + " " + functionName; 1182 | } 1183 | else 1184 | { 1185 | contentPath += functionName + " (" + argStr + ")"; // Add arguments 1186 | } 1187 | } 1188 | 1189 | if (!methodData.targetMethod.IsPublic) 1190 | contentPath += " " + (methodData.targetMethod.IsPrivate ? "" : ""); 1191 | 1192 | if (methodData.targetMethod.GetCustomAttributes(typeof(System.ObsoleteAttribute), true).Length > 0) 1193 | contentPath += " "; 1194 | 1195 | methodData.listenerElement = elementProperty; 1196 | 1197 | SerializedProperty serializedTargetObject = elementProperty.FindPropertyRelative("m_Target"); 1198 | SerializedProperty serializedMethodName = elementProperty.FindPropertyRelative("m_MethodName"); 1199 | SerializedProperty serializedMode = elementProperty.FindPropertyRelative("m_Mode"); 1200 | 1201 | bool itemOn = serializedTargetObject.objectReferenceValue == methodData.targetObject && 1202 | serializedMethodName.stringValue == methodData.targetMethod.Name && 1203 | serializedMode.enumValueIndex == (int)methodData.listenerMode; 1204 | 1205 | menu.AddItem(new GUIContent(contentPath), itemOn, SetEventFunctionCallback, methodData); 1206 | } 1207 | 1208 | void BuildMenuForObject(Object targetObject, SerializedProperty elementProperty, GenericMenu menu, int componentCount = 0) 1209 | { 1210 | List methodInfos = new List(); 1211 | string contentPath = targetObject.GetType().Name + (componentCount > 0 ? string.Format("({0})", componentCount) : "") + "/"; 1212 | 1213 | FindValidMethods(targetObject, PersistentListenerMode.Void, methodInfos); 1214 | FindValidMethods(targetObject, PersistentListenerMode.Int, methodInfos); 1215 | FindValidMethods(targetObject, PersistentListenerMode.Float, methodInfos); 1216 | FindValidMethods(targetObject, PersistentListenerMode.String, methodInfos); 1217 | FindValidMethods(targetObject, PersistentListenerMode.Bool, methodInfos); 1218 | FindValidMethods(targetObject, PersistentListenerMode.Object, methodInfos); 1219 | 1220 | methodInfos = methodInfos.OrderBy(method1 => method1.targetMethod.Name.StartsWith("set_") ? 0 : 1).ThenBy((method1) => method1.targetMethod.Name).ToList(); 1221 | 1222 | // Get event args to determine if we can do a pass through of the arg to the parameter 1223 | System.Type[] eventArgs = dummyEvent.GetType().GetMethod("Invoke").GetParameters().Select(p => p.ParameterType).ToArray(); 1224 | 1225 | bool dynamicBinding = false; 1226 | 1227 | if (eventArgs.Length > 0) 1228 | { 1229 | List dynamicMethodInfos = new List(); 1230 | FindValidMethods(targetObject, PersistentListenerMode.EventDefined, dynamicMethodInfos, eventArgs); 1231 | 1232 | if (dynamicMethodInfos.Count > 0) 1233 | { 1234 | dynamicMethodInfos = dynamicMethodInfos.OrderBy(m => m.targetMethod.Name.StartsWith("set") ? 0 : 1).ThenBy(m => m.targetMethod.Name).ToList(); 1235 | 1236 | dynamicBinding = true; 1237 | 1238 | // Add dynamic header 1239 | menu.AddDisabledItem(new GUIContent(contentPath + string.Format("Dynamic {0}", GetTypeName(eventArgs[0])))); 1240 | menu.AddSeparator(contentPath); 1241 | 1242 | foreach (FunctionData dynamicMethod in dynamicMethodInfos) 1243 | { 1244 | AddFunctionToMenu(contentPath, elementProperty, dynamicMethod, menu, 0, true); 1245 | } 1246 | } 1247 | } 1248 | 1249 | // Add static header if we have dynamic bindings 1250 | if (dynamicBinding) 1251 | { 1252 | menu.AddDisabledItem(new GUIContent(contentPath + "Static Parameters")); 1253 | menu.AddSeparator(contentPath); 1254 | } 1255 | 1256 | foreach (FunctionData method in methodInfos) 1257 | { 1258 | AddFunctionToMenu(contentPath, elementProperty, method, menu, componentCount); 1259 | } 1260 | } 1261 | 1262 | class ComponentTypeCount 1263 | { 1264 | public int TotalCount = 0; 1265 | public int CurrentCount = 1; 1266 | } 1267 | 1268 | GenericMenu BuildPopupMenu(Object targetObj, SerializedProperty elementProperty, System.Type objectArgType) 1269 | { 1270 | GenericMenu menu = new GenericMenu(); 1271 | 1272 | string currentMethodName = elementProperty.FindPropertyRelative("m_MethodName").stringValue; 1273 | 1274 | menu.AddItem(new GUIContent("No Function"), string.IsNullOrEmpty(currentMethodName), ClearEventFunctionCallback, new FunctionData(elementProperty)); 1275 | menu.AddSeparator(""); 1276 | 1277 | if (targetObj is Component) 1278 | { 1279 | targetObj = (targetObj as Component).gameObject; 1280 | } 1281 | else if (!(targetObj is GameObject)) 1282 | { 1283 | // Function menu for asset objects and such 1284 | BuildMenuForObject(targetObj, elementProperty, menu); 1285 | return menu; 1286 | } 1287 | 1288 | // GameObject menu 1289 | BuildMenuForObject(targetObj, elementProperty, menu); 1290 | 1291 | Component[] components = (targetObj as GameObject).GetComponents(); 1292 | Dictionary componentTypeCounts = new Dictionary(); 1293 | 1294 | // Only get the first instance of each component type 1295 | if (cachedSettings.groupSameComponentType) 1296 | { 1297 | components = components.GroupBy(comp => comp.GetType()).Select(group => group.First()).ToArray(); 1298 | } 1299 | 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. 1300 | { 1301 | foreach (Component component in components) 1302 | { 1303 | ComponentTypeCount typeCount; 1304 | if (!componentTypeCounts.TryGetValue(component.GetType(), out typeCount)) 1305 | { 1306 | typeCount = new ComponentTypeCount(); 1307 | componentTypeCounts.Add(component.GetType(), typeCount); 1308 | } 1309 | 1310 | typeCount.TotalCount++; 1311 | } 1312 | 1313 | } 1314 | 1315 | foreach (Component component in components) 1316 | { 1317 | int componentCount = 0; 1318 | 1319 | if (!cachedSettings.groupSameComponentType) 1320 | { 1321 | ComponentTypeCount typeCount = componentTypeCounts[component.GetType()]; 1322 | if (typeCount.TotalCount > 1) 1323 | componentCount = typeCount.CurrentCount++; 1324 | } 1325 | 1326 | BuildMenuForObject(component, elementProperty, menu, componentCount); 1327 | } 1328 | 1329 | return menu; 1330 | } 1331 | 1332 | // Where the event data actually gets added when you choose a function 1333 | static void SetEventFunctionCallback(object functionUserData) 1334 | { 1335 | FunctionData functionData = functionUserData as FunctionData; 1336 | 1337 | SerializedProperty serializedElement = functionData.listenerElement; 1338 | 1339 | SerializedProperty serializedTarget = serializedElement.FindPropertyRelative("m_Target"); 1340 | SerializedProperty serializedMethodName = serializedElement.FindPropertyRelative("m_MethodName"); 1341 | SerializedProperty serializedArgs = serializedElement.FindPropertyRelative("m_Arguments"); 1342 | SerializedProperty serializedMode = serializedElement.FindPropertyRelative("m_Mode"); 1343 | 1344 | SerializedProperty serializedArgAssembly = serializedArgs.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName"); 1345 | SerializedProperty serializedArgObjectValue = serializedArgs.FindPropertyRelative("m_ObjectArgument"); 1346 | 1347 | serializedTarget.objectReferenceValue = functionData.targetObject; 1348 | serializedMethodName.stringValue = functionData.targetMethod.Name; 1349 | serializedMode.enumValueIndex = (int)functionData.listenerMode; 1350 | 1351 | if (functionData.listenerMode == PersistentListenerMode.Object) 1352 | { 1353 | ParameterInfo[] methodParams = functionData.targetMethod.GetParameters(); 1354 | if (methodParams.Length == 1 && typeof(Object).IsAssignableFrom(methodParams[0].ParameterType)) 1355 | serializedArgAssembly.stringValue = methodParams[0].ParameterType.AssemblyQualifiedName; 1356 | else 1357 | serializedArgAssembly.stringValue = typeof(Object).AssemblyQualifiedName; 1358 | } 1359 | else 1360 | { 1361 | serializedArgAssembly.stringValue = typeof(Object).AssemblyQualifiedName; 1362 | serializedArgObjectValue.objectReferenceValue = null; 1363 | } 1364 | 1365 | System.Type argType = EasyEventEditorHandler.FindTypeInAllAssemblies(serializedArgAssembly.stringValue); 1366 | if (!typeof(Object).IsAssignableFrom(argType) || !argType.IsInstanceOfType(serializedArgObjectValue.objectReferenceValue)) 1367 | serializedArgObjectValue.objectReferenceValue = null; 1368 | 1369 | functionData.listenerElement.serializedObject.ApplyModifiedProperties(); 1370 | } 1371 | 1372 | static void ClearEventFunctionCallback(object functionUserData) 1373 | { 1374 | FunctionData functionData = functionUserData as FunctionData; 1375 | 1376 | functionData.listenerElement.FindPropertyRelative("m_Mode").enumValueIndex = (int)PersistentListenerMode.Void; 1377 | functionData.listenerElement.FindPropertyRelative("m_MethodName").stringValue = null; 1378 | functionData.listenerElement.serializedObject.ApplyModifiedProperties(); 1379 | } 1380 | 1381 | void DrawElementCallback(Rect rect, int index, bool active, bool focused) 1382 | { 1383 | SerializedProperty element = listenerArray.GetArrayElementAtIndex(index); 1384 | 1385 | rect.y++; 1386 | Rect[] rects = GetElementRects(rect); 1387 | 1388 | Rect enabledRect = rects[0]; 1389 | Rect gameObjectRect = rects[1]; 1390 | Rect functionRect = rects[2]; 1391 | Rect argRect = rects[3]; 1392 | 1393 | SerializedProperty serializedCallState = element.FindPropertyRelative("m_CallState"); 1394 | SerializedProperty serializedMode = element.FindPropertyRelative("m_Mode"); 1395 | SerializedProperty serializedArgs = element.FindPropertyRelative("m_Arguments"); 1396 | SerializedProperty serializedTarget = element.FindPropertyRelative("m_Target"); 1397 | SerializedProperty serializedMethod = element.FindPropertyRelative("m_MethodName"); 1398 | 1399 | Color oldColor = GUI.backgroundColor; 1400 | GUI.backgroundColor = Color.white; 1401 | 1402 | EditorGUI.PropertyField(enabledRect, serializedCallState, GUIContent.none); 1403 | 1404 | EditorGUI.BeginChangeCheck(); 1405 | 1406 | Object oldTargetObject = serializedTarget.objectReferenceValue; 1407 | 1408 | GUI.Box(gameObjectRect, GUIContent.none); 1409 | EditorGUI.PropertyField(gameObjectRect, serializedTarget, GUIContent.none); 1410 | if (EditorGUI.EndChangeCheck()) 1411 | { 1412 | Object newTargetObject = serializedTarget.objectReferenceValue; 1413 | 1414 | // Attempt to maintain the function pointer and component pointer if someone changes the target object and it has the correct component type on it. 1415 | if (oldTargetObject != null && newTargetObject != null) 1416 | { 1417 | if (oldTargetObject.GetType() != newTargetObject.GetType()) // If not an asset, if it is an asset and the same type we don't do anything 1418 | { 1419 | // 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 1420 | if (typeof(Component).IsAssignableFrom(oldTargetObject.GetType()) && newTargetObject.GetType() == typeof(GameObject)) 1421 | { 1422 | GameObject oldParentObject = ((Component)oldTargetObject).gameObject; 1423 | GameObject newParentObject = (GameObject)newTargetObject; 1424 | 1425 | Component[] oldComponentList = oldParentObject.GetComponents(oldTargetObject.GetType()); 1426 | 1427 | int componentLocationOffset = 0; 1428 | for (int i = 0; i < oldComponentList.Length; ++i) 1429 | { 1430 | if (oldComponentList[i] == oldTargetObject) 1431 | break; 1432 | 1433 | 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. 1434 | componentLocationOffset++; 1435 | } 1436 | 1437 | Component[] newComponentList = newParentObject.GetComponents(oldTargetObject.GetType()); 1438 | 1439 | int newComponentIndex = 0; 1440 | int componentCount = -1; 1441 | for (int i = 0; i < newComponentList.Length; ++i) 1442 | { 1443 | if (componentCount == componentLocationOffset) 1444 | break; 1445 | 1446 | if (newComponentList[i].GetType() == oldTargetObject.GetType()) 1447 | { 1448 | newComponentIndex = i; 1449 | componentCount++; 1450 | } 1451 | } 1452 | 1453 | if (newComponentList.Length > 0 && newComponentList[newComponentIndex].GetType() == oldTargetObject.GetType()) 1454 | { 1455 | serializedTarget.objectReferenceValue = newComponentList[newComponentIndex]; 1456 | } 1457 | else 1458 | { 1459 | serializedMethod.stringValue = null; 1460 | } 1461 | } 1462 | else 1463 | { 1464 | serializedMethod.stringValue = null; 1465 | } 1466 | } 1467 | } 1468 | else 1469 | { 1470 | serializedMethod.stringValue = null; 1471 | } 1472 | } 1473 | 1474 | PersistentListenerMode mode = (PersistentListenerMode)serializedMode.enumValueIndex; 1475 | 1476 | SerializedProperty argument; 1477 | if (serializedTarget.objectReferenceValue == null || string.IsNullOrEmpty(serializedMethod.stringValue)) 1478 | mode = PersistentListenerMode.Void; 1479 | 1480 | switch (mode) 1481 | { 1482 | case PersistentListenerMode.Object: 1483 | case PersistentListenerMode.String: 1484 | case PersistentListenerMode.Bool: 1485 | case PersistentListenerMode.Float: 1486 | argument = serializedArgs.FindPropertyRelative("m_" + System.Enum.GetName(typeof(PersistentListenerMode), mode) + "Argument"); 1487 | break; 1488 | default: 1489 | argument = serializedArgs.FindPropertyRelative("m_IntArgument"); 1490 | break; 1491 | } 1492 | 1493 | string argTypeName = serializedArgs.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName").stringValue; 1494 | System.Type argType = typeof(Object); 1495 | if (!string.IsNullOrEmpty(argTypeName)) 1496 | argType = EasyEventEditorHandler.FindTypeInAllAssemblies(argTypeName) ?? typeof (Object); 1497 | 1498 | if (mode == PersistentListenerMode.Object) 1499 | { 1500 | EditorGUI.BeginChangeCheck(); 1501 | Object result = EditorGUI.ObjectField(argRect, GUIContent.none, argument.objectReferenceValue, argType, true); 1502 | if (EditorGUI.EndChangeCheck()) 1503 | argument.objectReferenceValue = result; 1504 | } 1505 | else if (mode != PersistentListenerMode.Void && mode != PersistentListenerMode.EventDefined) 1506 | EditorGUI.PropertyField(argRect, argument, GUIContent.none); 1507 | 1508 | EditorGUI.BeginDisabledGroup(serializedTarget.objectReferenceValue == null); 1509 | { 1510 | EditorGUI.BeginProperty(functionRect, GUIContent.none, serializedMethod); 1511 | 1512 | GUIContent buttonContent; 1513 | 1514 | if (EditorGUI.showMixedValue) 1515 | { 1516 | buttonContent = new GUIContent("\u2014", "Mixed Values"); 1517 | } 1518 | else 1519 | { 1520 | if (serializedTarget.objectReferenceValue == null || string.IsNullOrEmpty(serializedMethod.stringValue)) 1521 | { 1522 | buttonContent = new GUIContent("No Function"); 1523 | } 1524 | else 1525 | { 1526 | buttonContent = new GUIContent(GetFunctionDisplayName(serializedTarget, serializedMethod, mode, argType, cachedSettings.displayArgumentType)); 1527 | } 1528 | } 1529 | 1530 | if (GUI.Button(functionRect, buttonContent, EditorStyles.popup)) 1531 | { 1532 | BuildPopupMenu(serializedTarget.objectReferenceValue, element, argType).DropDown(functionRect); 1533 | } 1534 | 1535 | EditorGUI.EndProperty(); 1536 | } 1537 | EditorGUI.EndDisabledGroup(); 1538 | } 1539 | 1540 | void SelectCallback(ReorderableList list) 1541 | { 1542 | currentState.lastSelectedIndex = list.index; 1543 | } 1544 | 1545 | void ReorderCallback(ReorderableList list) 1546 | { 1547 | currentState.lastSelectedIndex = list.index; 1548 | } 1549 | 1550 | void AddEventListener(ReorderableList list) 1551 | { 1552 | if (listenerArray.hasMultipleDifferentValues) 1553 | { 1554 | foreach (Object targetObj in listenerArray.serializedObject.targetObjects) 1555 | { 1556 | SerializedObject tempSerializedObject = new SerializedObject(targetObj); 1557 | SerializedProperty listenerArrayProperty = tempSerializedObject.FindProperty(listenerArray.propertyPath); 1558 | listenerArrayProperty.arraySize += 1; 1559 | tempSerializedObject.ApplyModifiedProperties(); 1560 | } 1561 | 1562 | listenerArray.serializedObject.SetIsDifferentCacheDirty(); 1563 | listenerArray.serializedObject.Update(); 1564 | list.index = list.serializedProperty.arraySize - 1; 1565 | } 1566 | else 1567 | { 1568 | ReorderableList.defaultBehaviours.DoAddButton(list); 1569 | } 1570 | 1571 | currentState.lastSelectedIndex = list.index; 1572 | 1573 | // Init default state 1574 | SerializedProperty serialiedListener = listenerArray.GetArrayElementAtIndex(list.index); 1575 | ResetEventState(serialiedListener); 1576 | } 1577 | 1578 | void ResetEventState(SerializedProperty serialiedListener) 1579 | { 1580 | SerializedProperty serializedCallState = serialiedListener.FindPropertyRelative("m_CallState"); 1581 | SerializedProperty serializedTarget = serialiedListener.FindPropertyRelative("m_Target"); 1582 | SerializedProperty serializedMethodName = serialiedListener.FindPropertyRelative("m_MethodName"); 1583 | SerializedProperty serializedMode = serialiedListener.FindPropertyRelative("m_Mode"); 1584 | SerializedProperty serializedArgs = serialiedListener.FindPropertyRelative("m_Arguments"); 1585 | 1586 | serializedCallState.enumValueIndex = (int)UnityEventCallState.RuntimeOnly; 1587 | serializedTarget.objectReferenceValue = null; 1588 | serializedMethodName.stringValue = null; 1589 | serializedMode.enumValueIndex = (int)PersistentListenerMode.Void; 1590 | 1591 | serializedArgs.FindPropertyRelative("m_IntArgument").intValue = 0; 1592 | serializedArgs.FindPropertyRelative("m_FloatArgument").floatValue = 0f; 1593 | serializedArgs.FindPropertyRelative("m_BoolArgument").boolValue = false; 1594 | serializedArgs.FindPropertyRelative("m_StringArgument").stringValue = null; 1595 | serializedArgs.FindPropertyRelative("m_ObjectArgument").objectReferenceValue = null; 1596 | serializedArgs.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName").stringValue = null; 1597 | } 1598 | 1599 | void RemoveCallback(ReorderableList list) 1600 | { 1601 | if (currentState.reorderableList.count > 0) 1602 | { 1603 | ReorderableList.defaultBehaviours.DoRemoveButton(list); 1604 | currentState.lastSelectedIndex = list.index; 1605 | } 1606 | } 1607 | } 1608 | 1609 | } // namespace Merlin 1610 | 1611 | #endif 1612 | -------------------------------------------------------------------------------- /EasyEventEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d1a419a4a0da2f8488bdca226e119ddc 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Merlin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3a8d27326d1650844a12e525cd70e6db 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Easy Event Editor 2 | Drop in replacement for the default Unity event editor drawer that allows listener reordering and a few other things 3 | 4 | 5 | 6 | ## Features 7 | * Drag and drop reordering of Unity Event listeners in the inspector 8 | * Gives easy access to private, internal, and obsolete methods/properties that Unity usually hides 9 | * Allows editing of multiple of the same component type on the same object 10 | * Provides an Invoke button on each event which executes the methods in the list (make sure you are in play mode if they are runtime only!) 11 | * Adds hotkeys for editing events; hotkeys added are Add (Ctrl+A), Copy, Cut, Paste, Delete, and Duplicate 12 | 13 | ## Installation 14 | 1. Grab the EasyEventEditor.cs script by [downloading the zip](https://github.com/Merlin-san/EasyEventEditor/archive/master.zip) and put it somewhere in your project files or download [the most recent release](https://github.com/Merlin-san/EasyEventEditor/releases/latest) and open the .unitypackage in Unity. 15 | 16 | If you want to install from the package manager, [download a zip](https://github.com/Merlin-san/EasyEventEditor/archive/master.zip) or clone the project and extract it to some directory outside of your project or into the `Packages` directory of your project. Then open the package manager and click the '+' button on the bottom left of the window, click "Add package from disk..." and navigate to the `package.json` file in the directory you extracted the zip to. For Unity 2019 and up you can add the package directly from the .git link that github gives you when you click the `Clone or download` dropdown. 17 | 18 | 2. You will now have everything enabled. To disable features that you don't want you will need to open the EEE settings menu. 19 | 3. Open the settings menu under Edit->Easy Event Editor Settings, or in 2018.3 and up open your editor Preferences and find the Easy Event Editor section. These settings will persist between projects, though you'll need to re-import the event editor script. 20 | 21 | 22 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 573c8e6112a931e4c8531353c2931f21 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.merlin.easyeventeditor", 3 | "displayName": "Easy Event Editor", 4 | "version": "1.0.4", 5 | "unity": "2017.4", 6 | "description": "Drop in replacement for the default Unity event editor drawer that allows listener reordering and a few other things", 7 | "keywords": ["Event", "Editor", "Delegate"], 8 | "category": "Productivity", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Merlin-san/EasyEventEditor.git" 12 | }, 13 | "author": { 14 | "name" : "Merlin" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 56a19e0ea97f0a849ad4ca14fe420166 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------