Recreating the Unity Console: Tips and Trick for Editor Scripting II

This post is continuation on the series of posts dedicated to creating editor extensions in Unity.
You can find part one here.

Toolbars

In the previous part we mostly did preparation work, now it’s time to start drawing some stuff. But first, a few notes on GUI scripting.

GUI vs EditorGUI

In general using the EditorGUI/EditorGUILayout object in the editor windows seems to be better, as it usually draws controls that have a similar look and feel to the ones used in the editor. Also, EditorGUILayout.BeginHorizontal/Vertical returns the allocated rectangle during the Repaint event (more about the Repaint later), which will turn out to be quite useful. With that said, I haven’t found any reason not to mix between GUI, EditorGUI, GUILayout and EditorGUILayout

Drawing Toolbars

In order to draw a toolbar, all we need to do is draw a horizontal area using the toolbar style (either from EditorStyles or the editor skin). If you inspect the style in the editor you’ll find that it has a fixed height set at 18 pixels, which can be used as the height of your toolbar buttons. Keep in mind that you do need to set GUILayout.Height parameter if you’re going for the layout version, or there will be gap between your toolbar and the areas directly below or above it.

EditorGUILayout.BeginHorizontal (EditorStyles.toolbar, GUILayout.Height(EditorStyles.toolbar.fixedHeight), GUILayout.ExpandWidth(true));
EditorGUILayout.EndHorizontal ();

The lines above draw toolbar rectangle that stretches the whole width of the window.
toolbarThe option GUILayout.ExpandWidth(true) is required to make the layout system stretch the toolbar rectangle, otherwise only space for the padding and border will be allocated.
toolbar_wrong
Drawing the buttons is fairly straightforward as well – just draw a simple button using the toolbarButton style. A couple of things need to be mentioned – in the layout mode you don’t need to specify a height, since the button will take on the height of the enclosing area, but you need to specify width – for some reason buttons are expanding by default. Also, there doesn’t seem to be a toolbarToggle style, so drawing toggles (like “Collapse”, for instance) should be done using toolbarButton as well.

EditorGUILayout.BeginHorizontal (EditorStyles.toolbar, GUILayout.Height(EditorStyles.toolbar.fixedHeight), GUILayout.ExpandWidth(true));
{
    GUILayout.Button("Clear", EditorStyles.toolbarButton, GUILayout.Width(50));
    collapse = GUILayout.Toggle(collapse, "Collapse", EditorStyles.toolbarButton, GUILayout.Width(70));
    clearOnPlay = GUILayout.Toggle(clearOnPlay, "Clear on Play", EditorStyles.toolbarButton, GUILayout.Width(70));
    errorPause = GUILayout.Toggle(errorPause, "Error Pause", EditorStyles.toolbarButton, GUILayout.Width(70));
}
EditorGUILayout.EndHorizontal ();

You’ll notice one problem with the image below – it looks nothing like console toolbar, all the buttons are the wrong size. One solution would be to simply eyeball it or grab Photoshop and measure the exact size of the original buttons, but that’s not the proper way to do it.
buttons_wrong

Content Size

The best way would be to exactly measure the size of the content we put in the buttons using GUIStyle.CalcSize.

private bool collapse;
private bool clearOnPlay;
private bool errorPause;

private GUIContent clearContent = new GUIContent("Clear");
private GUIContent collapseContent = new GUIContent("Collapse");
private GUIContent clearOnPlayContent = new GUIContent("Clear on Play");
private GUIContent errorPauseContent = new GUIContent("ErrorPause");

