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.
The 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.
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.
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:
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();
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:
What’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);
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 currentGUIWindow
.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 byIMGUI
when the window is fist created, and when it’s resized or moved.OnGUI
does not seem to be called periodically likeUpdate
, 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:
Awesome and very useful posts.
I’m hoping to see part 3 soon 🙂
LikeLike
This is great, thanks! When is part 3 coming? 🙂
LikeLike