Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Add custom button below "Add Component" button

Discussion in 'Scripting' started by WanAmir, Jul 26, 2023.

  1. WanAmir

    WanAmir

    Joined:
    Jul 17, 2018
    Posts:
    27
    Hi everyone.
    How do i create custom button like Add Component button in Inspector window?
    I have tried create custom editor script but the script is dependent with the component. I want when the user click the GameObject in the Hierarchy, it always shows below Add Component button.
    I want to achieve something like this:
    Screenshot_1.png
    Screenshot are taken from Asset Store called AI Toolbox for ChatGPT and DALL·E | Generative AI | Unity Asset Store

    Thank you
     
  2. KillDashNine

    KillDashNine

    Joined:
    Apr 19, 2020
    Posts:
    449
    I don't think it's possible to put it there, but at least it's possible to override your GameObject or Transform default editors. Like this:


    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3.  
    4. [CustomEditor(typeof(Transform))]
    5. public class TransformEditorOverride : Editor {
    6.  
    7.     public override void OnInspectorGUI() {
    8.         base.OnInspectorGUI();
    9.         GUILayout.Button("hello");
    10.     }
    11. }
     
  3. WanAmir

    WanAmir

    Joined:
    Jul 17, 2018
    Posts:
    27
    I tried and it doesn't work like i want it.
    Here's the result:
    Screenshot_1.png

    Then I tried change the type to GameObject like this:
    Code (CSharp):
    1. [CustomEditor(typeof(GameObject))]
    Here's the result:
    Screenshot_2.png

    Thats what I thought first but then how these developers do it.
     
  4. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    Which developers?

    It's probably not impossible but a lot of this stuff is internal C# code that you can't get access to without liberal amounts of reflection or use of a publiciser.

    All the code relating to the
    InspectorWindow
    is internal. We can't even repaint it without reflection.
     
  5. WanAmir

    WanAmir

    Joined:
    Jul 17, 2018
    Posts:
    27
    This developers: AI Toolbox for ChatGPT and DALL·E | Generative AI | Unity Asset Store
    Im not sure how they achieve this. Maybe someone bought this asset can share a bit code ;)

    The closest thing i can get is from UI Toolkit.
    Screenshot_3.png
    If I select the element in the red box, Unity will highlight the area in the green box.
    But UI Toolkit is complicated for me
     
  6. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    20,083
    Okay, having played around with editor scripting for a bit and not getting any results I decided I was curious enough to buy a copy of the asset and see what they're doing, and it's almost 200 lines of code involving reflection like was mentioned into the Unity editor to make everything work.

    Edit: After skimming through the code for a few minutes I was able to find some key elements to make a far better search on Google, and here is the code built from the results made to be as simple as possible. I'm a little shocked that it ended up less than one-fourth the number of lines.

    You will need to apply formatting to make it look more presentable, but you should be able to just pull all of that from the other button.

    upload_2023-7-26_22-9-0.png

    Code (CSharp):
    1. using System.Linq;
    2. using UnityEditor;
    3. using UnityEngine;
    4. using UnityEngine.UIElements;
    5.  
    6. [InitializeOnLoad]
    7. public static class Example
    8. {
    9.     static Example()
    10.     {
    11.         EditorApplication.update += Update;
    12.     }
    13.  
    14.     private static void Update()
    15.     {
    16.         // Reflection to retrieve a reference to the Inspector window.
    17.         var assembly = typeof(Editor).Assembly;
    18.         var windowTypeName = "UnityEditor.InspectorWindow";
    19.         var windowType = assembly.GetType(windowTypeName);
    20.         var inspectorWindow = Resources.FindObjectsOfTypeAll(windowType).FirstOrDefault() as EditorWindow;
    21.  
    22.         // Scans the visual elements. If it finds the "Add Component" button it adds a button directly below it.
    23.         if (inspectorWindow == null) return;
    24.         inspectorWindow.rootVisualElement.Query().ForEach(visualElement =>
    25.         {
    26.             if (!visualElement.GetClasses().Contains("unity-inspector-add-component-button")) return;
    27.  
    28.             if (visualElement.childCount == 1)
    29.             {
    30.                 var button = new Button(() => OnClick())
    31.                 {
    32.                     text = "Button Text"
    33.                 };
    34.  
    35.                 visualElement.Add(button);
    36.             }
    37.         });
    38.     }
    39.  
    40.     private static void OnClick()
    41.     {
    42.         Debug.Log("Foo!");
    43.     }
    44. }
     
    Last edited: Jul 27, 2023
  7. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,495
    You may replace your ForEach with a "proper" UQuery call. It works similar to JQuery in the browser / javascript world. This would reduce the lines even more. Though I barely used UIToolkit, so I currently can't tell you the exact syntax. The UQuery reference should help here. It's probably just
    Code (CSharp):
    1.  
    2. var addComponentContainer = inspectorWindow.rootVisualElement.Q(className:"unity-inspector-add-component-button").FirstOrDefault();
    3. if(addComponentContainer != null){
    4. // add the button here
    5. }

    Though I don't think using EditorApplication.update is a good idea here. That delegate runs about 200 times per second if I remember correctly. A hook when the inspector content is rebuilt would be great, but Unity still lacks of many callbacks that would be super useful. So I don't have a good replacement at the moment :) Maybe a delayed call from a custom inspector could do the trick or generateVisualContent of the root element. Though if it should be on every gameobject you probably need a different approach. Note that the inspector may "inspect" something that is not a gameobject and it wouldn't have the add component button in that case.
     
    Ryiah and Yuchen_Chang like this.
  8. KillDashNine

    KillDashNine

    Joined:
    Apr 19, 2020
    Posts:
    449
    Through privileged access.
     
  9. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    20,083
    Here's the result of applying your recommendations and improving readability. I've forgotten the exact message but
    generateVisualContent
    wouldn't allow the code to add the button during the callback so that's why I have lines 13 and 32 adding and removing the callback to
    EditorApplication.update
    .

    I briefly looked into formatting the button to match the Add Component button only to discover that the button is an IMGUI button within an IMGUI container. I imagine that would require more reflection to access and I just didn't want to muddy the code with all of it.
    Code (CSharp):
    1. using System.Linq;
    2. using UnityEditor;
    3. using UnityEngine;
    4. using UnityEngine.UIElements;
    5.  
    6. [InitializeOnLoad]
    7. public static class MyCustomButton
    8. {
    9.     private static readonly VisualElement CustomButton;
    10.  
    11.     static MyCustomButton()
    12.     {
    13.         GetInspectorWindow().rootVisualElement.generateVisualContent += _ => { EditorApplication.update += Update; };
    14.  
    15.         CustomButton = new Button(OnClick)
    16.         {
    17.             text = "My Custom Button"
    18.         };
    19.     }
    20.  
    21.     private static void Update()
    22.     {
    23.         var inspectorWindow = GetInspectorWindow();
    24.         if (inspectorWindow == null) return;
    25.  
    26.         var addComponentButton = GetAddComponentButton(inspectorWindow.rootVisualElement);
    27.         if (addComponentButton != null && addComponentButton.childCount > 0 && !addComponentButton.Contains(CustomButton))
    28.         {
    29.             addComponentButton.Add(CustomButton);
    30.         }
    31.  
    32.         EditorApplication.update -= Update;
    33.     }
    34.  
    35.     private static void OnClick()
    36.     {
    37.         // do stuff
    38.     }
    39.  
    40.     private static EditorWindow GetInspectorWindow()
    41.     {
    42.         var windowType = typeof(Editor).Assembly.GetType("UnityEditor.InspectorWindow");
    43.         var inspectorWindow = Resources.FindObjectsOfTypeAll(windowType).FirstOrDefault() as EditorWindow;
    44.  
    45.         return inspectorWindow;
    46.     }
    47.  
    48.     private static VisualElement GetAddComponentButton(VisualElement rootVisualElement)
    49.     {
    50.         if (rootVisualElement != null)
    51.             return rootVisualElement.Q(className: "unity-inspector-add-component-button").GetFirstOfType<VisualElement>();
    52.  
    53.         return null;
    54.     }
    55. }
     
    Last edited: Jul 27, 2023
    Bunny83 and WanAmir like this.
  10. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,495
    Yes, that's one way you could do it. Though using EditorApplication.delayCall would probably make more sense. It's similar to update but delegates subscribed to this event are automatically removed after one execution. Since adding / removing delegates generate garbage, delayCall should reduce it a bit. Thought since it probably isn't called that often it probably doesn't matter much.

    Of course there may be other implications like multiple Inspector windows. Though what we do here is just a proof of concept. People should be able to adapt to their needs.
     
  11. KillDashNine

    KillDashNine

    Joined:
    Apr 19, 2020
    Posts:
    449
    In short, it's not meant to be done without hacking the editor with reflection.

    Reflection in general is a bad idea. I know a lot of people will disagree with this, so I will tell you why. Basically reflection means tinkering with runtimes and bypassing the compiler and public APIs.

    If Unity had meant for developers to put buttons into that place, they would have provided an API for it. Now that there's no such API, putting a button there means to do it with reflection in a way that is first of all slow, error-prone and difficult to read, but secondly tied to the editor version in a manner that any undocumented change to that CSS class will break it. This breaking bug will break in an untraceable way and litter your code, you'll have something that breaks in a manner that nobody knows what's wrong, and you'll in vain try to contact the original developer who just sends you a postcard from the Bahamas saying "good luck".

    In any commercial project, being in charge of code quality, I would reject anything written with reflection. And in general, if you are able to do something, doesn't mean you should do it.
     
    Last edited: Jul 28, 2023
    Chubzdoomer, Ryiah and Bunny83 like this.
  12. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,495
    That's true

    While this is true as a general statement, there are valid exceptions. Reflection should be avoided in the same way goto should be avoided. It's not bad in general but has a high tendency to produce hard to read / hard to reason code and espectially hard to test properly. So it's adviced to avoid if possible since it bypasses encapsulation which is one of the safety guards that OOP gives us to avoid stupid mistakes.

    Well, as mentioned above you're not bypassing the compiler but just the "access contracts" we get from OOP which are there to make it harder to shoot yourself in the foot. The compiler does actually compile the reflection code as well :)

    That's not really a good argument. In the past Unity was much more restricted when it comes to customization of editor UI. A lot has been added later and a lot workaround and hacks that were used before are now possible natively as Unity has added a callback or systems to integrate menus

    Well, do you use Newtonsoft's Json.NET? Yes? So you should reject it immediately, because pretty much all object mappers use reflection to serialize and deserialize. There are exceptions with things like most protobuf implementation which essentially do the reflection part at compile time and create type specific serialization and deserialization code.

    That's true. But that's also the point where you have to weigh the benefits against the costs. Especially in editor code that only influences your development team and not the actual product the risks are much lower. I had created a lot of reflection based solutions for shortcommings in the Unity editor. Some are not necessary anymore. Proper tooling is important to be productive. Just saying: "Stick to what Unity provides you" doesn't make much sense. I do agree that you should first look for an alternative solution if possible / applicable. Though in the end the only thing that matters is if it works and if it helps your team. Yes, you should be aware of potential issues when upgrading Unity (which you shouldn't do in the middle of production anyways). All my reflection based solutions always come with a notice and warning that messing with internal stuff can break in the future, even though it's unlikely.
     
    KillDashNine, Ryiah and SisusCo like this.
  13. KillDashNine

    KillDashNine

    Joined:
    Apr 19, 2020
    Posts:
    449
    There's obviously a difference of using reflection against JSON which is an open standard, than using reflection against just whatever internal workings Unity Editor might have, such as its internal CSS layouts. Object mappers are well-contained entities for a singular task which is to map between a standardised text format and a data format.

    Reflection is a low level hacking tool. And I wouldn't trust any reflection based object mapper unless it was well tested under a community. Which is why I would (and in backend, most anybody would) reject reflection based code outright. Cos it's a place where the majority of what you are doing is trying to avoid bubblegum-based solutions rather than inviting them.

    And no, I don't use Newtonsoft's Json.NET for the same reason of lack of maturity as why I would not even install Unity 2023.
     
  14. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    20,083
    While reading your post it occurred to me that the only thing the reflection was doing in the code was fetching the type for the Inspector window, and then immediately searching through a list of EditorWindows which means that the window is accessible without reflection.

    Here's an update version of the GetInspectorWindow() method without reflection.
    Code (CSharp):
    1. private static EditorWindow GetInspectorWindow()
    2. {
    3.     var editorWindowArray = Resources.FindObjectsOfTypeAll<EditorWindow>();
    4.     var inspectorWindow = editorWindowArray.FirstOrDefault(window => window.titleContent.text == "Inspector");
    5.  
    6.     return inspectorWindow;
    7. }
     
    KillDashNine likes this.
  15. KillDashNine

    KillDashNine

    Joined:
    Apr 19, 2020
    Posts:
    449
    Potentially better that way at least from code perspective. And yeah overall kudos to you that the original task was achieved :) Now somebody can infest their codebase with this hack :D
     
  16. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    20,083
    Alright, I think this is the final update for this script. I've added support for multiple inspectors, I've rearranged some of the code and added fields to easily change UI Elements class names, and I've added support for a UI style sheet (just need to place it into a Resources folder with a name that matches the appropriate field).

    I've profiled the script. On execution (via
    delayCall
    ) it generates 0.5KB of garbage and takes 0.5ms. Most of the garbage is likely LINQ. Like the earlier versions of the script no reflection was involved. The style sheet was made by eyeing the differences as I adjusted the values.

    upload_2023-7-28_20-6-55.png

    Code (CSharp):
    1. using System.Linq;
    2. using UnityEditor;
    3. using UnityEngine;
    4. using UnityEngine.UIElements;
    5.  
    6. [InitializeOnLoad]
    7. public static class MyCustomButton
    8. {
    9.     private static readonly string CustomButtonText = "My Custom Button";
    10.     private static readonly string CustomButtonClassName = "unity-inspector-my-custom-button";
    11.     private static readonly string CustomButtonStyleSheet = "MyCustomButton";
    12.  
    13.     private static readonly string UnityInspectorClassName = "unity-inspector-main-container";
    14.     private static readonly string AddComponentButtonClassName = "unity-inspector-add-component-button";
    15.  
    16.     static MyCustomButton()
    17.     {
    18.         EditorApplication.delayCall += OnUpdate;
    19.     }
    20.  
    21.     private static void OnUpdate()
    22.     {
    23.         var inspectorWindowArray = TryGetInspectorWindows();
    24.         if (inspectorWindowArray.Length == 0) return;
    25.  
    26.         foreach (var inspectorWindow in inspectorWindowArray)
    27.         {
    28.             AddCustomButton(inspectorWindow);
    29.         }
    30.  
    31.         EditorApplication.delayCall += OnUpdate;
    32.     }
    33.  
    34.     private static void OnClick()
    35.     {
    36.         Debug.Log("Click.");
    37.     }
    38.  
    39.     private static void AddCustomButton(EditorWindow editorWindow)
    40.     {
    41.         var addComponentButton = GetAddComponentButton(editorWindow.rootVisualElement);
    42.         if (addComponentButton == null || addComponentButton.childCount < 1) return;
    43.  
    44.         var customButton = GetCustomButton(addComponentButton);
    45.         if (customButton != null) return;
    46.  
    47.         var button = new Button(OnClick)
    48.         {
    49.             text = CustomButtonText
    50.         };
    51.         button.AddToClassList(CustomButtonClassName);
    52.  
    53.         var styleSheet = Resources.Load<StyleSheet>(CustomButtonStyleSheet);
    54.         if (styleSheet)
    55.         {
    56.             button.styleSheets.Add(styleSheet);
    57.         }
    58.  
    59.         addComponentButton.Add(button);
    60.     }
    61.  
    62.     private static EditorWindow[] TryGetInspectorWindows()
    63.     {
    64.         return Resources
    65.             .FindObjectsOfTypeAll<EditorWindow>()
    66.             .Where(window => window.rootVisualElement.Q(className: UnityInspectorClassName) != null)
    67.             .ToArray();
    68.     }
    69.  
    70.     private static VisualElement GetAddComponentButton(VisualElement rootVisualElement)
    71.     {
    72.         return rootVisualElement
    73.             .Q(className: AddComponentButtonClassName);
    74.     }
    75.  
    76.     private static VisualElement GetCustomButton(VisualElement rootVisualElement)
    77.     {
    78.         return rootVisualElement
    79.             .Q(className: CustomButtonClassName);
    80.     }
    81. }

    Code (CSharp):
    1. .unity-inspector-my-custom-button {
    2.     align-self: center;
    3.     min-height: 25px;
    4.     max-height: 25px;
    5.     min-width: 230px;
    6.     max-width: 230px;
    7. }
     
    Last edited: Jul 29, 2023
    WanAmir likes this.