private void OnGUI()
{
    var style = EditorStyles.toolbarButton;
    EditorGUILayout.BeginHorizontal (EditorStyles.toolbar, GUILayout.Height(EditorStyles.toolbar.fixedHeight), GUILayout.ExpandWidth(true));
    {
        GUILayout.Button(clearContent, style, GUILayout.Width(style.CalcSize(clearContent).x));
        GUILayout.Space(7);
        collapse = GUILayout.Toggle(collapse, collapseContent, style, GUILayout.Width(style.CalcSize(collapseContent).x));
        clearOnPlay = GUILayout.Toggle(clearOnPlay, clearOnPlayContent, style, GUILayout.Width(style.CalcSize(clearOnPlayContent).x));
        errorPause = GUILayout.Toggle(errorPause,errorPauseContent, style, GUILayout.Width(style.CalcSize(errorPauseContent).x));
    }
    EditorGUILayout.EndHorizontal ();
}

What we’ve done here is that we’ve created a GUIContent for each button, then we display that content in the button and also use it to calculate the required size of that button. Keep in mind that, as it says in the docs, CalcSize is not very good at determining the size of the content if it needs to be word wrapped.
Now, our buttons are the exact size they need to be, and coincidentally the exact same size as the original console (I admit to eyeballing space between the first two buttons).
There are, of course, much more optimized ways of using these functions, but since this is an example there is no point in making the code harder to understand. Also with this set up we can do something cool:

console

Custom Cursors and Resizing

As you’ve probably noticed, the original console is separate into two areas – let’s call the top one the message area and the bottom one – the details area.

Getting Area Size

To simulate that we’re going to draw to separate vertical areas, filled with to boxes to expand them.

GUIStyle box = GUI.skin.GetStyle("CN Box");
EditorGUILayout.BeginVertical();
GUILayout.Box("", box, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true));
EditorGUILayout.EndVertical();
EditorGUILayout.BeginVertical();
GUILayout.Box("", box, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true));
EditorGUILayout.EndVertical();

This looks very similar, but there is a problem – you can’t move the line in the middle, whereas in the original console the two areas can be resized freely. That creates a problem – we want to resize the stuff inside the main area, but we don’t know the size of the main area itself. We could probably setup some constants that ensure some fixed size of the console and then subtract the height of the toolbar from it.

However, that is nowhere near the flexibility of the original console. You’ve probably also noticed that as you resize the console the areas also resize, keeping their relative sizes.

Here, EditorGUILayout comes to the rescue! If we look at the reference for BeginVertical we’ll see that it returns a Rect object.

public static Rect BeginVertical(params GUILayoutOption[] options);

This is the rectangle that the vertical area will occupy. How the function knows it’s rectangle in advance will be covered below. For now we’ll just make use of this behavior to set the relative sizes of our areas.

float ratio = 0.9f;
Rect rect = EditorGUILayout.BeginVertical(GUILayout.ExpandHeight(true));
{
    EditorGUILayout.BeginVertical(GUILayout.Height(rect.height * ratio));
    GUILayout.Box("", box, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true));
    EditorGUILayout.EndVertical();
    EditorGUILayout.BeginVertical();
    GUILayout.Box("", box, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true));
    EditorGUILayout.EndVertical();
}
EditorGUILayout.EndVertical();

wrong_ratio

However, there is a problem – the size of the areas don’t seem to correspond to the ratio number. If we slap a Debug.Log(rect), we’ll see the following input:

debugWhat’s going on here? Well, we’ve stumbled onto our fist event.

Events

Whenever Unity calls OnGUI on your window it can be for various reasons, one such reason is painting the view, but it can also be because of user triggered events like button presses.
To find out the reason OnGUI was called you have to look into the Event class, and more particularly Event.current object. Let’s see what happens if we output the type of the event every time OnGUI is called.

        Debug.Log("Event Type " + Event.current.type);
        Debug.Log("Rectangle: " + rect);

event_log

We can see that the zero size of rect is associated with the Layout event. This is because Layout is a special type of event that seems to be called before painting the the actual window. It is associated with -Layout objects like GUILayout or EditorGUILayout and through this event the size of the BeginVertical rectangle is known before it’s inner elements have been drawn.

