OdinDrawer's for ScripableObjects?

Issue #87 resolved
Scott Richmond created an issue

I am trying to replace our own drawers with Odins and I cannot seem to get it to draw for a ScriptableObject. Below is some sample code in use - The goal is to be able to draw the Blueprint class and its components (which do have a strong type, but are stored as objects).

The Odin drawer simply does nothing.

public class Blueprint
{
    private readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings()
    {
        TypeNameHandling = TypeNameHandling.All,
        NullValueHandling = NullValueHandling.Include,
        ReferenceLoopHandling = ReferenceLoopHandling.Ignore
    };

    public List<object> Components = new List<object>();
}
[CreateAssetMenu(menuName = "Ravioli/New Blueprint")]
public class BlueprintContainer : ScriptableObject
{
    [SerializeField]
    private string _data;

    [NonSerialized]
    public Blueprint Blueprint;

    public void OnEnable()
    {
        Deserialize();
    }

    public void Deserialize()
    {
        Blueprint = string.IsNullOrEmpty(_data)
            ? new Blueprint()
            : JsonConvert.DeserializeObject<Blueprint>(_data,
                new JsonSerializerSettings()
                {
                    TypeNameHandling = TypeNameHandling.Auto,
                    //NullValueHandling = NullValueHandling.Include,
                    ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
                    //Binder = new GUIDBasedTypeBinder(),
                    Error = HandleDeserializationError
                    //Converters = new List<JsonConverter>() { new UnityObjectJsonConverter() }
                });
    }

    public void Serialize()
    {
        Serialize(Blueprint);
    }

    public void Serialize(Blueprint blueprint)
    {
        _data = JsonConvert.SerializeObject(blueprint,
            new JsonSerializerSettings()
            {
                TypeNameHandling = TypeNameHandling.Auto,
                //NullValueHandling = NullValueHandling.Include,
                ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
                Formatting = Formatting.None,
                //Binder = new GUIDBasedTypeBinder(),
                //Converters = new List<JsonConverter>() { new UnityObjectJsonConverter() }
            });
    }

    public void HandleDeserializationError(object sender, ErrorEventArgs errorArgs)
    {
        var currentError = errorArgs.ErrorContext.Error.Message;
        Debug.LogError(currentError);
        errorArgs.ErrorContext.Handled = true;
    }
}
[OdinDrawer]
    public class BlueprintContainerInspector : OdinValueDrawer<BlueprintContainer>
    {
        protected override void DrawPropertyLayout(IPropertyValueEntry<BlueprintContainer> entry, GUIContent label)
        {
            BlueprintContainer bpContainer = entry.SmartValue;
            SirenixEditorGUI.DetailedMessageBox("BlueprintContainer msg box", "more info", MessageType.Info, true);
            this.CallNextDrawer(entry, label);
        }
    }

