// UltEvents // Copyright 2020 Kybernetik // #if UNITY_EDITOR using System; using System.Collections.Generic; using System.Reflection; using UnityEditor; using UnityEditorInternal; using UnityEngine; using Object = UnityEngine.Object; namespace UltEvents.Editor { [CustomPropertyDrawer(typeof(PersistentCall), true)] internal sealed class PersistentCallDrawer : PropertyDrawer { /************************************************************************************************************************/ public const float RowHeight = 16, Padding = 2, SuggestionButtonWidth = 16; public static readonly GUIStyle PopupButtonStyle, PopupLabelStyle; private static readonly GUIContent ArgumentLabel = new GUIContent(), MethodNameSuggestionLabel = new GUIContent("?", "Suggest a method name"); public static readonly Color ErrorFieldColor = new Color(1, 0.65f, 0.65f); /************************************************************************************************************************/ static PersistentCallDrawer() { PopupButtonStyle = new GUIStyle(EditorStyles.popup) { fixedHeight = RowHeight }; PopupLabelStyle = new GUIStyle(GUI.skin.label) { fontSize = 10, alignment = TextAnchor.MiddleLeft, padding = new RectOffset(4, 14, 0, 0) }; } /************************************************************************************************************************/ public override float GetPropertyHeight(SerializedProperty callProperty, GUIContent label) { if (callProperty.hasMultipleDifferentValues) { if (DrawerState.GetPersistentArgumentsProperty(callProperty).hasMultipleDifferentValues) return EditorGUIUtility.singleLineHeight; if (DrawerState.GetMethodNameProperty(callProperty).hasMultipleDifferentValues) return EditorGUIUtility.singleLineHeight; } if (DrawerState.GetCall(callProperty).GetMethodSafe() == null) return EditorGUIUtility.singleLineHeight; callProperty = DrawerState.GetPersistentArgumentsProperty(callProperty); return (EditorGUIUtility.singleLineHeight + Padding) * (1 + callProperty.arraySize) - Padding; } /************************************************************************************************************************/ public override void OnGUI(Rect area, SerializedProperty callProperty, GUIContent label) { DrawerState.Current.BeginCall(callProperty); var propertyarea = area; // If we are in the reorderable list of an event, adjust the property area to cover the list bounds. if (DrawerState.Current.CachePreviousCalls) { propertyarea.xMin -= 20; propertyarea.yMin -= 4; propertyarea.width += 4; } label = EditorGUI.BeginProperty(propertyarea, label, callProperty); { const float Space = 2; var x = area.x; var xMax = area.xMax; area.height = RowHeight; // Target Field. area.xMax = EditorGUIUtility.labelWidth + 12; bool autoOpenMethodMenu; DoTargetFieldGUI(area, DrawerState.Current.TargetProperty, DrawerState.Current.MethodNameProperty, out autoOpenMethodMenu); EditorGUI.showMixedValue = DrawerState.Current.PersistentArgumentsProperty.hasMultipleDifferentValues || DrawerState.Current.MethodNameProperty.hasMultipleDifferentValues; var method = EditorGUI.showMixedValue ? null : DrawerState.Current.call.GetMethodSafe(); // Method Name Dropdown. area.x += area.width + Space; area.xMax = xMax; DoMethodFieldGUI(area, method, autoOpenMethodMenu); // Persistent Arguments. if (method != null) { area.x = x; area.xMax = xMax; DrawerState.Current.callParameters = method.GetParameters(); if (DrawerState.Current.callParameters.Length == DrawerState.Current.PersistentArgumentsProperty.arraySize) { var labelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth -= area.x - 14; for (int i = 0; i < DrawerState.Current.callParameters.Length; i++) { DrawerState.Current.parameterIndex = i; area.y += area.height + Padding; ArgumentLabel.text = DrawerState.Current.callParameters[i].Name; var argumentProperty = DrawerState.Current.PersistentArgumentsProperty.GetArrayElementAtIndex(i); if (argumentProperty.propertyPath != "") { EditorGUI.PropertyField(area, argumentProperty, ArgumentLabel); } else { if (GUI.Button(area, new GUIContent( "Reselect these objects to show arguments", "This is the result of a bug in the way Unity updates the SerializedProperty for an array after it is resized while multiple objects are selected"))) { var selection = Selection.objects; Selection.objects = new Object[0]; EditorApplication.delayCall += () => Selection.objects = selection; } break; } } EditorGUIUtility.labelWidth = labelWidth; } else { Debug.LogError("Method parameter count doesn't match serialized argument count " + DrawerState.Current.callParameters.Length + " : " + DrawerState.Current.PersistentArgumentsProperty.arraySize); } DrawerState.Current.callParameters = null; } EditorGUI.showMixedValue = false; } EditorGUI.EndProperty(); DrawerState.Current.EndCall(); } /************************************************************************************************************************/ #region Target Field /************************************************************************************************************************/ private static void DoTargetFieldGUI(Rect area, SerializedProperty targetProperty, SerializedProperty methodNameProperty, out bool autoOpenMethodMenu) { autoOpenMethodMenu = false; // Type field for a static type. if (targetProperty.objectReferenceValue == null && !targetProperty.hasMultipleDifferentValues) { var methodName = methodNameProperty.stringValue; string typeName; var lastDot = methodName.LastIndexOf('.'); if (lastDot >= 0) { typeName = methodName.Substring(0, lastDot); lastDot++; methodName = methodName.Substring(lastDot, methodName.Length - lastDot); } else typeName = ""; var color = GUI.color; if (Type.GetType(typeName) == null) GUI.color = ErrorFieldColor; const float ObjectPickerButtonWidth = 35, Padding = 2; area.width -= ObjectPickerButtonWidth + Padding; EditorGUI.BeginChangeCheck(); typeName = ObjectPicker.DrawTypeField(area, typeName, GetAllTypes, 0, EditorStyles.miniButton); if (EditorGUI.EndChangeCheck()) { methodNameProperty.stringValue = typeName + "." + methodName; } HandleTargetFieldDragAndDrop(area, ref autoOpenMethodMenu); GUI.color = color; area.x += area.width + Padding; area.width = ObjectPickerButtonWidth; } // Object field for an object reference. DoTargetObjectFieldGUI(area, targetProperty, ref autoOpenMethodMenu); } /************************************************************************************************************************/ private static void DoTargetObjectFieldGUI(Rect area, SerializedProperty targetProperty, ref bool autoOpenMethodMenu) { if (targetProperty.hasMultipleDifferentValues) EditorGUI.showMixedValue = true; EditorGUI.BeginChangeCheck(); var oldTarget = targetProperty.objectReferenceValue; var target = EditorGUI.ObjectField(area, oldTarget, typeof(Object), true); if (EditorGUI.EndChangeCheck()) { SetBestTarget(oldTarget, target, out autoOpenMethodMenu); } EditorGUI.showMixedValue = false; } /************************************************************************************************************************/ private static List _AllTypes; private static List GetAllTypes() { if (_AllTypes == null) { // Gather all types in all currently loaded assemblies. _AllTypes = new List(4192); var assemblies = AppDomain.CurrentDomain.GetAssemblies(); for (int i = 0; i < assemblies.Length; i++) { var types = assemblies[i].GetTypes(); for (int j = 0; j < types.Length; j++) { var type = types[j]; if (!type.ContainsGenericParameters &&// No Generics (because the type picker field doesn't let you pick generic parameters). !type.IsInterface &&// No Interfaces (because they can't have any static methods). !type.IsDefined(typeof(ObsoleteAttribute), true) &&// No Obsoletes. type.GetMethods(UltEventUtils.StaticBindings).Length > 0)// No types without any static methods. { // The type might still not have any valid methods, but at least we've narrowed down the list a lot. _AllTypes.Add(type); } } } _AllTypes.Sort((a, b) => a.FullName.CompareTo(b.FullName)); // We probably just allocated thousands of arrays with all those GetMethods calls, so call for a cleanup imediately. GC.Collect(); } return _AllTypes; } /************************************************************************************************************************/ private static void HandleTargetFieldDragAndDrop(Rect area, ref bool autoOpenMethodMenu) { // Drag and drop objects into the type field. switch (Event.current.type) { case EventType.Repaint: case EventType.DragUpdated: { if (!area.Contains(Event.current.mousePosition)) break; var dragging = DragAndDrop.objectReferences; if (dragging != null && dragging.Length == 1) { DragAndDrop.visualMode = DragAndDropVisualMode.Copy; } } break; case EventType.DragPerform: { if (!area.Contains(Event.current.mousePosition)) break; var dragging = DragAndDrop.objectReferences; if (dragging != null && dragging.Length == 1) { SetBestTarget(DrawerState.Current.TargetProperty.objectReferenceValue, dragging[0], out autoOpenMethodMenu); DragAndDrop.AcceptDrag(); GUI.changed = true; } } break; default: break; } } /************************************************************************************************************************/ private static void SetBestTarget(Object oldTarget, Object newTarget, out bool autoOpenMethodMenu) { // It's more likely that the user intends to target a method on a Component than the GameObject itself so // if a GameObject was dropped in, try to select a component with the same type as the old target, // otherwise select it's first component after the Transform. var gameObject = newTarget as GameObject; if (!(oldTarget is GameObject) && !ReferenceEquals(gameObject, null)) { var oldComponent = oldTarget as Component; if (!ReferenceEquals(oldComponent, null)) { newTarget = gameObject.GetComponent(oldComponent.GetType()); if (newTarget != null) goto FoundTarget; } var components = gameObject.GetComponents(); newTarget = components.Length > 1 ? components[1] : components[0]; } FoundTarget: SetTarget(newTarget); autoOpenMethodMenu = BoolPref.AutoOpenMenu && newTarget != null && DrawerState.Current.call.GetMethodSafe() == null; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ private static void DoMethodFieldGUI(Rect area, MethodBase method, bool autoOpenMethodMenu) { EditorGUI.BeginProperty(area, null, DrawerState.Current.MethodNameProperty); { if (includeRemoveButton) area.width -= RemoveButtonWidth; var color = GUI.color; string label; if (EditorGUI.showMixedValue) { label = "Mixed Values"; } else if (method != null) { label = MethodSelectionMenu.GetMethodSignature(method, false); DoGetSetToggleGUI(ref area, method); } else { var methodName = DrawerState.Current.MethodNameProperty.stringValue; Type declaringType; PersistentCall.GetMethodDetails(methodName, DrawerState.Current.TargetProperty.objectReferenceValue, out declaringType, out label); DoMethodNameSuggestionGUI(ref area, declaringType, methodName); GUI.color = ErrorFieldColor; } if (autoOpenMethodMenu || (GUI.Button(area, GUIContent.none, PopupButtonStyle) && Event.current.button == 0)) { MethodSelectionMenu.ShowMenu(area); } GUI.color = color; PopupLabelStyle.fontStyle = DrawerState.Current.MethodNameProperty.prefabOverride ? FontStyle.Bold : FontStyle.Normal; GUI.Label(area, label, PopupLabelStyle); } EditorGUI.EndProperty(); } /************************************************************************************************************************/ private static float _GetSetWidth; private static float GetSetWidth { get { if (_GetSetWidth <= 0) { float _, width; ArgumentLabel.text = "Get"; GUI.skin.button.CalcMinMaxWidth(ArgumentLabel, out _, out width); ArgumentLabel.text = "Set"; GUI.skin.button.CalcMinMaxWidth(ArgumentLabel, out _, out _GetSetWidth); if (_GetSetWidth < width) _GetSetWidth = width; } return _GetSetWidth; } } /************************************************************************************************************************/ private static void DoGetSetToggleGUI(ref Rect area, MethodBase method) { // Check if the method name starts with "get_" or "set_". // Check the underscore first since it's hopefully the rarest so it can break out early. var name = method.Name; if (name.Length <= 4 || name[3] != '_' || name[2] != 't' || name[1] != 'e') return; var first = name[0]; var isGet = first == 'g'; var isSet = first == 's'; if (!isGet && !isSet) return; var methodName = (isGet ? "set_" : "get_") + name.Substring(4, name.Length - 4); var oppositePropertyMethod = method.DeclaringType.GetMethod(methodName, UltEventUtils.AnyAccessBindings); if (oppositePropertyMethod == null || (isGet && !MethodSelectionMenu.IsSupported(method.GetReturnType()))) return; area.width -= GetSetWidth + Padding; var buttonArea = new Rect( area.x + area.width + Padding, area.y, GetSetWidth, area.height); if (GUI.Button(buttonArea, isGet ? "Get" : "Set")) { var cachedState = new DrawerState(); cachedState.CopyFrom(DrawerState.Current); EditorApplication.delayCall += () => { DrawerState.Current.CopyFrom(cachedState); SetMethod(oppositePropertyMethod); DrawerState.Current.Clear(); InternalEditorUtility.RepaintAllViews(); }; } } /************************************************************************************************************************/ private static void DoMethodNameSuggestionGUI(ref Rect area, Type declaringType, string methodName) { if (declaringType == null || string.IsNullOrEmpty(methodName)) return; var lastDot = methodName.LastIndexOf('.'); if (lastDot >= 0) { lastDot++; if (lastDot >= methodName.Length) return; methodName = methodName.Substring(lastDot); } var methods = declaringType.GetMethods(UltEventUtils.AnyAccessBindings); if (methods.Length == 0) return; area.width -= SuggestionButtonWidth + Padding; var buttonArea = new Rect( area.x + area.width + Padding, area.y, SuggestionButtonWidth, area.height); if (GUI.Button(buttonArea, MethodNameSuggestionLabel)) { var cachedState = new DrawerState(); cachedState.CopyFrom(DrawerState.Current); EditorApplication.delayCall += () => { DrawerState.Current.CopyFrom(cachedState); var bestMethod = methods[0]; var bestDistance = UltEventUtils.CalculateLevenshteinDistance(methodName, bestMethod.Name); var i = 1; for (; i < methods.Length; i++) { var method = methods[i]; var distance = UltEventUtils.CalculateLevenshteinDistance(methodName, method.Name); if (bestDistance > distance) { bestDistance = distance; bestMethod = method; } } SetMethod(bestMethod); DrawerState.Current.Clear(); InternalEditorUtility.RepaintAllViews(); }; } } /************************************************************************************************************************/ public static void SetTarget(Object target) { DrawerState.Current.TargetProperty.objectReferenceValue = target; DrawerState.Current.TargetProperty.serializedObject.ApplyModifiedProperties(); if (target == null || DrawerState.Current.call.GetMethodSafe() == null) { SetMethod(null); } } /************************************************************************************************************************/ public static void SetMethod(MethodInfo methodInfo) { DrawerState.Current.CallProperty.ModifyValues((call) => { if (call != null) call.SetMethod(methodInfo, DrawerState.Current.TargetProperty.objectReferenceValue); }, "Set Method"); } /************************************************************************************************************************/ #region Remove Button /************************************************************************************************************************/ public const float RemoveButtonWidth = 18; public static bool includeRemoveButton; /************************************************************************************************************************/ public static bool DoRemoveButtonGUI(Rect rowArea) { includeRemoveButton = false; rowArea.xMin = rowArea.xMax - RemoveButtonWidth + 2; rowArea.height = EditorGUIUtility.singleLineHeight + 2; return GUI.Button(rowArea, ReorderableList.defaultBehaviours.iconToolbarMinus, ReorderableList.defaultBehaviours.preButton); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } } #endif