The problem with our setup is that during the layout phase we pass zeroes as the sub-area size, which messes up the layout and our areas don’t reflect what we’ve set up in code.
A small check should take care of this.

private Rect rect;
/*.....*/
Rect tmp = EditorGUILayout.BeginVertical(GUILayout.ExpandHeight(true));
{
    if ((Event.current.type != EventType.Layout) && (Event.current.type != EventType.used))
    {
    rect = tmp;
    }

    EditorGUILayout.BeginVertical(GUILayout.Height(rect.height * ratio));
    GUILayout.Box("", box, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true));
    EditorGUILayout.EndVertical();
    EditorGUILayout.BeginVertical();
    GUILayout.Box("", box, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true));
    EditorGUILayout.EndVertical();
}

Now, if you set ratio to something like 0.1 you’ll see that one ares is clearly smaller. EventType.used is also filtered away, since it creates a similar problem to Layout.

An EventType.used occurs when Event.Use() has been called on the current event, this prevents the inner UI elements from triggering actions on the same event.

Now that we have the console correctly displaying what we want, let’s examine the following snippet and look into some more events:

private Rect resizeRect = new Rect(0, 0, 0, 20);
private bool resizing = false;
private float ratio = 0.7f;
private const int MinBoxSize = 28; // I admit to eyeballing this from he original console

/*here lies the toolbar drawing code we did last time */
if (Event.current.type == EventType.MouseDown && resizeRect.Contains(Event.current.mousePosition))
{
    resizing = true;
}
else if (resizing && Event.current.type == EventType.MouseUp)
{
    resizing = false;
}

if (resizing && (Event.current.type == EventType.MouseDrag))
{
    resizeRect.y = Event.current.mousePosition.y - rect.y;
    ratio = resizeRect.y / rect.height;
    Repaint();
}

Rect tmp = EditorGUILayout.BeginVertical(GUILayout.ExpandHeight(true));
{
    if ((Event.current.type != EventType.Layout) && (Event.current.type != EventType.used))
    {
        rect = tmp;
        resizeRect.width = rect.width;
        resizeRect.y = Mathf.Clamp(rect.height * ratio, MinBoxSize, rect.height - MinBoxSize);
        ratio = (resizeRect.y) / rect.height;
    }

    EditorGUILayout.BeginVertical(GUILayout.Height(rect.height * ratio));
    GUILayout.Box("", box, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true));
    EditorGUILayout.EndVertical();
    EditorGUILayout.BeginVertical();
    GUILayout.Box("", box, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true));
    EditorGUILayout.EndVertical();
}
EditorGUILayout.EndVertical();

/*here lies the rest of the GUIWindow */

Here we use MouseDown, MouseUp and MouseDrag to check weather the user is clicking round the border area and trying to resize the box. Here, resizeRect is used to define the rectangle around the border line. Changing the last parameter of resizeRect’s constructor will change how easy it is to select the exact area around the border.Note that we don’t filter away the Mouse events when updating rect – otherwise the border line will hiccup when being dragged.

Notice the Repaint(); call after we resize the areas.

Calling Repaint() forces Unity to redraw the current GUIWindow. Repaint() needs to be called after the user makes any changes to the UI to ensure that it updates. Usually Layout and Repaint calls are made by IMGUI when the window is fist created, and when it’s resized or moved. OnGUI does not seem to be called periodically like Update, likely for performance reasons.

Cursor Rect

One last touch is the cursor – if you look at the original console you’ll see that the cursor changes to a vertical resize icon when you hover the border. How do they do that?
Enter cursor rectangles. Through EditorGUIUtility.AddCursorRect Unity allows you to change the cursor of the mouse over different areas of the UI. Thus we only need to add

EditorGUIUtility.AddCursorRect(resizeRect, MouseCursor.ResizeVertical);

in the bottom of the function to draw the resize cursor. We should end up with something like this:
console2

Advertisements

2 thoughts on “Recreating the Unity Console: Tips and Trick for Editor Scripting II

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s