How to make a custom transform inspector with UI elements?

Position and Scale are working as expected - but Rotation … I have no idea how to do this.

using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements;

namespace JL
{
    [CustomEditor(typeof(Transform))]
    [CanEditMultipleObjects]
    public class TransformInspector : Editor
    {
        public override VisualElement CreateInspectorGUI()
        {
            VisualElement root = new VisualElement();

            Transform transform = target as Transform;
            SerializedObject serializedObject = new SerializedObject(transform);


            SerializedProperty posProp = serializedObject.FindProperty("m_LocalPosition");
            SerializedProperty rotProp = serializedObject.FindProperty("m_LocalRotation");
            SerializedProperty scaleProp = serializedObject.FindProperty("m_LocalScale");

            Vector3Field position = new Vector3Field("Position");
            position.BindProperty(posProp);
            root.Add(position);

            //Vector3Field rotation = new Vector3Field("Rotation");
            //rotation.BindProperty(rotProp);
            //root.Add(rotation);

            //TransformUtils.SetInspectorRotation(transform, rotProp.vector3Value);
            //Vector3 eulerRot = TransformUtils.GetInspectorRotation(transform);

            //Quaternion quaternionRot = rotProp.quaternionValue;

            Vector3Field scale = new Vector3Field("Scale");
            scale.BindProperty(scaleProp);
            root.Add(scale);

            return root;
        }

    }
}

The big problem is, I don’t get an event when the User rotates the Object with the RotationGizmo - there is no “OnValueChanged”-Event in a SerializedProperty or SerializedObject - how would I know this happend without an event?

Is there a way to modify the binding, so it converts from Vector3 to Quaternion back and forth?

How to solve this? Has anyone made a custom Transform Inspector with UI Elements? I found nothing like this on the forums.

Ok that was actually easy xD
No idea why I struggled for two hours, just to find the solution 5min after asking the forum …

using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements;

namespace JL
{
    [CustomEditor(typeof(Transform))]
    [CanEditMultipleObjects]
    public class TransformInspector : Editor
    {
        Transform transform;
        Vector3Field rotationField;

        public override VisualElement CreateInspectorGUI()
        {
            VisualElement root = new VisualElement();

            transform = target as Transform;
            SerializedObject serializedObject = new SerializedObject(transform);


            SerializedProperty posProp = serializedObject.FindProperty("m_LocalPosition");
            SerializedProperty rotProp = serializedObject.FindProperty("m_LocalRotation");
            SerializedProperty scaleProp = serializedObject.FindProperty("m_LocalScale");

            Vector3Field position = new Vector3Field("Position");
            position.BindProperty(posProp);
            root.Add(position);

            rotationField = new Vector3Field("Rotation");
            //rotationField.BindProperty(rotProp);
            root.Add(rotationField);

            rotationField.value = TransformUtils.GetInspectorRotation(transform);
            rotationField.RegisterValueChangedCallback(eventData =>
            {
                Undo.RecordObject(transform, "ChangeRotation");
                TransformUtils.SetInspectorRotation(transform, eventData.newValue);
            });

            Vector3Field scale = new Vector3Field("Scale");
            scale.BindProperty(scaleProp);
            root.Add(scale);

            return root;
        }


        private void OnSceneGUI()
        {
            rotationField.SetValueWithoutNotify(TransformUtils.GetInspectorRotation(transform));
        }
    }
}

Ok actually it was way, way more complicated than that thanks to multi object editing.
[CanEditMultipleObjects]

using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements;

namespace JL
{
    [CustomEditor(typeof(Transform))]
    [CanEditMultipleObjects]
    public class CustomTransformInspector : Editor
    {
        Vector3Field positionField;
        Vector3Field rotationField;
        Vector3Field scaleField;

        struct MixedHelper
        {
            public bool x;
            public bool y;
            public bool z;
        }