Comments (9)

  1. Tor Esa Vestergaard

    OdinDrawer classes (derived from OdinValueDrawer, OdinAttributeDrawer or OdinGroupDrawer) are all property drawers, not custom editors which are found by the inspector window. They do nothing but draw properties that already exist and are contained by, for example, an inspector window, or others - through the use of Odin's PropertyTree class.

    So your BlueprintContainerInspector drawer would only ever have a chance of being used if you had a reference to a BlueprintContainer in your inspector somewhere. Even then, it has the same (default) priority as Odin's UnityObjectDrawer class, so you would have to mark your drawer with a higher priority using, say, [DrawerPriority(0, 0, 5)].

    The answer to your problem would be to make Odin draw your scriptable object type - you can do this in Window -> Odin Inspector -> Preferences -> Editor Types. Search for the BlueprintContainer class, and make sure that the checkbox next to it is marked. If it doesn't have a checkbox, you already have a custom editor defined for your BlueprintContainer class, and Odin will refuse to touch the class. You will either have to delete your custom editor class (its name will be shown in the editor types preferences), or change your custom editor class so it draws Odin-style editors.

    For example, you could derive a custom editor from OdinEditor, and draw the Odin part of your inspector by calling base.OnInspectorGUI - or you can make your own property tree, and draw that:

    [CustomEditor(typeof(BlueprintContainer))]
    public class MyCustomEditor : Editor
    {
        private PropertyTree tree;
    
        public override void OnInspectorGUI()
        {
            // Make sure you have an Odin property tree
            if (tree == null)
            {
                tree = PropertyTree.Create(serializedObject);
            }
    
            // Custom UI stuff
    
            tree.Draw(true); // Draw Odin
        }
    }
    

    Hopefully that should resolve your issue.

  2. Scott Richmond reporter

    Thanks for the additional info. However I'm struggling given the lack of documentation on how I might draw each object (Which in fact has a strong struct type) in the List<object> on the Blueprint? I'd like to draw the objects and their contents as a series of FoldoutGroups.

  3. Tor Esa Vestergaard

    Whoops, must have missed your reply here. Sorry about that!

    I'm not sure exactly what you need, but if I'm guessing right, this example I've cooked up should perhaps get you going. It will cause the Blueprint components to be drawn by Odin, and results in an inspector looking like this: http://prntscr.com/fdgd8x

    (Note that Odin does need to be enabled for the type BlueprintContainer - you can check if it is in "Windows -> Odin Inspector -> Preferences -> Editor Types")

    using UnityEngine;
    using Sirenix.OdinInspector;
    using System;
    using System.Collections.Generic;
    using Sirenix.OdinInspector.Editor;
    using System.Collections;
    using Sirenix.Utilities.Editor;
    
    [CreateAssetMenu(menuName = "Ravioli/New Blueprint")]
    public class BlueprintContainer : ScriptableObject
    {
        [NonSerialized]
        public Blueprint Blueprint = new Blueprint();
    
    #if UNITY_EDITOR // No need to have this in builds, since it only exists to be rendered in the inspector
    
        // List drawer settings make sure we can't add or remove items from the list, nor rearrange them by dragging (I presume you have your own preferred way of doing that?)
        [ShowInInspector, ListDrawerSettings(DraggableItems = false, IsReadOnly = true), BlueprintComponentWrapper]
        private List<object> BlueprintComponents
        {
            get
            {
                return this.Blueprint.Components; // This will draw Blueprint.Components as a list at the "root level" of the inspector
            }
            set { } // Empty set so the list instance can't be changed
        }
    
    #endif
    }
    
    public class Blueprint
    {
        public List<object> Components = new List<object>() { new MyComponentStruct1(), new MyComponentStruct2() }; // Just populate it with some data for the example
    }
    
    public struct MyComponentStruct1
    {
        public string Struct1Value1;
        public string Struct1Value2;
        public string Struct1Value3;
        public string Struct1Value4;
    }
    
    public struct MyComponentStruct2
    {
        public int Struct2Value;
    }
    
    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
    public class BlueprintComponentWrapperAttribute : Attribute
    {
    }
    
    #if UNITY_EDITOR // Might want to put this into an editor folder script, though it'll work just fine here
    
    [OdinDrawer]
    public class BlueprintComponentWrapperDrawer<T> : OdinAttributeDrawer<BlueprintComponentWrapperAttribute, T> // Draw for any type that has a BlueprintComponentWrapperAttribute
    {
        public override bool CanDrawTypeFilter(Type type)
        {
            // Further filter so that we don't wrap the list itself, but only its elements
            return !typeof(IList).IsAssignableFrom(type);
        }
    
        protected override void DrawPropertyLayout(IPropertyValueEntry<T> entry, BlueprintComponentWrapperAttribute attribute, GUIContent label)
        {
            var isExpandedContext = entry.Property.Context.Get(this, "is_expanded", false); // Get a context value for the expansion toggle
    
            SirenixEditorGUI.BeginBox();
            SirenixEditorGUI.BeginBoxHeader();
            {
                isExpandedContext.Value = SirenixEditorGUI.Foldout(isExpandedContext.Value, GUIHelper.TempContent(entry.TypeOfValue.Name)); // Create a foldout with the type name as label
            }
            SirenixEditorGUI.EndBoxHeader();
    
            if (SirenixEditorGUI.BeginFadeGroup(UniqueDrawerKey.Create(entry, this), isExpandedContext.Value))
            {
                // Call the next drawer, which will draw all the fields of the list items
                this.CallNextDrawer(entry.Property, null);
            }
            SirenixEditorGUI.EndFadeGroup();
            SirenixEditorGUI.EndBox();
        }
    }
    
    #endif
    

    I hope that helps. If it doesn't, I'm afraid I'll need a bit more context before I understand exactly what it is you need. We're currently writing the Drawers 101 section of our manual which explains exactly how to do things like this, but we're still prioritizing responding to issues like this over finishing that, so that people don't feel neglected while we work on the manual.

  4. Scott Richmond reporter

    Thanks! This works pretty well. A couple of things:

    1. The list + sign - Can I customize what shows up there? The objects that are in this list are all decorated by a specific attribute. I already have a function that can return the types available using reflection, etc. I just need a version of AssetList that works for me, like so:
        [ShowInInspector]
        [ListDrawerSettings(DraggableItems = true, IsReadOnly = false, Expanded = true)]
        [AssetList(CustomFilterMethod = "GetComponentTypes")]
        [BlueprintComponentWrapper]
        private List<object> BlueprintComponents {
            get {
                return Blueprint.Components; // This will draw Blueprint.Components as a list at the "root level" of the inspector
            }
            set { Blueprint.Components = value; } // Empty set so the list instance can't be changed
        }
    
        public static IEnumerable<Type> GetComponentTypes()
        {
            return AppDomain.CurrentDomain.GetAssemblies()
                .SelectMany(a => a.GetTypes()
                    .Where(t => t.GetCustomAttribute<ComponentType>() != null));
        }
    
    1. My components that show up in the list seem to automatically expand lists inside them. IE: http://i.imgur.com/4G3i7AU.png
      Can all expandables inside a component shown here be collapsed by default?
    2. For some reason the following struct won't show its public properties unless I explicitly write [ShowInInspector] for each property. Can I force all properties to show when rendering via this Blueprint Drawer?
        public struct RoomVisuals
        {
            // ResourceRef is just another struct with at least 1 public property specifically with a [ShowInInspector]
            public ResourceRef<GameObject> WallPhysicsPrefab;
            public List<ResourceRef<GameObject>> FloorTileset;
            public List<ResourceRef<GameObject>> WallTileset;
            public ResourceRef<GameObject> Floor;
            public ResourceRef<GameObject> Door;
        }
    
  5. Tor Esa Vestergaard

    The list + sign - Can I customize what shows up there? The objects that are in this list are all decorated by a specific attribute. I already have a function that can return the types available using reflection, etc. I just need a version of AssetList that works for me, like so:

    You can't (for now) customize what shows up when you click the plus sign. You can, however, add your own icon with your own special behaviour:

    #if UNITY_EDITOR // No need to have this in builds, since it only exists to be rendered in the inspector
    
        // List drawer settings make sure we can't add or remove items from the list, nor rearrange them by dragging (I presume you have your own preferred way of doing that?)
        [ShowInInspector, ListDrawerSettings(OnTitleBarGUI = "OnTitleBarGUI"), BlueprintComponentWrapper]
        private List<object> BlueprintComponents
        {
            get
            {
                return this.Blueprint.Components; // This will draw Blueprint.Components as a list at the "root level" of the inspector
            }
            set { } // Empty set so the list instance can't be changed
        }
    
        private void OnTitleBarGUI()
        {
            if (SirenixEditorGUI.ToolbarButton(EditorIcons.HamburgerMenu))
            {
                UnityEditor.GenericMenu menu = new UnityEditor.GenericMenu();
    
                foreach (var type in GetComponentTypes())
                {
                    Type capture = type; // Make sure the lambda gets a proper closure capture (this depends on your compiler)
    
                    menu.AddItem(new GUIContent(capture.Name), false, () =>
                    {
                        this.Blueprint.Components.Add(Activator.CreateInstance(capture));
                    });
                }
    
                menu.ShowAsContext();
            }
        }
    
        public static IEnumerable<Type> GetComponentTypes()
        {
            return AppDomain.CurrentDomain.GetAssemblies()
                .SelectMany(a => a.GetTypes()
                    .Where(t => t.GetCustomAttribute<ComponentType>() != null));
        }
    
    #endif
    

    My components that show up in the list seem to automatically expand lists inside them. IE: http://i.imgur.com/4G3i7AU.png Can all expandables inside a component shown here be collapsed by default?

    You can adjust whether foldouts and lists are collapsed or expanded by default in "Windows -> Odin Inspector -> Preferences -> Drawers -> General". (Note that this setting won't affect the default expansion of the foldouts drawn by the blueprint wrapper drawer for each item, only the foldouts drawn by Odin itself, as they use the "GeneralDrawerConfig.Instance.ExpandFoldoutByDefault" value as the default value for their drawer contexts.)

    For some reason the following struct won't show its public properties unless I explicitly write [ShowInInspector] for each property. Can I force all properties to show when rendering via this Blueprint Drawer?

    You cannot do so on the drawer level, as this happens on the property tree level - and the full property tree is generated for the BlueprintContainer type. You can, however, mark the BlueprintContainer class with the [ShowOdinSerializedPropertiesInInspector] attribute. This will cause the inspector to assume that the type is being specially serialized (by you in this case, not Odin, but the difference should be negligible), and thus show public fields that Unity normally wouldn't show by default, such as the ones you have there.

    My inspector now looks like this, and the full code is like this:

    using UnityEngine;
    using Sirenix.OdinInspector;
    using System;
    using System.Collections.Generic;
    
    using System.Collections;
    using System.Linq;
    using Sirenix.Utilities;
    
    #if UNITY_EDITOR
    
    using Sirenix.OdinInspector.Editor;
    using Sirenix.Utilities.Editor;
    using UnityEditor;
    
    #endif
    
    [CreateAssetMenu(menuName = "Ravioli/New Blueprint")]
    [ShowOdinSerializedPropertiesInInspector]
    public class BlueprintContainer : ScriptableObject
    {
        [NonSerialized]
        public Blueprint Blueprint = new Blueprint();
    
    #if UNITY_EDITOR // No need to have this in builds, since it only exists to be rendered in the inspector
    
        // List drawer settings make sure we can't add or remove items from the list, nor rearrange them by dragging (I presume you have your own preferred way of doing that?)
        [ShowInInspector, ListDrawerSettings(OnTitleBarGUI = "OnTitleBarGUI"), BlueprintComponentWrapper]
        private List<object> BlueprintComponents
        {
            get
            {
                return this.Blueprint.Components; // This will draw Blueprint.Components as a list at the "root level" of the inspector
            }
            set { } // Empty set so the list instance can't be changed
        }
    
        private void OnTitleBarGUI()
        {
            if (SirenixEditorGUI.ToolbarButton(EditorIcons.HamburgerMenu))
            {
                GenericMenu menu = new GenericMenu();
    
                foreach (var type in GetComponentTypes())
                {
                    Type capture = type; // Make sure the lambda gets a proper closure capture (this depends on your compiler)
    
                    menu.AddItem(new GUIContent(capture.Name), false, () =>
                    {
                        this.Blueprint.Components.Add(Activator.CreateInstance(capture));
                    });
                }
    
                menu.ShowAsContext();
            }
        }
    
        public static IEnumerable<Type> GetComponentTypes()
        {
            return AppDomain.CurrentDomain.GetAssemblies()
                .SelectMany(a => a.GetTypes()
                    .Where(t => t.GetCustomAttribute<ComponentType>() != null));
        }
    
    #endif
    }
    
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
    public class ComponentType : Attribute
    {
    }
    
    public class Blueprint
    {
        public List<object> Components = new List<object>() { new MyComponentStruct1(), new MyComponentStruct2() }; // Just populate it with some data for the example
    }
    
    [ComponentType]
    public struct MyComponentStruct1
    {
        public string Struct1Value1;
        public string Struct1Value2;
        public string Struct1Value3;
        public string Struct1Value4;
    }
    
    [ComponentType]
    public struct MyComponentStruct2
    {
        public int Struct2Value;
    }
    
    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
    public class BlueprintComponentWrapperAttribute : Attribute
    {
    }
    
    #if UNITY_EDITOR // Might want to put this into an editor folder script, though it'll work just fine here
    
    [OdinDrawer]
    public class BlueprintComponentWrapperDrawer<T> : OdinAttributeDrawer<BlueprintComponentWrapperAttribute, T> // Draw for any type that has a BlueprintComponentWrapperAttribute
    {
        public override bool CanDrawTypeFilter(Type type)
        {
            // Further filter so that we don't wrap the list itself, but only its elements
            return !typeof(IList).IsAssignableFrom(type);
        }
    
        protected override void DrawPropertyLayout(IPropertyValueEntry<T> entry, BlueprintComponentWrapperAttribute attribute, GUIContent label)
        {
            var isExpandedContext = entry.Property.Context.Get(this, "is_expanded", false); // Get a context value for the expansion toggle
    
            SirenixEditorGUI.BeginBox();
            SirenixEditorGUI.BeginBoxHeader();
            {
                isExpandedContext.Value = SirenixEditorGUI.Foldout(isExpandedContext.Value, GUIHelper.TempContent(entry.TypeOfValue.Name)); // Create a foldout with the type name as label
            }
            SirenixEditorGUI.EndBoxHeader();
    
            if (SirenixEditorGUI.BeginFadeGroup(UniqueDrawerKey.Create(entry, this), isExpandedContext.Value))
            {
                // Call the next drawer, which will draw all the fields of the list items
                this.CallNextDrawer(entry.Property, null);
            }
            SirenixEditorGUI.EndFadeGroup();
            SirenixEditorGUI.EndBox();
        }
    }
    
    #endif
    
  6. Scott Richmond reporter

    Awesome thanks for the solid replies here. The only remaining issue is the list menu thing - That's no good for this purpose as we need search and sorting into their namespace categories like the default list has. It would be nice to see that expanded upon soon. In the mean time I think I can get my previous inspector code working again with the Odin inspector.

  7. Tor Esa Vestergaard

    That's good to hear. As for the last issue, we're working on a new, much-improved type resolver to replace the one we have now - it'll be able to handle constructors, fetch assets or even objects from the current scene, and will also support entirely custom type resolution logic. We're not sure when it will land yet, but when it does, you will be able to completely customize the types you can get from it. Meanwhile, you can at least sort the menu items you get by namespaces, by using submenus in the GenericMenu, achieved by introducing forward slashes into the menu item name:

    private void OnTitleBarGUI()
    {
        if (SirenixEditorGUI.ToolbarButton(EditorIcons.HamburgerMenu))
        {
            GenericMenu menu = new GenericMenu();
    
            foreach (var type in GetComponentTypes())
            {
                Type capture = type; // Make sure the lambda gets a proper closure capture (this depends on your compiler)
    
                menu.AddItem(new GUIContent(capture.GetNiceFullName().Replace(".", "/")), false, () =>
                {
                    this.Blueprint.Components.Add(Activator.CreateInstance(capture));
                });
            }
    
            menu.ShowAsContext();
        }
    }
    
  8. Log in to comment