marijnz / unity-toolbar-extender

Extend the Unity Toolbar with your own Editor UI code.
MIT License
1.66k stars 168 forks source link

Simple approach #5

Closed OndrejPetrzilka closed 5 years ago

OndrejPetrzilka commented 5 years ago

By taking advantage of UIElements, we can attach drawing handler right AFTER the toolbar.

This works well when resizing window and changing layouts. Tested on Unity 2018.2.10f1. Implementation could be probably improved:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEditor;
using System.Reflection;
using UnityEngine.Experimental.UIElements;

[InitializeOnLoad]
public static class ToolbarExtension
{
    static Type m_toolbarType = typeof(Editor).Assembly.GetType("UnityEditor.Toolbar");
    static Type m_guiViewType = typeof(Editor).Assembly.GetType("UnityEditor.GUIView");
    static PropertyInfo m_viewVisualTree = m_guiViewType.GetInstanceProperty("visualTree");
    static FieldInfo m_imguiContainerOnGui = typeof(IMGUIContainer).GetInstanceField("m_OnGUIHandler");

    static ToolbarExtension()
    {
        EditorApplication.update -= OnUpdate;
        EditorApplication.update += OnUpdate;
    }

    private static void OnUpdate()
    {
        // Find toolbar
        var toolbar = (ScriptableObject)Resources.FindObjectsOfTypeAll(m_toolbarType).FirstOrDefault();

        // Get it's visual tree
        var visualTree = (VisualElement)m_viewVisualTree.GetValue(toolbar);

        // Get first child which 'happens' to be toolbar IMGUIContainer
        var container = (IMGUIContainer)visualTree.Children().First();

        // Attach our handler
        var handler = (Action)m_imguiContainerOnGui.GetValue(container);
        handler -= OnGUI;
        handler += OnGUI;
        m_imguiContainerOnGui.SetValue(container, handler);
    }

    private static void OnGUI()
    {
        var buttonRect = new Rect(Screen.width / 2 + 50, 0, 100, 30);
        buttonRect.y = 4;

        if (GUI.Button(buttonRect, "TestButton"))
        {
            Debug.Log("Pressed");
        }
    }
}
marijnz commented 5 years ago

Great! Both approach #4 and #5 crossed my mind but didn't get concrete. I did explore the UIElements way before, but lost my way (code was highly undergoing change too). So anyway, this is awesome and props to you for finding a way. It's pretty clean, like really clean compared to the current solution and #4 ;). Only potential downside that comes up now is that it's likely to break because of UIElements undergoing changes, but that's ok.

I could "properly" implement it tomorrow morning? Or you create a PR.

OndrejPetrzilka commented 5 years ago

Do you have some idea how layout serialization works? It looks like magic to me... visualTree.Add seems like cleaner solution, however it might be problematic if layout serializes UIElements somehow.

marijnz commented 5 years ago

No I haven't looked into it. But serialisation is also problematic with the current solution, that's why I added this code that prevents it: https://github.com/marijnz/unity-toolbar-extender/blob/master/Assets/ToolbarExtender/Scripts/Editor/ExtendedToolbarWindow.cs#L134. That's also an option with UIElements

OndrejPetrzilka commented 5 years ago

Okay, I'm afraid it might get broken when visualTree is modified.

I've tested modification when toolbar object is cached in static field. Since it's ScriptableObject, it's possible to detect when it's destroyed by null check. This makes EditorApplication.update very fast in most cases (single null check). Everything else still works (resizing, layout changes).

I've also removed [InitializeOnLoad] since it's not necessary, when callback is added to OnToolbarGUI, hook gets installed (through static constructor).

I'll make PR which will add this class soon. Together with one more helper class which helps with positioning around existing toolbar controls.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEditor;
using System.Reflection;
using UnityEngine.Experimental.UIElements;

public static class ToolbarCallback
{
    static Type m_toolbarType = typeof(Editor).Assembly.GetType("UnityEditor.Toolbar");
    static Type m_guiViewType = typeof(Editor).Assembly.GetType("UnityEditor.GUIView");
    static PropertyInfo m_viewVisualTree = m_guiViewType.GetProperty("visualTree", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
    static FieldInfo m_imguiContainerOnGui = typeof(IMGUIContainer).GetField("m_OnGUIHandler", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

    static ScriptableObject m_currentToolbar;

    /// <summary>
    /// Callback for toolbar OnGUI method.
    /// </summary>
    public static Action OnToolbarGUI;

    static ToolbarCallback()
    {
        EditorApplication.update -= OnUpdate;
        EditorApplication.update += OnUpdate;
    }

    static void OnUpdate()
    {
        // Relying on the fact that toolbar is ScriptableObject and gets deleted when layout changes
        if (m_currentToolbar == null)
        {
            // Find toolbar
            m_currentToolbar = (ScriptableObject)Resources.FindObjectsOfTypeAll(m_toolbarType).At(0);
            if (m_currentToolbar != null)
            {
                // Get it's visual tree
                var visualTree = (VisualElement)m_viewVisualTree.GetValue(m_currentToolbar);

                // Get first child which 'happens' to be toolbar IMGUIContainer
                var container = (IMGUIContainer)visualTree[0];

                // (Re)attach handler
                var handler = (Action)m_imguiContainerOnGui.GetValue(container);
                handler -= OnGUI;
                handler += OnGUI;
                m_imguiContainerOnGui.SetValue(container, handler);
            }
        }
    }

    static void OnGUI()
    {
        OnToolbarGUI?.Invoke();
    }
}