using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Rive.Utils; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; namespace Rive.EditorTools { #if UNITY_EDITOR /// /// Base class for custom inspectors for Rive components. /// internal class RiveBaseEditor : Editor { protected VisualElement rootElement; private GameObject m_gameObject; private Dictionary sections = new Dictionary(); protected virtual void OnEnable() { if (target is MonoBehaviour) { m_gameObject = (target as MonoBehaviour).gameObject; } // Using Editor.update, queue a repaint for the next frame to hide components // Hiding immediately causes issues with the inspector layout EditorApplication.update += FirstRepaint; } private void FirstRepaint() { HandleHideComponents(); EditorApplication.update -= FirstRepaint; } private void HandleHideComponents() { var hideComponentsAttrs = target.GetType().GetCustomAttributes(); var targetComponent = target as MonoBehaviour; if (targetComponent != null) { foreach (var attr in hideComponentsAttrs) { CustomInspectorUtils.HideNonInteractiveComponents( targetComponent, new List(attr.ComponentTypes), this, attr.HideFlags ); } } } public override VisualElement CreateInspectorGUI() { rootElement = new VisualElement(); rootElement.styleSheets.Add(StyleHelper.StyleSheet); rootElement.AddToClassList("rive-inspector"); var serializedFields = GetSerializedFields(); // Get all fields with InspectorFieldAttribute var attributeFields = serializedFields .Where(f => f.GetCustomAttribute() != null) .OrderBy(f => f.GetCustomAttribute().Order); // Split into fields with and without sections var sectionFields = attributeFields.Where(f => !string.IsNullOrEmpty(f.GetCustomAttribute().SectionId)); var nonSectionFields = attributeFields.Where(f => string.IsNullOrEmpty(f.GetCustomAttribute().SectionId)); // Get fields without any attributes var plainFields = serializedFields .Except(attributeFields); // Process non-sectioned fields first (both plain and attributed) foreach (var field in plainFields.Concat(nonSectionFields)) { var attr = field.GetCustomAttribute(); CreateFieldElement(field, attr, rootElement); } // Get sections that have fields var usedSectionIds = sectionFields .Select(f => f.GetCustomAttribute().SectionId) .Distinct() .ToHashSet(); CreateSections(usedSectionIds); foreach (var field in sectionFields) { var attr = field.GetCustomAttribute(); var container = sections[attr.SectionId]; CreateFieldElement(field, attr, container); } return rootElement; } private HashSet GetSerializedFields() { var fields = new HashSet(); var currentType = target.GetType(); // Walk up the inheritance chain until we hit MonoBehaviour while (currentType != typeof(MonoBehaviour) && currentType != null) { var typeFields = currentType .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly) .Where(f => (f.GetCustomAttribute() != null || f.IsPublic) && f.GetCustomAttribute() == null && IsUnitySerializable(f.FieldType)); // check for Unity-serializable types foreach (var field in typeFields) { fields.Add(field); } currentType = currentType.BaseType; } return fields; } // Helper method to check if a type is serializable by Unity. We do this to avoid types like Actions, Funcs, etc not showing up in the inspector but still taking up space. private bool IsUnitySerializable(Type type) { if (type == null) return false; if (Attribute.IsDefined(type, typeof(SerializableAttribute))) return true; if (type.IsPrimitive || type == typeof(string) || type == typeof(decimal)) return true; if (typeof(UnityEngine.Object).IsAssignableFrom(type)) return true; if (type.IsEnum) return true; if (type.IsValueType && !type.IsPrimitive) return true; if (typeof(UnityEngine.Events.UnityEventBase).IsAssignableFrom(type)) return true; if (type.IsArray) return IsUnitySerializable(type.GetElementType()); if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) return IsUnitySerializable(type.GetGenericArguments()[0]); return false; } private void CreateSections(HashSet usedSectionIds) { var sectionAttrs = target.GetType() .GetCustomAttributes() .OrderBy(s => s.Order); foreach (var attr in sectionAttrs) { // Only create sections that have fields if (!usedSectionIds.Contains(attr.Id)) { continue; } VisualElement section; switch (attr.Style) { case SectionStyle.Foldout: var foldout = new Foldout { text = attr.DisplayName }; foldout.viewDataKey = $"RiveFoldout_{target.GetType().Name}_{attr.Id}"; // The initial value will be used only if there's no saved state foldout.value = attr.StartExpanded; section = foldout; break; case SectionStyle.Header: default: section = new VisualElement(); if (!string.IsNullOrEmpty(attr.DisplayName)) { var label = new Label(attr.DisplayName); label.AddToClassList(StyleHelper.CLASS_SECTION_LABEL); section.Add(label); } break; } section.AddToClassList(StyleHelper.CLASS_SECTION); sections[attr.Id] = section; rootElement.Add(section); } } public static VisualElement GetVisualElementForField(FieldInfo field, SerializedProperty property, string label = null) { VisualElement element; // We do this to show the alignment dropdown because some versions of Unity seems to have issues with the default PropertyDrawer (e.g Unity 2022.3.10) // In those versions, the default PropertyDrawer doesn't show the dropdown, but rather the X and Y fields. // It's possible that this is a bug in Unity, but this is a workaround for now. if (field.FieldType == typeof(Alignment)) { var alignmentDrawer = new AlignmentPropertyDrawer(); element = alignmentDrawer.CreatePropertyGUI(property); } else { var propertyField = new PropertyField { bindingPath = field.Name }; if (label != null) { propertyField.label = label; } element = propertyField; } return element; } private void CreateFieldElement(FieldInfo field, InspectorFieldAttribute attr, VisualElement container) { string displayName = attr?.DisplayName ?? ObjectNames.NicifyVariableName(field.Name); var property = serializedObject.FindProperty(field.Name); VisualElement element = GetVisualElementForField(field, property, displayName); string uniqueId = $"field-{target.GetInstanceID()}-{target.GetType().Name}-{field.Name}"; element.name = uniqueId; HandleValueChangedIfNeeded(field, element); VisualElement fieldRoot = element; if (attr != null && attr.HasHelpUrl) { var fieldContainer = new VisualElement(); fieldContainer.AddToClassList(StyleHelper.CLASS_FIELD_CONTAINER); fieldContainer.style.flexDirection = FlexDirection.Row; fieldContainer.style.alignItems = Align.Center; element.AddToClassList(StyleHelper.CLASS_FIELD_CONTENT); fieldContainer.Add(element); fieldContainer.Add(CreateHelpButton(attr.HelpUrl, displayName)); fieldRoot = fieldContainer; } HandleConditionalVisibilityIfNeeded(field, fieldRoot, property); element.Bind(serializedObject); fieldRoot.AddToClassList(StyleHelper.CLASS_FIELD); container.Add(fieldRoot); } private void HandleValueChangedIfNeeded(FieldInfo field, VisualElement element) { var onValueChangedAttr = field.GetCustomAttribute(); if (onValueChangedAttr != null) { var methodInfo = target.GetType().GetMethod(onValueChangedAttr.CallbackName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (methodInfo != null) { bool isInitializing = true; element.RegisterCallback(evt => { element.schedule.Execute(() => { isInitializing = false; }); }); if (element is PropertyField propertyField) { propertyField.RegisterValueChangeCallback(evt => { if (!isInitializing || onValueChangedAttr.InvokeOnInitialization) { methodInfo.Invoke(target, null); } }); return; } // The alignment dropdown is a PopupField // Get the PopupField from the element. It's possible that the passed in element might be a container so we might need to find the PopupField in the children. var popupField = element.Q>(); if (element != null) { popupField.RegisterValueChangedCallback(evt => { if (!isInitializing || onValueChangedAttr.InvokeOnInitialization) { methodInfo.Invoke(target, null); } }); } } } } private void HandleConditionalVisibilityIfNeeded(FieldInfo field, VisualElement element, SerializedProperty property) { var showIfAttr = field.GetCustomAttribute(); var hideIfAttr = field.GetCustomAttribute(); if (showIfAttr == null && hideIfAttr == null) return; string conditionName = showIfAttr?.ConditionName ?? hideIfAttr?.ConditionName; bool isHideIf = hideIfAttr != null; void UpdateVisibility() { if (property.serializedObject == null || property.serializedObject.targetObject == null) { return; } var target = property.serializedObject.targetObject; if (ReflectionUtils.TryGetBoolValue(target, conditionName, out bool condition)) { element.style.display = (condition != isHideIf) ? DisplayStyle.Flex : DisplayStyle.None; } } element.RegisterCallback(evt => { property.serializedObject.Update(); UpdateVisibility(); }); UpdateVisibility(); // Update visibility whenever the inspector updates scheduledUpdate = element.schedule.Execute(() => { if (property.serializedObject == null || property.serializedObject.targetObject == null) { // Stop scheduling future updates scheduledUpdate?.Pause(); return; } property.serializedObject.Update(); UpdateVisibility(); }).Every(100); } private IVisualElementScheduledItem scheduledUpdate; private Button CreateHelpButton(string helpUrl, string displayName) { var button = new Button(() => { if (!string.IsNullOrEmpty(helpUrl)) { Application.OpenURL(helpUrl); } }); button.tooltip = "Open documentation for this field"; button.focusable = false; button.AddToClassList(StyleHelper.CLASS_FIELD_HELP_BUTTON); var iconContent = EditorGUIUtility.IconContent("_Help"); if (iconContent?.image != null) { var icon = new Image { image = iconContent.image, scaleMode = ScaleMode.ScaleToFit }; button.Add(icon); } else { button.text = "?"; } return button; } private void OnDestroy() { if (Application.isPlaying) return; bool componentRemoved = m_gameObject != null && m_gameObject.GetComponent(target.GetType()) == null; //If the component was removed but not the gameobject, let's destroy the required components it added that are hidden // If they're not hidden, then the user can remove them manually. if (componentRemoved) { var hideComponentsAttrs = target.GetType().GetCustomAttributes(); foreach (var attr in hideComponentsAttrs) { CustomInspectorUtils.DestroyRequiredHiddenComponents( m_gameObject, target.GetType(), component => (component.hideFlags & attr.HideFlags) != 0 ); } } } } #endif }