Search Unity

Modifying component inspector headers (for component folders)

Discussion in 'Immediate Mode GUI (IMGUI)' started by DreamingImLatios, Nov 12, 2017.

  1. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    I'm fairly new to editor extensions and such, and am trying to implement component folders. I tend to build systems with composition which means that I can very easily stack up 30-50 components on a GameObject.

    I actually have nearly all of the basics of the system working. But there's one catch, I can't rename folders in a way that is intuitive.

    ComponentFolders Help 1.PNG

    I have several ideas on how to get around this, but I'm not sure what the best approach is.

    Approach 1. Create a custom Inspector Window to replace the existing window. This might allow me to control the headings, but then I would need to figure out how to draw all the basic GameObject header stuff. This would be the answer if I had a base custom inspector window to go off of, but right now I don't know where to start.

    Approach 2. Try drawing an out-of-bounds rectangle to overlap the header and display the new name. I'm not sure if this is allowed, and if it is, I would prefer to do so without hard-coding the offsets. And then comes the problem of whether or not I can read drag and drop in that region.

    Approach 3. Override OnHeaderGUI or some other method that I'm not aware of. This would also be an ideal solution if I found something that worked. But it seems that an Editor's header is only used for assets and isn't used for Components.

    Approach 4. Create a tool that generates a class which inherits from ComponentFolder every time a folder is renamed. Recompiling code would be terrible and I still probably wouldn't get drag and drop working.

    Any ideas?

    Thanks in advance!
     
    NotaNaN likes this.
  2. Fajlworks

    Fajlworks

    Joined:
    Sep 8, 2014
    Posts:
    344
    I got curious reading your post and have been fiddling with Editor scripts. It is unfortunate Approach 3 doesn't work because OnHeaderGUI is internal and not exposed to us, as it seems the most elegant.

    I managed to implement something like Approach 2:
    Code (CSharp):
    1. #if UNITY_EDITOR
    2. using UnityEngine;
    3. using UnityEditor;
    4.  
    5. [CustomEditor (typeof (ComponentFolder))]
    6. internal class ComponentFolderEditor : Editor
    7. {
    8.     Color proColor = (Color) new Color32 (56, 56, 56, 255);
    9.     Color plebColor = (Color) new Color32 (194, 194, 194, 255);
    10.  
    11.     public override void OnInspectorGUI ()
    12.     {
    13.         OnHeaderGUI();
    14.  
    15.         base.OnInspectorGUI ();
    16.     }
    17.  
    18.     protected override void OnHeaderGUI ()
    19.     {
    20.         var rect = EditorGUILayout.GetControlRect(false, 0f);
    21.         rect.height = EditorGUIUtility.singleLineHeight;
    22.         rect.y -= rect.height;
    23.         rect.x = 48;
    24.         rect.xMax -= rect.x * 2f;
    25.  
    26.         EditorGUI.DrawRect (rect, EditorGUIUtility.isProSkin ? proColor : plebColor);
    27.  
    28.         string header = (target as ComponentFolder).folderName; // <--- your variable
    29.         if (string.IsNullOrEmpty (header))
    30.             header = target.ToString();
    31.  
    32.         EditorGUI.LabelField (rect, header, EditorStyles.boldLabel);
    33.     }
    34. }
    35.  
    36. #endif
    Good thing is, it supports click and drag. One downside is that component has to be expanded in order to run the HeaderGUI code. If you can find a way to call it while collapsed, great!

    As for Approach 1, that might be the best option but definitely the most time-consuming. You could analyse the Editor implementation some guys did with ILSpy and try to recreate a similar implementation in your own EditorWindow: https://github.com/MattRix/UnityDecompiled/blob/master/UnityEditor/UnityEditor/Editor.cs

    Hope it helps!
     
  3. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    Hey! That's pretty cool!

    I'm going to have to do some testing to see if events on the overdrawn rect trump priority over the original header's events. If so, I could make it so that once the drop-down is opened, it couldn't be closed. Even if it means the component always remains double the height of being fully collapsed, that would still be sufficient.

    You also gave me an idea for yet another approach to this problem, but I don't think it will work either. If there was a convenient means to detect whenever a component was added, it could be easily snatched and added to a list in a ComponentFolderSystem class which would be a required component by all ComponentFolders and would do all of the drawing for components.

    Anyways, thank you for the help!

    I'm probably going to test this with some event handling over the next weekend and see if custom events can block the minimizing event. If it works, it will only be a matter of time before I get this system into a clean enough state that I can open source it!
     
  4. Fajlworks

    Fajlworks

    Joined:
    Sep 8, 2014
    Posts:
    344
    Actually, there is an "elegant" way to handle added component, with Reset() function in Editor subclass.

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Events;
    3.  
    4. #if UNITY_EDITOR
    5. using UnityEditor;
    6.  
    7. [CustomEditor (typeof (ComponentFolder))]
    8. internal class ComponentFolderEditor : Editor
    9. {
    10.      void Reset ()
    11.      {
    12.           var folder = target as ComponentFolder;
    13.           int count = folder.GetComponents<Component>().Length;
    14.           if (folder. lastComponentCount != count)
    15.           {
    16.                folder.lastComponentCount = count;
    17.            
    18.                // your event handling solution
    19.                folder.OnComponentsChanged.Invoke ();
    20.           }
    21.      }
    22. }
    23.  
    24. #endif
    25.  
    26. public class ComponentFolder : MonoBehaviour
    27. {
    28.      [HideInInspector]
    29.      [SerializeField]
    30.      internal int lastComponentCount = 0;
    31.  
    32.      public UnityEvent OnComponentsChanged = new UnityEvent();
    33. }
    However, this is editor only. Hope it helps!
     
  5. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    So I got to test out all of these today.

    Regarding overriding Unity's events, I'm not even close. Unity's InspectorWindow seems to trump everything.

    I do want to know how you figured out that an Editor subclass has a Reset callback! There is no documentation on it anywhere. But it works as you said, so I guess that is my solution. This will take me a couple weeks before I find more time to rebuild my system with the new scheme.

    Thanks for all your help!
     
  6. Fajlworks

    Fajlworks

    Joined:
    Sep 8, 2014
    Posts:
    344
    There is some obscure documentation for MonoBehaviour:
    https://docs.unity3d.com/ScriptReference/MonoBehaviour.Reset.html

    Editor subclasses from ScriptableObject, and ScriptableObject has some similar lifecycle methods like OnEnabled, OnDisabled, etc. But yeah, seems like undocumented feature.

    Actually this post pointed me into the right direction:
    https://answers.unity.com/questions/235514/execute-code-when-component-added.html

    As for overriding Unity's events - what's giving you trouble; overriding functionality or the inspector drawing?
     
  7. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    I knew about MonoBehaviour.Reset. However, Editor.Reset is undocumented, and gets called on all Editors for a GameObject when a new component gets added or removed. It is awesome and I'm glad we discovered it. I'm just wondering why it isn't documented and why it works.

    In regards to Unity events, I'm referring to the editor's engine-side event handling. Basically what is happening is in the rectangle that covers the component name instead of putting the folder name, I tried putting other UI icons such as foldouts and checkboxes. They do not respond at all. Instead the ComponentFolder collapses and expands as normal. I tried adding debugging messages, checking mouse events and using them if they occur inside the rectangle, and several other techniques. It's not even that both my icons and the Unity interface are both responding to the events. It's more like Unity eats the events before they even propagate to my code.

    It's not a huge issue if I can draw all the components inside a master component automatically, which Editor.Reset lets me do. But I feel it isn't as clean. Oh well.
     
  8. Fajlworks

    Fajlworks

    Joined:
    Sep 8, 2014
    Posts:
    344
    Yeah, a master component approach seems like the way to go. Perhaps you could create a specialized EditorWindow that mimicks the inspector, but only for your master component. That way you can customize the layout how you like, without normal components interfering. You could even have something like Windows style folder icons, which you can double click to expand. Sounds fun, good luck!
     
  9. SorraTheOrc

    SorraTheOrc

    Joined:
    Jun 11, 2016
    Posts:
    208
    Small update to the script to override the header. Only change is a tweak of the darkskin color code and the positioning of the rect, this is to update to the new UI from 2019:


    Code (CSharp):
    1. Color darkSkinHeaderColor = (Color)new Color32(62, 62, 62, 255);
    2.         Color lightSkinHeaderColor = (Color)new Color32(194, 194, 194, 255);
    3.  
    4.         protected override void OnHeaderGUI()
    5.         {
    6.             var rect = EditorGUILayout.GetControlRect(false, 0f);
    7.             rect.height = EditorGUIUtility.singleLineHeight * 1.4f;
    8.             rect.y -= rect.height;
    9.             rect.x = 60;
    10.             rect.xMax -= rect.x * 2f;
    11.  
    12.             EditorGUI.DrawRect(rect, EditorGUIUtility.isProSkin ? darkSkinHeaderColor : lightSkinHeaderColor);
    13.  
    14.             string header = (target as GenericAIBehaviour).DisplayName + " (AI Behaviour)";
    15.             if (string.IsNullOrEmpty(header))
    16.                 header = target.ToString();
    17.  
    18.             EditorGUI.LabelField(rect, header, EditorStyles.boldLabel);
    19.         }
    20.    
    Still not figured out how to keep this change when the component is collapsed though :-(
     
  10. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,329
    I have a solution in the asset store now that makes it very easy to rename components :)

    Pulling off this functionality on the other hand was not very easy :p

    The first part of the puzzle was figuring out that Unity stores component inspector titles internally in a dictionary in a nested class inside ObjectNames.
    Code (CSharp):
    1. class ObjectNames
    2. {
    3.     static class InspectorTitles
    4.     {
    5.          static readonly Dictionary<Type, string> s_InspectorTitles; // <-- There you are hiding!
    6.     }
    7. }
    Realizing that was a big Eureka moment for me!

    But even with that key piece of knowledge the second immediate issue is that the titles are stored on a per-type basis, not per instance. Devising a solution for swapping in the custom names for each component just before their header is drawn required quite a bit of work.

    If you were to limit the renaming functionality to only components that have the DisallowMultipleComponent attribute, then it should be relatively simple to inject custom names to the dictionary during the Editor.finishedDefaultHeaderGUI event.
     
    NotaNaN likes this.
  11. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,329
    This is the code I used for retrieving the internal dictionary, if somebody needs it:
    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using System.Reflection;
    4. using UnityEditor;
    5.  
    6. public static class ObjectNamesUtility
    7. {
    8.     public static Dictionary<Type, string> GetInternalInspectorTitlesCache()
    9.     {
    10.         Type inspectorTitlesType = typeof(ObjectNames).GetNestedType("InspectorTitles", BindingFlags.Static | BindingFlags.NonPublic);
    11.         var inspectorTitlesField = inspectorTitlesType.GetField("s_InspectorTitles", BindingFlags.Static | BindingFlags.NonPublic);
    12.         return (Dictionary<Type, string>)inspectorTitlesField.GetValue(null);
    13.     }
    14. }
    Example usage:
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class Example : MonoBehaviour
    4. {
    5.     [ContextMenu("Robots in disguise")]
    6.     private void Test()
    7.     {
    8.         var inspectorTitles = ObjectNamesUtility.GetInternalInspectorTitlesCache();
    9.         inspectorTitles[typeof(Transform)] = "Transformers!";
    10.     }
    11. }
    Example result:
    transformers.png
     
  12. ExNinja

    ExNinja

    Joined:
    Dec 4, 2013
    Posts:
    30
    @SisusCo thank you. This is fantastic!

    One issue that some people may run into is that this has issues when there are two Components of the same type in the Inspector, and you wish to have different names for them (but that is a pretty rarified issue).
     
    SisusCo likes this.
  13. Vigil

    Vigil

    Joined:
    Jun 12, 2013
    Posts:
    1
    @SisusCo

    Fantastic, you're a lifesaver, this is exactly what I've been looking for!
     
    SisusCo likes this.