arimger/Unity-Editor-Toolbox

[Feature Request] Draw Toolbox drawers on Custom Editors

Closed this issue · 12 comments

I am currently extending a base class (Unity made) that has a custom editor.
In order to my class to display the base editor, I created a custom editor for it that derives from the Unity's one.
Now that I have the custom base editor working and the default drawers for my new fields (without defining them in the new custom editor), the Toolbox drawers are now being rendered.

Can this be made possible? I can't change the base classe for Unity code to derive from Toolbox.Editor.ToolboxEditor

Hello,

you can use IToolboxEditor interface and handle it like in the ToolboxScriptedImporterEditor example:
https://github.com/arimger/Unity-Editor-Toolbox/blob/master/Assets/Editor%20Toolbox/Editor/Editors/ToolboxScriptedImporterEditor.cs

Tried with this:

using UnityEditor;
using UnityEditor.XR.Interaction.Toolkit;
using Toolbox.Editor;

namespace MyNamespace.OpenXR.Teleport
{
    [CustomEditor(typeof(TeleportInteractableDriver), true), CanEditMultipleObjects]
    public class TeleportInteractableDriverEditor : BaseTeleportationInteractableEditor, IToolboxEditor
    {
        public Editor ContextEditor => this;

        public IToolboxEditorDrawer Drawer { get; } = new ToolboxEditorDrawer();

        public sealed override void OnInspectorGUI()
        {
            // /!\ Requires ToolboxEditorHandler to be public (which is not) and 
            // shows the Toolbox drawers properly on the new fields
            // but the base custom editor is not drawn.
            ToolboxEditorHandler.HandleToolboxEditor(this);

            // OR

            // Draws the base custom editor properly
            // Does not show the Toolbox drawers on the new fields
            base.OnInspectorGUI();
        }

        public void DrawCustomInspector()
        {
            Drawer.DrawEditor(serializedObject);
        }

        public void IgnoreProperty(SerializedProperty property)
        {
            Drawer.IgnoreProperty(property);
        }

        public void IgnoreProperty(string propertyPath)
        {
            Drawer.IgnoreProperty(propertyPath);
        }
    }
}

Any suggestion?

I had plans to make ToolboxEditorHandler public, I can do this in the next update. For now, you can use reflection to retrieve the appropriate method.
Regarding drawing issues:

  • You can implement a custom IToolboxEditorDrawer that handles drawing for you, e.g. draws only new fields
  • You can ignore base class properties using the IgnoreProperty methods or any custom solution implemented in your IToolboxEditorDrawer
  • Depending on the base class implementation you can use base.OnInspectorGUI to draw the base Editor and then all new properties using Toolbox's API. If the base class still draws all properties (e.g. iterates over the SerializedObject) then it requires to somehow ignore the "new ones".
  • The best solution I can see is to not use EditorGUILayout.PropertyField but ToolboxEditorGui.DrawToolboxProperty but it can be used only within ToolboxEditors. Since you can't override the base Editor class and using IToolboxEditor & ToolboxEditorHandler.HandleToolboxEditor(this) is not really convenient I can implement utility methods like BeginToolboxEditor/CloseToolboxEditor or a dedicated scope-like class.

Example:

base.OnInspectorGUI();
using (new ToolboxEditorScope(myEditor))
{
   ToolboxEditorGui.DrawToolboxProperty(myProperty);
}

Do I understand your problem correctly and the proposed solution would help?

My particular use case is that I am extending the BaseTeleportationInteractable from Unity's XR Interaction Toolkit. That has a whole hierarchy of derived classes with custom editors made by Unity.
In my new class TeleportInteractableDriver I want to extend the base functionality and I added new fields with Toolbox attributes.
In order for Unity's custom editors to show, I had to create a custom editor for my class that derives from their BaseTeleportationInteractableEditor. Which solved this problem, and automatically draws my new fields and properly renders the Unity attributes (Header, Space, etc).
If I use a Toolbox attribute it is not taking effect. Now, if I implement IToolboxEditorDrawer I can get the Toolbox attributes to draw properly, but I lose the custom editor from the base class.
Keep in mind, that I did not want to make a custom editor for my class, I just want the base custom editor plus all my new fields supporting the Toolbox attributes. I was trying to avoid that scenario of needing to ignore all previously drawn fields or implementing property fields for all new fields. It should be somewhat automatic.

I analyzed for you how BaseTeleportationInteractableEditor works. In your custom base Editor you need to override DrawDerivedProperties method. As far as I know, this method is used to draw all properties that are not defined (and handled internally) in all Editor classes. Basically, it draws all your custom fields. Internally it utilizes Editor.DrawPropertiesExcluding but you cannot use it if you want to show Toolbox-based attributes.
What you need to do:

  • Use knownSerializedPropertyNames list (protected property) to ignore these properties in the IToolboxEditorDrawer
  • Override DrawDerivedProperties
  • Should work, if not probably some minor tweaks are needed
protected override void DrawDerivedProperties()
{
   //original line
   //DrawPropertiesExcluding(serializedObject, knownSerializedPropertyNames);

   //this can be done once in the OnEnable callback or you can override InitializeKnownSerializedPropertyNames
   foreach (var propertyName in knownSerializedPropertyNames)
   {
       IgnoreProperty(propertyName);
   }

   ToolboxEditorHandler.HandleToolboxEditor(this);
}