        public override VisualElement CreateInspectorGUI()
        {
            VisualElement root = new VisualElement();

            positionField = new Vector3Field("Position");
            root.Add(positionField);
            positionField.RegisterValueChangedCallback(eventData =>
            {
                foreach (GameObject selected in Selection.gameObjects)
                {
                    Undo.RecordObject(selected.transform, "ChangePosition");
                    selected.transform.localPosition = eventData.newValue;
                }
            });

            rotationField = new Vector3Field("Rotation");
            root.Add(rotationField);
            rotationField.RegisterValueChangedCallback(eventData =>
            {
                foreach (GameObject selected in Selection.gameObjects)
                {
                    Undo.RecordObject(selected.transform, "ChangeRotation");
                    TransformUtils.SetInspectorRotation(selected.transform, eventData.newValue);
                }
            });

            scaleField = new Vector3Field("Scale");
            root.Add(scaleField);
            scaleField.RegisterValueChangedCallback(eventData =>
            {
                foreach (GameObject selected in Selection.gameObjects)
                {
                    Undo.RecordObject(selected.transform, "ChangeScale");
                    selected.transform.localScale = eventData.newValue;
                }
            });

            OnSceneGUI();

            Label label = new Label("It's working");
            root.Add(label);

            return root;
        }


        private void OnSceneGUI()
        {
            Vector3 displayPos;
            Vector3 displayRot;
            Vector3 displayScale;

            if (!Selection.activeGameObject || !Selection.activeGameObject.transform) return;

            Transform mainT = Selection.activeGameObject.transform;
            displayPos = mainT.localPosition;
            displayRot = TransformUtils.GetInspectorRotation(mainT);
            displayScale = mainT.localScale;

            MixedHelper posMixed = new MixedHelper();
            MixedHelper rotMixed = new MixedHelper();
            MixedHelper scaleMixed = new MixedHelper();

            foreach (GameObject selected in Selection.gameObjects)
            {
                Transform t = selected.transform;

                if (displayPos.x != t.localPosition.x) posMixed.x = true;
                if (displayPos.y != t.localPosition.y) posMixed.y = true;
                if (displayPos.z != t.localPosition.z) posMixed.z = true;

                Vector3 rotation = TransformUtils.GetInspectorRotation(t);

                if (displayRot.x != rotation.x) rotMixed.x = true;
                if (displayRot.y != rotation.y) rotMixed.y = true;
                if (displayRot.z != rotation.z) rotMixed.z = true;

                if (displayScale.x != t.localScale.x) scaleMixed.x = true;
                if (displayScale.y != t.localScale.y) scaleMixed.y = true;
                if (displayScale.z != t.localScale.z) scaleMixed.z = true;
            }

            positionField.SetValueWithoutNotify(displayPos);
            rotationField.SetValueWithoutNotify(displayRot);
            scaleField.SetValueWithoutNotify(displayScale);

            XYZisMixed(positionField, posMixed);
            XYZisMixed(rotationField, rotMixed);
            XYZisMixed(scaleField, scaleMixed);
        }

        void XYZisMixed(Vector3Field vector3Field, MixedHelper mixed)
        {
            ShowMixed(vector3Field, "x", mixed.x);
            ShowMixed(vector3Field, "y", mixed.y);
            ShowMixed(vector3Field, "z", mixed.z);
        }
        void ShowMixed(Vector3Field vector3Field, string axis, bool mixed)
        {
            TextInputBaseField<float> inputText = vector3Field.Q<FloatField>($"unity-{axis}-input")
                                .Q<TextInputBaseField<float>>();
            inputText.showMixedValue = mixed;
        }
    }
}

This is fantastic

1 Like

Just a quick Note: I changed “OnSceneGUI” to “Update”, using “EditorApplication.update” like so:

private void OnEnable()
{
    EditorApplication.update -= Update;
    EditorApplication.update += Update;
}
private void OnDisable()
{
    EditorApplication.update -= Update;
}

private void Update()
{
    // not initialized
    if (localPositionField == null) return;

    GetPosRotScaleValues(); // original OnSceneGUI() method
}

because “OnSceneGUI” is only executed when Gizmos are turned on and we want the inspector to be independent of Gizmos rendering.

2 Likes