Improving the EditorWindow class in Unity

Extending the Unity editor UI is pretty simple. They have a bunch of classes that you inherit from and the editor will automatically discover and wire them up for you.

A couple of days ago I started looking into extending the editor with a custom window for some behavior tree work I am doing. I wanted to create a simple tree designer.

The initial spike went well and I managed to open up a new window, with very little code and no problems, but then I fell into the rabbit hole...

Creating a simple editor window

To create a custom window, in the Unity editor, all you have to do is inherit from a class called EditorWindow and provide a static Init-method.

The purpose of the Init-method is to provide a "factory"-like method that Unity can use to create an instance of your editor window.

The code below created a blank editor window and tells Unity to add a menu item, called MyEditorWindow in the Window menu.

public class MyEditorWindow : EditorWindow
{
    [MenuItem("Window/MyEditorWindow")]
    public static void Init()
    {
        var window =
            EditorWindow.GetWindow<MyEditorWindow>();

        // Prevent the window from being destroyed when a new
        // scene is loaded into the editor.
        Object.DontDestroyOnLoad(window);
    }

    private void OnGUI()
    {
        // Put your code here
    }
}

Looks simple right? The OnGUI method has a dark secret. It does not really communicate its full intent and it violates the Single Responsibility Principle *gasp*

More than meets the eye

As you saw, in our previous code sample, we are supposed to put our editor GUI code in the OnGUI-method. If we check the documentation for EditorWindow.OnGUI method

Implement your own editor GUI here.
Use OnGUI to draw all the controls of your window.

Then we see that we have reason not to believe that this is all there is to it, right? It turns out, that when I started to implement my custom editor window I sometimes got random ArgumentNullException errors.
While searching for the cause, I came across something that caught me by surprise; the OnGUI-method can be called several times, with different intents, by the Unity editor.

In reality you have to take a look at the Event.type property (in reality you are also going to want to make sure that the event was intended for your control and not someone else), to know what kind of pass that the editor is doing when it is calling into your method. The type property return one of the EventType enum values and, as you can see, you are supposed to handle everything from repaint, layout, mouse-events, keyboard-events and so on, from inside a single OnGUI-method.

Even if you are careful, this is likely to lead to you writing horrible spaghetti code that is riddled with if/else selections. So not only does the OnGUI-method (or its associated documentation) express its intent clearly, but it will also write code that quickly will become hard to maintain as the complexity of your editor increase.

A stab at a better approach

I come from a strong background in software engineering so code structure and quality are two things that are very close to my heart.

To make it easier for myself to write my editor, I started looking into alternative designs to decrease the complexity of my code and to help increase the maintainability. I also wanted to solve the problem of having code that did not clearly express its intent. I want to run the right code, at the right time, and still have readable code.