Thank you, that worked perfectly (as long as you make ToolboxEditorHandler public).
Maybe this could be done better/automatically in the future?

If anyone comes across this, here's the final code:

[CustomEditor(typeof(TeleportInteractableDriver), true), CanEditMultipleObjects]
    public class TeleportInteractableDriverEditor : BaseTeleportationInteractableEditor, IToolboxEditor
    {
        public Editor ContextEditor => this;

        public IToolboxEditorDrawer Drawer { get; } = new ToolboxEditorDrawer();

        protected override void DrawDerivedProperties(){
            foreach(var propertyName in knownSerializedPropertyNames){
                IgnoreProperty(propertyName);
            }

            ToolboxEditorHandler.HandleToolboxEditor(this);    
        }

        public void DrawCustomInspector()
        {
            Drawer.DrawEditor(serializedObject);
        }

        public void IgnoreProperty(SerializedProperty property)
        {
            Drawer.IgnoreProperty(property);
        }

        public void IgnoreProperty(string propertyPath)
        {
            Drawer.IgnoreProperty(propertyPath);
        }
    }

I would also like to have ToolboxEditorHandler to be public, to be able to integrate it with my custom editor.

Hello,
@imniko, @p-monteiro ToolboxEditorHandler is now public.

Sorry to dig this again, but I found another problem. Now everything is drawn properly, but the fields/properties from the base editors cannot be changed. They stay always with the default values.

I tried to change DrawCustomInspector to the following without success:

public void DrawCustomInspector()
{
    serializedObject.Update();

    Drawer.DrawEditor(serializedObject);

    serializedObject.ApplyModifiedProperties();
}

Do you have any pointers @arimger ?

Hello! @p-monteiro it's the same case? I mean the issue occurs while extending the BaseTeleportationInteractableEditor class? I will try to look at this tomorrow.

EDIT.
I double-checked this logic with custom Editors and everything works fine. Methods serializedObject.Update() and serializedObject.ApplyModifiedProperties() are used internally by the ToolboxEditorDrawer so there is no need to use them in your example. You can try to add them in the OnInspectorGUI override (or override void DrawProperties() if we are talking about the same Editor classes).

[CustomEditor(typeof(SampleInheritedBehaviour))]
public class SampleInheritedBehaviourEditor : SampleStandardEditor, IToolboxEditor
{
    public Editor ContextEditor => this;
    public IToolboxEditorDrawer Drawer => new ToolboxEditorDrawer();

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();
        ToolboxEditorHandler.HandleToolboxEditor(this);
    }

    public void DrawCustomInspector()
    {
        EditorGUILayout.LabelField("SampleLabel");
        Drawer.DrawToolboxEditor(serializedObject);
    }

    public void IgnoreProperty(SerializedProperty property)
    {
        Drawer.IgnoreProperty(property);
    }

    public void IgnoreProperty(string propertyPath)
    {
        Drawer.IgnoreProperty(propertyPath);
    }
}

Sorry the late answer, but yes, same case.

So what you mention in the EDIT does not work. It does not render ToolboxEditor Attributes with this:

public override void OnInspectorGUI()
{
    base.OnInspectorGUI();
    ToolboxEditorHandler.HandleToolboxEditor(this);
}

What you found and told me previously, and works, was to do this:

protected override void DrawDerivedProperties()
{
    foreach (var propertyName in knownSerializedPropertyNames)
    {
        IgnoreProperty(propertyName);
    }

    ToolboxEditorHandler.HandleToolboxEditor(this);
}

However, there are certain properties from the base editor that can't be changed in the Editor as they stay with the same value always.

Complete script:

[CustomEditor(typeof(TeleportInteractableDriver), true), CanEditMultipleObjects]
public class TeleportInteractableDriverEditor : BaseTeleportationInteractableEditor, IToolboxEditor
{
    public Editor ContextEditor => this;

    public IToolboxEditorDrawer Drawer { get; } = new ToolboxEditorDrawer();

    protected override void DrawDerivedProperties()
    {
        foreach (var propertyName in knownSerializedPropertyNames)
        {
            IgnoreProperty(propertyName);
        }

        ToolboxEditorHandler.HandleToolboxEditor(this);
    }

    public void DrawCustomInspector()
    {
        Drawer.DrawEditor(serializedObject);
    }

    public void IgnoreProperty(SerializedProperty property)
    {
        Drawer.IgnoreProperty(property);
    }

    public void IgnoreProperty(string propertyPath)
    {
        Drawer.IgnoreProperty(propertyPath);
    }
}

This example:

public override void OnInspectorGUI()
{
   EditorGUILayout.LabelField("Base Properties", EditorStyles.boldLabel);
   base.OnInspectorGUI();

   EditorGUILayout.LabelField("Toolbox Properties", EditorStyles.boldLabel);
   ToolboxEditorHandler.HandleToolboxEditor(this);
}

was used during my simple test and it works completely fine. I mean all properties are rendered properly, and all properties can be edited.
image

I assume there is no problem with Toolbox-related code itself, probably you need to dig into the com.unity.xr.interaction.toolkit package to see how Editors work there. My first bet: you should override the DrawProperties() method and call there serializedObject.Update() and serializedObject.ApplyModifiedProperties() methods.