using System; using UnityEngine; using UnityEditor; using System.Linq; using System.Reflection; using UnityEditor.Search; using System.Collections; using UnityEngine.UIElements; using UnityEditor.UIElements; using System.Collections.Generic; using EditorAttributes.Editor.Utility; using Object = UnityEngine.Object; namespace EditorAttributes.Editor { public class PropertyDrawerBase : PropertyDrawer { internal const string GROUPED_PROPERTY_ID = "GroupedProperty"; protected bool CanApplyGlobalColor => EditorExtension.GLOBAL_COLOR != EditorExtension.DEFAULT_GLOBAL_COLOR; public override VisualElement CreatePropertyGUI(SerializedProperty property) => CreatePropertyField(property); public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { EditorGUI.PropertyField(position, property, label, true); var helpBoxStyle = GUI.skin.GetStyle("HelpBox"); helpBoxStyle.richText = true; EditorGUILayout.HelpBox("You cannot use EditorAttributes with ImGUI based editors. " + "Convert your editor to UI Toolkit for attributes to work, or remove the attributes from properties drawn by the editor script.", MessageType.Warning); } /// /// Override this function to customize the copied value from an element with using /// /// The element on which the context menu was added /// The attached serialized property /// The string that will be copied into the clipboard protected virtual string CopyValue(VisualElement element, SerializedProperty property) => GetCopyPropertyValue(property); /// /// Override this function to customize the paste behaivour for an element with using /// /// The element on which the context menu was added /// The attached serialized property /// The current clipboard value protected virtual void PasteValue(VisualElement element, SerializedProperty property, string clipboardValue) => SetPropertyValueFromString(clipboardValue, property); /// /// Creates a properly binded property field from a serialized property /// /// The serialized property /// The binded property field public static PropertyField CreatePropertyField(SerializedProperty property, string label = "") { var propertyField = new PropertyField(property, string.IsNullOrWhiteSpace(label) ? property.displayName : label); propertyField.BindProperty(property.serializedObject); return propertyField; } /// /// Sets the value of a property from a string /// /// The string value to convert /// The serialized property to assign the value to protected void SetPropertyValueFromString(string value, SerializedProperty property) { try { switch (property.propertyType) { case SerializedPropertyType.Integer: property.intValue = Convert.ToInt32(value); break; case SerializedPropertyType.Float: property.floatValue = Convert.ToSingle(value); break; case SerializedPropertyType.Boolean: property.boolValue = Convert.ToBoolean(value); break; case SerializedPropertyType.String: property.stringValue = value; break; case SerializedPropertyType.Character: property.intValue = Convert.ToChar(value); break; case SerializedPropertyType.Color: property.colorValue = ColorUtility.TryParseHtmlString(value, out Color color) ? color : Color.white; break; case SerializedPropertyType.Vector2: property.vector2Value = VectorUtils.ParseVector2(value); break; case SerializedPropertyType.Vector3: property.vector3Value = VectorUtils.ParseVector3(value); break; case SerializedPropertyType.Vector4: property.vector4Value = VectorUtils.ParseVector4(value); break; case SerializedPropertyType.Vector2Int: property.vector2IntValue = VectorUtils.ParseVector2Int(value); break; case SerializedPropertyType.Vector3Int: property.vector3IntValue = VectorUtils.ParseVector3Int(value); break; default: Debug.LogWarning($"The type {property.propertyType} is not supported", property.serializedObject.targetObject); break; } property.serializedObject.ApplyModifiedProperties(); } catch (FormatException) { Debug.LogError($"Could not convert the value \"{value}\" to {property.propertyType}", property.serializedObject.targetObject); } } /// /// Gets the value of a serialzied property and returns it as a string /// /// The serialized property to get the value from /// The serialized property value as a string protected string GetPropertyValueAsString(SerializedProperty property) => property.propertyType switch { SerializedPropertyType.String => property.stringValue, SerializedPropertyType.Integer or SerializedPropertyType.LayerMask or SerializedPropertyType.Character => property.intValue.ToString(), SerializedPropertyType.Enum => IsPropertyEnumFlag() ? property.enumValueFlag.ToString() : property.enumDisplayNames[property.enumValueIndex], SerializedPropertyType.Float => property.floatValue.ToString(), SerializedPropertyType.Boolean => property.boolValue.ToString(), SerializedPropertyType.Vector2 => property.vector2Value.ToString(), SerializedPropertyType.Vector3 => property.vector3Value.ToString(), SerializedPropertyType.Vector4 => property.vector4Value.ToString(), SerializedPropertyType.Rect => property.vector4Value.ToString(), SerializedPropertyType.Bounds => property.boundsValue.ToString(), SerializedPropertyType.Color => property.colorValue.ToString(), SerializedPropertyType.Gradient => property.gradientValue.ToString(), SerializedPropertyType.AnimationCurve => property.animationCurveValue.ToString(), SerializedPropertyType.Quaternion => property.quaternionValue.ToString(), SerializedPropertyType.Vector2Int => property.vector2IntValue.ToString(), SerializedPropertyType.Vector3Int => property.vector3IntValue.ToString(), SerializedPropertyType.RectInt => property.rectIntValue.ToString(), SerializedPropertyType.BoundsInt => property.boundsIntValue.ToString(), SerializedPropertyType.Hash128 => property.hash128Value.ToString(), SerializedPropertyType.ArraySize => property.arraySize.ToString(), SerializedPropertyType.FixedBufferSize => property.fixedBufferSize.ToString(), SerializedPropertyType.ObjectReference => property.objectReferenceValue.ToString(), SerializedPropertyType.ExposedReference => property.exposedReferenceValue.ToString(), SerializedPropertyType.ManagedReference => property.managedReferenceValue.ToString(), _ => string.Empty }; /// /// Converts the values of a collection into strings /// /// The name of the collection to convert /// The serialized property /// The member info of the collection /// The error box to display any errors to /// The values of the collection in a list of strings protected static List ConvertCollectionValuesToStrings(string collectionName, SerializedProperty serializedProperty, MemberInfo memberInfo, HelpBox errorBox) { var stringList = new List(); var memberInfoValue = ReflectionUtility.GetMemberInfoValue(memberInfo, serializedProperty); if (memberInfoValue is Array array) { foreach (var item in array) stringList.Add(item == null ? "NULL" : item.ToString()); } else if (memberInfoValue is IList list) { foreach (var item in list) stringList.Add(item == null ? "NULL" : item.ToString()); } else if (memberInfoValue is IDictionary dictionary) { foreach (DictionaryEntry item in dictionary) stringList.Add(item.Value == null ? "NULL" : item.Value.ToString()); } else { errorBox.text = $"Could not find the collection {collectionName}"; } return stringList; } /// /// Finds a nested serialized property /// /// The serialized property /// The name of the property to find /// The nested serialized property protected static SerializedProperty FindNestedProperty(SerializedProperty property, string propertyName) { var propertyPath = property.propertyPath; var cutPathIndex = propertyPath.LastIndexOf('.'); if (cutPathIndex == -1) { return property.serializedObject.FindProperty(propertyName); } else { propertyPath = propertyPath[..cutPathIndex]; return property.serializedObject.FindProperty(propertyPath).FindPropertyRelative(propertyName); } } /// /// Checks to see if a seralized property is a list or array /// /// The serialized property to check /// True if the property is a list or array, false otherwise public static bool IsPropertyCollection(SerializedProperty property) => property.propertyPath.Contains("Array"); /// /// Gets the collection property from a collection item property /// /// The collection item property /// The collection property public static SerializedProperty GetCollectionProperty(SerializedProperty property) { string path = property.propertyPath; int index = path.LastIndexOf(".Array.data["); if (index >= 0) { string collectionPath = path[..index]; return property.serializedObject.FindProperty(collectionPath); } return null; } /// /// Gets the name of a serialized property accounting for C# properties /// /// The name of the property to look for /// The serialized property /// The name of the serialized property public static string GetSerializedPropertyName(string propertyName, SerializedProperty property) { var memberInfo = ReflectionUtility.GetValidMemberInfo(propertyName, property); return memberInfo is PropertyInfo ? $"<{propertyName}>k__BackingField" : propertyName; } /// /// Checks to see if the serialized property is a flagged enum /// /// True if the serialized property type is a flagged enum protected bool IsPropertyEnumFlag() => fieldInfo.FieldType.IsDefined(typeof(FlagsAttribute), false); /// /// Displays an error box in the inspector /// /// The root visual element /// The help box to displaying the errors public static void DisplayErrorBox(VisualElement root, HelpBox errorBox) { errorBox.messageType = HelpBoxMessageType.Error; if (!string.IsNullOrEmpty(errorBox.text)) { AddElement(root, errorBox); } else { RemoveElement(root, errorBox); } } /// /// Schedules an action to update /// /// The visual element to schedule the update /// The logic to execute on the specified element /// The update interval in milliseconds /// The scheduled visual element item public static IVisualElementScheduledItem UpdateVisualElement(VisualElement visualElement, Action logicToUpdate, long intervalMs = 60) { logicToUpdate.Invoke(); // Execute the logic once so we don't have to wait for the first execution of the scheduler return visualElement.schedule.Execute(logicToUpdate).Every(intervalMs); } /// /// Schedules an action to execute after a delay /// /// The visual element to schedule the execution /// The logic to execute on the specified element /// The execution delay in milliseconds /// The scheduled visual element item public static IVisualElementScheduledItem ExecuteLater(VisualElement visualElement, Action logicToExecute, long delayMs = 1) => visualElement.schedule.Execute(logicToExecute).StartingIn(delayMs); /// /// Add an element from another visual element if it doesn't exist /// /// The root to add the element on /// The element to add public static void AddElement(VisualElement root, VisualElement element) { if (!root.Contains(element)) root.Add(element); } /// /// Removes an element from another visual element if it exists /// /// The owner containing the element /// The element to remove public static void RemoveElement(VisualElement owner, VisualElement element) { if (owner.Contains(element)) owner.Remove(element); } /// /// Gets the value of a condition for a conditional attribute /// /// The member info of the condition /// The conditional attribute /// The serialized property /// The error box to display any errors to /// True if the condition is satisfied public static bool GetConditionValue(MemberInfo memberInfo, IConditionalAttribute conditionalAttribute, SerializedProperty serializedProperty, HelpBox errorBox) { var memberInfoType = ReflectionUtility.GetMemberInfoType(memberInfo); if (memberInfoType == null) { errorBox.text = $"The provided condition \"{conditionalAttribute.ConditionName}\" could not be found"; return false; } if (memberInfoType == typeof(bool)) { var memberInfoValue = ReflectionUtility.GetMemberInfoValue(memberInfo, serializedProperty); if (memberInfoValue == null) return false; return (bool)memberInfoValue; } else if (memberInfoType.IsEnum) { var memberInfoValue = ReflectionUtility.GetMemberInfoValue(memberInfo, serializedProperty); if (memberInfoValue == null) return false; return (int)memberInfoValue == conditionalAttribute.EnumValue; } errorBox.text = $"The provided condition \"{conditionalAttribute.ConditionName}\" is not a valid boolean or an enum"; return false; } internal static bool GetConditionValue(MemberInfo memberInfo, IConditionalAttribute conditionalAttribute, object targetObject, HelpBox errorBox) // Internal function used for the button drawer { var memberInfoType = ReflectionUtility.GetMemberInfoType(memberInfo); if (memberInfoType == null) { errorBox.text = $"The provided condition \"{conditionalAttribute.ConditionName}\" could not be found"; return false; } if (memberInfoType == typeof(bool)) { return (bool)ReflectionUtility.GetMemberInfoValue(memberInfo, targetObject); } else if (memberInfoType.IsEnum) { return (int)ReflectionUtility.GetMemberInfoValue(memberInfo, targetObject) == conditionalAttribute.EnumValue; } errorBox.text = $"The provided condition \"{conditionalAttribute.ConditionName}\" is not a valid boolean or an enum"; return false; } /// /// Gets the string value from a member if the input mode is set to Dynamic /// /// The string input that may contain the member name /// The serialized property /// The dynamic string attribute /// The error box to display any errors to /// If the input mode is Constant will return the base input string, if is Dynamic will return the string value of the member public static string GetDynamicString(string inputText, SerializedProperty property, IDynamicStringAttribute dynamicStringAttribute, HelpBox errorBox) { switch (dynamicStringAttribute.StringInputMode) { default: case StringInputMode.Constant: return inputText; case StringInputMode.Dynamic: var memberInfo = ReflectionUtility.GetValidMemberInfo(inputText, property); if (memberInfo == null) { errorBox.text = $"The member {inputText} could not be found"; return inputText; } var memberValue = ReflectionUtility.GetMemberInfoValue(memberInfo, property); var memberType = ReflectionUtility.GetMemberInfoType(memberInfo); if (memberValue == null) return inputText; if (memberType == typeof(string)) return memberValue.ToString(); errorBox.text = $"The member {inputText} needs to return a string"; return inputText; } } /// /// Adds the property context menu to a non property element /// /// The element to add the context menu to /// The serialized property protected void AddPropertyContextMenu(VisualElement element, SerializedProperty property) { if (element is PropertyField) Debug.LogError("Can't add the property context menu to a property field since it already has one by default."); element.AddManipulator(new ContextualMenuManipulator((@event) => { string searchText = $"h:#{property.serializedObject.targetObject.GetType().Name}.{property.propertyPath}={GetPropertyValueAsString(property).Replace(" ", "")}"; @event.menu.AppendAction("Copy Property Path", (action) => EditorGUIUtility.systemCopyBuffer = property.propertyPath); @event.menu.AppendAction("Search Same Property Value", (action) => SearchService.ShowWindow().SetSearchText(searchText)); @event.menu.AppendSeparator(); @event.menu.AppendAction("Copy", (action) => EditorGUIUtility.systemCopyBuffer = CopyValue(element, property)); @event.menu.AppendAction("Paste", (action) => PasteValue(element, property, ParsePropertyClipboardValue(property, EditorGUIUtility.systemCopyBuffer))); @event.menu.AppendSeparator(); })); } private string GetCopyPropertyValue(SerializedProperty property) { string propertyValue = GetPropertyValueAsString(property); return property.propertyType switch { SerializedPropertyType.Vector2 or SerializedPropertyType.Vector2Int => $"Vector2{propertyValue}", SerializedPropertyType.Vector3 or SerializedPropertyType.Vector3Int => $"Vector3{propertyValue}", SerializedPropertyType.Rect or SerializedPropertyType.RectInt => $"Rect{propertyValue}", SerializedPropertyType.Bounds or SerializedPropertyType.BoundsInt => $"Bounds{propertyValue}", SerializedPropertyType.Vector4 or SerializedPropertyType.Quaternion => property.type + propertyValue, SerializedPropertyType.LayerMask => $"LayerMask({propertyValue})", SerializedPropertyType.Enum => $"Enum:{(IsPropertyEnumFlag() ? Convert.ToString(property.enumValueFlag, 2) : propertyValue)}", _ => propertyValue }; } private string ParsePropertyClipboardValue(SerializedProperty property, string clipboardValue) => property.propertyType switch { SerializedPropertyType.Vector2 or SerializedPropertyType.Vector2Int => clipboardValue.Replace("Vector2", ""), SerializedPropertyType.Vector3 or SerializedPropertyType.Vector3Int => clipboardValue.Replace("Vector3", ""), SerializedPropertyType.Rect or SerializedPropertyType.RectInt => clipboardValue.Replace("Rect", ""), SerializedPropertyType.Bounds or SerializedPropertyType.BoundsInt => clipboardValue.Replace("Bounds", ""), SerializedPropertyType.Vector4 or SerializedPropertyType.Quaternion => clipboardValue.Replace(property.type, ""), SerializedPropertyType.LayerMask => clipboardValue.Replace("LayerMask", ""), SerializedPropertyType.Enum => clipboardValue.Replace("Enum:", ""), _ => clipboardValue }; private protected string CreatePropertySaveKey(SerializedProperty property, string key) => $"{property.serializedObject.targetObject.GetInstanceID()}_{property.propertyPath}_{key}"; /// /// Invokes a function on all specified targets /// /// The property to get the targets from /// The name of the function to invoke /// Parameter values for the function public static void InvokeFunctionOnAllTargets(Object[] targets, string functionName, object[] parameterValues = null) { foreach (var target in targets) { var methodInfo = ReflectionUtility.FindFunction(functionName, target); Undo.RecordObject(target, $"Invoke {functionName}"); methodInfo.Invoke(target, parameterValues); EditorUtility.SetDirty(target); } } /// /// Applies the help box style to a visual element /// /// The element to apply the style to public static void ApplyBoxStyle(VisualElement visualElement) { visualElement.style.borderTopLeftRadius = 3f; visualElement.style.borderTopRightRadius = 3f; visualElement.style.borderBottomLeftRadius = 3f; visualElement.style.borderBottomRightRadius = 3f; visualElement.style.borderBottomWidth = 1f; visualElement.style.borderTopWidth = 1f; visualElement.style.borderLeftWidth = 1f; visualElement.style.borderRightWidth = 1f; visualElement.style.borderTopColor = new Color(26f / 255f, 26f / 255f, 26f / 255f); visualElement.style.borderBottomColor = new Color(26f / 255f, 26f / 255f, 26f / 255f); visualElement.style.borderLeftColor = new Color(26f / 255f, 26f / 255f, 26f / 255f); visualElement.style.borderRightColor = new Color(26f / 255f, 26f / 255f, 26f / 255f); visualElement.style.backgroundColor = EditorExtension.GLOBAL_COLOR != EditorExtension.DEFAULT_GLOBAL_COLOR ? EditorExtension.GLOBAL_COLOR / 2f : new Color(63f / 255f, 63f / 255f, 63f / 255f); visualElement.style.paddingTop = 3f; visualElement.style.paddingBottom = 3f; visualElement.style.paddingLeft = 3f; visualElement.style.paddingRight = 3f; visualElement.style.marginTop = 1f; visualElement.style.marginBottom = 1f; visualElement.style.marginRight = 3f; visualElement.style.marginLeft = 3f; } /// /// Copies all of the style values from a to another /// /// The element to copy the style from /// The element to copy the style to public void CopyStyle(VisualElement copyFrom, VisualElement copyTo) { copyTo.style.position = copyFrom.style.position; copyTo.style.top = copyFrom.style.top; copyTo.style.bottom = copyFrom.style.bottom; copyTo.style.left = copyFrom.style.left; copyTo.style.right = copyFrom.style.right; copyTo.style.paddingTop = copyFrom.style.paddingTop; copyTo.style.paddingBottom = copyFrom.style.paddingBottom; copyTo.style.paddingLeft = copyFrom.style.paddingLeft; copyTo.style.paddingRight = copyFrom.style.paddingRight; copyTo.style.alignContent = copyFrom.style.alignContent; copyTo.style.alignItems = copyFrom.style.alignItems; copyTo.style.alignSelf = copyFrom.style.alignSelf; copyTo.style.flexBasis = copyFrom.style.flexBasis; copyTo.style.flexDirection = copyFrom.style.flexDirection; copyTo.style.flexWrap = copyFrom.style.flexWrap; copyTo.style.width = copyFrom.style.width; copyTo.style.height = copyFrom.style.height; copyTo.style.justifyContent = copyFrom.style.justifyContent; copyTo.style.marginTop = copyFrom.style.marginTop; copyTo.style.marginBottom = copyFrom.style.marginBottom; copyTo.style.marginLeft = copyFrom.style.marginLeft; copyTo.style.marginRight = copyFrom.style.marginRight; copyTo.style.transformOrigin = copyFrom.style.transformOrigin; copyTo.style.translate = copyFrom.style.translate; copyTo.style.rotate = copyFrom.style.rotate; copyTo.style.scale = copyFrom.style.scale; copyTo.style.transitionDelay = copyFrom.style.transitionDelay; copyTo.style.transitionDuration = copyFrom.style.transitionDuration; copyTo.style.transitionProperty = copyFrom.style.transitionProperty; copyTo.style.transitionTimingFunction = copyFrom.style.transitionTimingFunction; copyTo.style.color = copyFrom.style.color; copyTo.style.backgroundColor = copyFrom.style.backgroundColor; copyTo.style.unityBackgroundImageTintColor = copyFrom.style.unityBackgroundImageTintColor; copyTo.style.backgroundImage = copyFrom.style.backgroundImage; copyTo.style.backgroundPositionX = copyFrom.style.backgroundPositionX; copyTo.style.backgroundPositionY = copyFrom.style.backgroundPositionY; copyTo.style.backgroundRepeat = copyFrom.style.backgroundRepeat; copyTo.style.backgroundSize = copyFrom.style.backgroundSize; copyTo.style.opacity = copyFrom.style.opacity; copyTo.style.unityOverflowClipBox = copyFrom.style.unityOverflowClipBox; copyTo.style.minWidth = copyFrom.style.minWidth; copyTo.style.maxWidth = copyFrom.style.maxWidth; copyTo.style.minHeight = copyFrom.style.minHeight; copyTo.style.maxHeight = copyFrom.style.maxHeight; copyTo.style.borderTopColor = copyFrom.style.borderTopColor; copyTo.style.borderBottomColor = copyFrom.style.borderBottomColor; copyTo.style.borderLeftColor = copyFrom.style.borderLeftColor; copyTo.style.borderRightColor = copyFrom.style.borderRightColor; copyTo.style.fontSize = copyFrom.style.fontSize; copyTo.style.unityFont = copyFrom.style.unityFont; copyTo.style.unityFontStyleAndWeight = copyFrom.style.unityFontStyleAndWeight; copyTo.style.unityFontDefinition = copyFrom.style.unityFontDefinition; copyTo.style.unityTextAlign = copyFrom.style.unityTextAlign; copyTo.style.textShadow = copyFrom.style.textShadow; copyTo.style.unityTextOutlineColor = copyFrom.style.unityTextOutlineColor; copyTo.style.unityTextOverflowPosition = copyFrom.style.unityTextOverflowPosition; copyTo.style.textOverflow = copyFrom.style.textOverflow; copyTo.style.unityTextOutlineWidth = copyFrom.style.unityTextOutlineWidth; copyTo.style.wordSpacing = copyFrom.style.wordSpacing; copyTo.style.unityParagraphSpacing = copyFrom.style.unityParagraphSpacing; copyTo.style.whiteSpace = copyFrom.style.whiteSpace; copyTo.style.cursor = copyFrom.style.cursor; copyTo.style.overflow = copyFrom.style.overflow; #if UNITY_6000_0_OR_NEWER copyTo.style.unityTextGenerator = copyFrom.style.unityTextGenerator; copyTo.style.unityEditorTextRenderingMode = copyFrom.style.unityEditorTextRenderingMode; #endif foreach (var @class in copyFrom.GetClasses()) copyTo.AddToClassList(@class); } /// /// Creates a field for a specific type /// /// The type of the field to create /// The name of the field /// The default value of the field /// Whether to show the mixed value state for the field /// A visual element of the appropriate field public static VisualElement CreateFieldForType(string fieldName, object fieldValue, bool showMixedValue = false) => CreateFieldForType(typeof(T), fieldName, fieldValue, showMixedValue); /// /// Creates a field for a specific type /// /// The type of the field to create /// The name of the field /// The default value of the field /// Whether to show the mixed value state for the field /// A visual element of the appropriate field public static VisualElement CreateFieldForType(Type fieldType, string fieldName, object fieldValue, bool showMixedValue = false) { fieldName = ObjectNames.NicifyVariableName(fieldName); if (fieldType == typeof(string)) { return new TextField(fieldName) { value = (string)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType == typeof(char)) { return new TextField(fieldName) { value = fieldValue.ToString(), showMixedValue = showMixedValue, maxLength = 1 }; } else if (fieldType == typeof(int)) { return new IntegerField(fieldName) { value = (int)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType == typeof(uint)) { return new UnsignedIntegerField(fieldName) { value = (uint)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType == typeof(long)) { return new LongField(fieldName) { value = (long)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType == typeof(ulong)) { return new UnsignedLongField(fieldName) { value = (ulong)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType == typeof(float)) { return new FloatField(fieldName) { value = (float)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType == typeof(double)) { return new DoubleField(fieldName) { value = (double)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType == typeof(bool)) { return new Toggle(fieldName) { value = (bool)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType.IsEnum) { return new EnumField(fieldName, (Enum)fieldValue) { showMixedValue = showMixedValue }; } else if (fieldType == typeof(Vector2)) { return new Vector2Field(fieldName) { value = (Vector2)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType == typeof(Vector2Int)) { return new Vector2IntField(fieldName) { value = (Vector2Int)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType == typeof(Vector3)) { return new Vector3Field(fieldName) { value = (Vector3)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType == typeof(Vector3Int)) { return new Vector3IntField(fieldName) { value = (Vector3Int)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType == typeof(Vector4)) { return new Vector4Field(fieldName) { value = (Vector4)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType == typeof(Color)) { return new ColorField(fieldName) { value = (Color)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType == typeof(Gradient)) { return new GradientField(fieldName) { value = (Gradient)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType == typeof(AnimationCurve)) { return new CurveField(fieldName) { value = (AnimationCurve)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType == typeof(LayerMask)) { return new LayerMaskField(fieldName, (LayerMask)fieldValue) { showMixedValue = showMixedValue }; } else if (fieldType == typeof(Rect)) { return new RectField(fieldName) { value = (Rect)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType == typeof(RectInt)) { return new RectIntField(fieldName) { value = (RectInt)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType == typeof(Bounds)) { return new BoundsField(fieldName) { value = (Bounds)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType == typeof(BoundsInt)) { return new BoundsIntField(fieldName) { value = (BoundsInt)fieldValue, showMixedValue = showMixedValue }; } else if (fieldType.IsSerializable && !ReflectionUtility.IsTypeCollection(fieldType) && !fieldType.IsPrimitive) { var serializedObjectFoldout = new Foldout { text = fieldName }; var nestedFields = fieldType.GetFields(); foreach (var field in nestedFields) { var createdField = CreateFieldForType(field.FieldType, field.Name, field.GetValue(fieldValue)); createdField.AddToClassList(BaseField.alignedFieldUssClassName); serializedObjectFoldout.Add(createdField); } return serializedObjectFoldout; } else { return new HelpBox($"The type {fieldType} is not supported", HelpBoxMessageType.Error); } } /// /// Registers a value changed callback for field of a specific type. /// /// The type of the value /// The visual element of the field /// The callback action /// The value of the registered serialized object. This parameter is only required if you need to register value callbacks to serialized objects public static void RegisterValueChangedCallbackByType(VisualElement field, Action valueCallback, object objectValue = null) => RegisterValueChangedCallbackByType(typeof(T), field, valueCallback, objectValue); /// /// Registers a value changed callback for field of a specific type. /// /// The type of the value /// The visual element of the field /// The callback action /// The value of the registered serialized object. This parameter is only required if you need to register value callbacks to serialized objects public static void RegisterValueChangedCallbackByType(Type fieldType, VisualElement field, Action valueCallback, object objectValue = null) { if (fieldType == typeof(string) || fieldType == typeof(char)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(int)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(uint)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(long)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(ulong)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(float)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(double)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(bool)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType.IsEnum) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(Vector2)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(Vector2Int)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(Vector3)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(Vector3Int)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(Vector4)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(Color)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(Gradient)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(AnimationCurve)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(LayerMask)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(Rect)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(RectInt)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(Bounds)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType == typeof(BoundsInt)) { field.RegisterCallback>((callback) => valueCallback.Invoke(callback.newValue)); } else if (fieldType.IsSerializable && !ReflectionUtility.IsTypeCollection(fieldType) && !fieldType.IsPrimitive) { if (objectValue == null) { Debug.LogError("You are attempting to register a value on a custom serialized object but the objectValue parameter is not assigned"); return; } var nestedFields = fieldType.GetFields(); foreach (var nestedField in nestedFields) { RegisterValueChangedCallbackByType(nestedField.FieldType, field, (value) => { nestedField.SetValue(objectValue, value); valueCallback.Invoke(objectValue); }); } } } /// /// Gets the label of the appropriate field /// /// The visual element of the field /// The field label public static string GetFieldLabel(VisualElement field) => field switch { TextField textField => textField.label, IntegerField integerField => integerField.label, UnsignedIntegerField unsignedIntegerField => unsignedIntegerField.label, LongField longField => longField.label, UnsignedLongField unsignedLongField => unsignedLongField.label, FloatField floatField => floatField.label, DoubleField doubleField => doubleField.label, Toggle toggle => toggle.label, EnumField enumField => enumField.label, Vector2Field vector2Field => vector2Field.label, Vector2IntField vector2IntField => vector2IntField.label, Vector3Field vector3Field => vector3Field.label, Vector3IntField vector3IntField => vector3IntField.label, Vector4Field vector4Field => vector4Field.label, ColorField colorField => colorField.label, GradientField gradientField => gradientField.label, CurveField curveField => curveField.label, LayerMaskField layerMaskField => layerMaskField.label, RectField rectField => rectField.label, RectIntField rectIntField => rectIntField.label, BoundsField boundsField => boundsField.label, BoundsIntField boundsIntField => boundsIntField.label, _ => null, }; /// /// Gets the value of the appropriate field /// /// The visual element of the field /// The field value public static object GetFieldValue(VisualElement field) => field switch { TextField textField => textField.value, IntegerField integerField => integerField.value, UnsignedIntegerField unsignedIntegerField => unsignedIntegerField.value, LongField longField => longField.value, UnsignedLongField unsignedLongField => unsignedLongField.value, FloatField floatField => floatField.value, DoubleField doubleField => doubleField.value, Toggle toggle => toggle.value, EnumField enumField => enumField.value, Vector2Field vector2Field => vector2Field.value, Vector2IntField vector2IntField => vector2IntField.value, Vector3Field vector3Field => vector3Field.value, Vector3IntField vector3IntField => vector3IntField.value, Vector4Field vector4Field => vector4Field.value, ColorField colorField => colorField.value, GradientField gradientField => gradientField.value, CurveField curveField => curveField.value, LayerMaskField layerMaskField => layerMaskField.value, RectField rectField => rectField.value, RectIntField rectIntField => rectIntField.value, BoundsField boundsField => boundsField.value, BoundsIntField boundsIntField => boundsIntField.value, _ => null, }; /// /// Sets the value of the appropriate field /// /// The visual element of the field /// The value to set /// Whether to call the value change callback when setting the value public static void SetFieldValue(VisualElement field, object value, bool notify = false) { if (field is TextField textField) { if (notify) { textField.value = value.ToString(); } else { textField.SetValueWithoutNotify(value.ToString()); } } else if (field is IntegerField integerField) { if (notify) { integerField.value = (int)value; } else { integerField.SetValueWithoutNotify((int)value); } } else if (field is UnsignedIntegerField unsignedIntegerField) { if (notify) { unsignedIntegerField.value = (uint)value; } else { unsignedIntegerField.SetValueWithoutNotify((uint)value); } } else if (field is LongField longField) { if (notify) { longField.value = (long)value; } else { longField.SetValueWithoutNotify((long)value); } } else if (field is UnsignedLongField unsignedLongField) { if (notify) { unsignedLongField.value = (ulong)value; } else { unsignedLongField.SetValueWithoutNotify((ulong)value); } } else if (field is FloatField floatField) { if (notify) { floatField.value = (float)value; } else { floatField.SetValueWithoutNotify((float)value); } } else if (field is DoubleField doubleField) { if (notify) { doubleField.value = (double)value; } else { doubleField.SetValueWithoutNotify((double)value); } } else if (field is Toggle toggle) { if (notify) { toggle.value = (bool)value; } else { toggle.SetValueWithoutNotify((bool)value); } } else if (field is EnumField enumField) { if (notify) { enumField.value = (Enum)value; } else { enumField.SetValueWithoutNotify((Enum)value); } } else if (field is Vector2Field vector2Field) { if (notify) { vector2Field.value = (Vector2)value; } else { vector2Field.SetValueWithoutNotify((Vector2)value); } } else if (field is Vector2IntField vector2IntField) { if (notify) { vector2IntField.value = (Vector2Int)value; } else { vector2IntField.SetValueWithoutNotify((Vector2Int)value); } } else if (field is Vector3Field vector3Field) { if (notify) { vector3Field.value = (Vector3)value; } else { vector3Field.SetValueWithoutNotify((Vector3)value); } } else if (field is Vector3IntField vector3IntField) { if (notify) { vector3IntField.value = (Vector3Int)value; } else { vector3IntField.SetValueWithoutNotify((Vector3Int)value); } } else if (field is Vector4Field vector4Field) { if (notify) { vector4Field.value = (Vector4)value; } else { vector4Field.SetValueWithoutNotify((Vector4)value); } } else if (field is ColorField colorField) { if (notify) { colorField.value = (Color)value; } else { colorField.SetValueWithoutNotify((Color)value); } } else if (field is GradientField gradientField) { if (notify) { gradientField.value = (Gradient)value; } else { gradientField.SetValueWithoutNotify((Gradient)value); } } else if (field is CurveField curveField) { if (notify) { curveField.value = (AnimationCurve)value; } else { curveField.SetValueWithoutNotify((AnimationCurve)value); } } else if (field is LayerMaskField layerMaskField) { if (notify) { layerMaskField.value = (LayerMask)value; } else { layerMaskField.SetValueWithoutNotify((LayerMask)value); } } else if (field is RectField rectField) { if (notify) { rectField.value = (Rect)value; } else { rectField.SetValueWithoutNotify((Rect)value); } } else if (field is RectIntField rectIntField) { if (notify) { rectIntField.value = (RectInt)value; } else { rectIntField.SetValueWithoutNotify((RectInt)value); } } else if (field is BoundsField boundsField) { if (notify) { boundsField.value = (Bounds)value; } else { boundsField.SetValueWithoutNotify((Bounds)value); } } else if (field is BoundsIntField boundsIntField) { if (notify) { boundsIntField.value = (BoundsInt)value; } else { boundsIntField.SetValueWithoutNotify((BoundsInt)value); } } } /// /// Bind a field to the target member value /// /// The type of the field /// The field visual element /// The member to bind /// The target object of the member public static void BindFieldToMember(VisualElement field, MemberInfo memberInfo, object targetObject) => BindFieldToMember(typeof(T), field, memberInfo, targetObject); /// /// Bind a field to the target member value /// /// The type of the field /// The field visual element /// The member to bind /// The target object of the member public static void BindFieldToMember(Type fieldType, VisualElement field, MemberInfo memberInfo, object targetObject) { UpdateVisualElement(field, () => { var memberValue = ReflectionUtility.GetMemberInfoValue(memberInfo, targetObject); if (IsTypeValid(fieldType)) { SetFieldValue(field, memberValue); } else if (!fieldType.IsPrimitive && fieldType.IsSerializable && !ReflectionUtility.IsTypeCollection(fieldType)) { var childFields = field.contentContainer.Children().ToArray(); var nestedFields = fieldType.GetFields(); for (int i = 0; i < nestedFields.Length; i++) { var nestedField = nestedFields[i]; SetFieldValue(childFields[i], nestedField.GetValue(memberValue)); } } else { Debug.LogError($"Cannot bind to the field to {fieldType}, this type is not supported", (Object)targetObject); } }); static bool IsTypeValid(Type type) => type.IsEnum || type == typeof(string) || type == typeof(char) || type == typeof(int) || type == typeof(uint) || type == typeof(long) || type == typeof(ulong) || type == typeof(float) || type == typeof(double) || type == typeof(bool) || type == typeof(Vector2) || type == typeof(Vector2Int) || type == typeof(Vector3) || type == typeof(Vector3Int) || type == typeof(Vector4) || type == typeof(Color) || type == typeof(LayerMask) || type == typeof(Rect) || type == typeof(RectInt) || type == typeof(Bounds) || type == typeof(BoundsInt) || type == typeof(Gradient) || type == typeof(AnimationCurve); } #region NON_GUI_RELATED_UTILITY_FUNCITONS /// /// A short handy version of Debug.Log /// /// The message to print protected static void Print(object message) => Debug.Log(message); /// /// Checks if a collection is null or has no members /// /// The collection to check /// False is the collection is null or has no members, true otherwise public static bool IsCollectionValid(ICollection collection) => collection != null && collection.Count != 0; /// /// Gets the size of a 2D texture /// /// The texture to get the size from /// The width and height of the texture as a Vector2 public static Vector2 GetTextureSize(Texture2D texture) => new(texture.width, texture.height); #endregion } }