My solution was to inherit from EditorWindow and provide my own abstraction. The class is very simple. It overrides the OnGUI-method and use an event map to delegate the call to more appropriate methods. It effectively intercepts calls to OnGUI and split them up into finer grained, more intent driven` methods, by using an event map.

Using System;
using System. Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public abstract class ExtendedEditorWindow : EditorWindow
{
    public Dictionary<EventType, Action> EventMap { get; set; }

    public ExtendedEditorWindow()
    {
        this.EventMap = new Dictionary<EventType, Action>
        {
            { EventType.ContextClick, this.OnContext },
            { EventType.Layout, this.OnLayout },
            { EventType.Repaint, this.OnRepaint },

            { EventType.KeyDown, () => {
                this.OnKeyDown(new Keyboard(Event.current));
            }},

            { EventType.KeyUp, () => {
                this.OnKeyUp(new Keyboard(Event.current));
            }},

            { EventType.MouseDown, () => {
                this.OnMouseDown((MouseButton)Event.current.button, Event.current.mousePosition);
            }},

            { EventType.MouseDrag, () => {
                this.OnMouseDrag((MouseButton)Event.current.button, Event.current.mousePosition,
                    Event.current.delta);
            }},

            { EventType.MouseMove, () => {
                this.OnMouseMove(Event.current.mousePosition, Event.current.delta);
            }},

            { EventType.ScrollWheel, () => {
                this.OnScrollWheel(Event.current.delta);
            }}
        };
    }

    protected virtual void OnGUI()
    {
        var controlId =
            GUIUtility.GetControlID(FocusType.Passive);

        var controlEvent =
            Event.current.GetTypeForControl(controlId);

        if(this.EventMap.ContainsKey(controlEvent))
        {
            this.EventMap[controlEvent].Invoke();
        }
    }

    protected void OnKeyDown(Keyboard keyboard)
    {
    }

    protected void OnKeyUp(Keyboard keyboard)
    {
    }

    protected virtual void OnMouseDown(MouseButton button, Vector2 position)
    {
    }

    protected virtual void OnMouseDrag(MouseButton button, Vector2 position, Vector2 delta)
    {
    }

    protected virtual void OnMouseMove(Vector2 position, Vector2 delta)
    {
    }

    protected virtual void OnContext()
    {
    }

    protected virtual void OnLayout()
    {
    }

    protected virtual void OnRepaint()
    {
    }

    protected virtual void OnScrollWheel(Vector2 delta)
    {
    }
}

The class contains a method for each of the Event.type values, that I am interested in managing. The mapping between the event type and method is stored in a simple dictionary. The event map is made public so when you inherit from the class, you can add more or override existing mappings.

The event map also performs a bit of mapping between the raw event and the method that is called to handle it. For instance the various mouse & keyboard related events are all passed to methods along with parameters that have been transformed into easy to use formats, instead of having to read it from the event.

Also, by making the methods virtual, instead of abstract, we only have to override the ones that are interesting to us and ignore the else.

For the sake of completeness, here is the MouseButton class

public enum MouseButton
{
    Left = 0,
    Right = 1,
    Middle = 2
}

and the Keyboard class

public class Keyboard
{

    public Keyboard()
    {   
    }

    public Keyboard(Event evt)
    {
        this.Code = evt.keyCode;
        this.IsAlt = evt.alt;
        this.IsCapsLock = evt.capsLock;
        this.IsControl = evt.control;
        this.IsFunctionKey = evt.functionKey;
        this.IsNumeric = evt.numeric;
        this.IsShift = evt.shift;
        this.Modifiers = evt.modifiers;
    }

    public KeyCode Code { get; set; }

    public bool IsAlt { get; set; }

    public bool IsCapsLock { get; set; }

    public bool IsControl { get; set; }

    public bool IsFunctionKey { get; set; }

    public bool IsNumeric { get; set; }

    public bool IsShift { get; set; }

    public EventModifiers Modifiers { get; set; }
}

These are used by ExtendedEditorWindow to pass in nicely formatted parameters, to the mouse & keyboard related methods.

Taking it for a spin

Let us revisit the MyEditorWindow implementation and make it use the new ExtendedEditorWindowclass instead

public class MyEditorWindow : ExtendedEditorWindow
{
    [MenuItem("Window/MyEditorWindow")]
    public static void Init()
    {
        var window =
            EditorWindow.GetWindow<MyEditorWindow>();

        // Prevent the window from being destroyed when a new
        // scene is loaded into the editor.
        Object.DontDestroyOnLoad(window);
    }

    protected override void OnMouseDrag(MouseButton button, Vector2 position, Vector2 delta)
    {
        // Code to handle mouse drag events
    }

    protected override void OnRepaint()
    {
        // Code to handle repaint events
    }

    // Override other methods that you need
}

As you can see, the intent of the code is now a lot clearer. You simply put your code in the right kind of methods and all of a sudden you are no longer violating the Single Responsibility Principle and your code will be a lot easier to maintain.

The source code for ExtendedEditorWindow, Keyboard and MouseButton are made available, under a MIT license, in the unity.resources repository on my github account.

I accept pull-requests! :P

comments powered by Disqus