Unity Editor: Add a reset-button to any control type

Continuing my series of concepts and solutions to adding or changing editor-related functionalities, this post covers a feature that I would love to see in Unity natively for most input controls. I'm talking about buttons next to each input, be it a string, number, or any other type, that resets that field back to its initial value, identical to the reset buttons next to the position, rotate, and scale parameters of each Transform component.

Though implementing this manually allows us to get a bit more creative and allows us to define a custom reset value (although we usually don't deviate from setting numbers back to zero and emptying a string), as well as where and how we want to display our reset buttons.

As per usual in my preferred tutorial format, let's first break down what is needed, look at a simple code example, and then recap what each part is for.

Because we don't want the reset-button taking too much space to keep our custom inspectors neat and tidy, our first requirement would be to put the button on the same line as the input itself, if applicable. Unlike in the aforementioned Transform component, I also prefer to put the reset button at the end of the control, as the left side is usually already reserved for the field name and having each reset button on the right allows us to align them evenly, provided we manage to sustain a well-organized control scheme.

Next, we want the button itself to take the least amount of space and not have 'Reset' written everywhere, so the tried and trusted 'x'-button will come in handy (and due to basic iconography makes it also universally comprehensible).

To see how this is done, here's an example with a custom float field that appends a reset button to its end:

/// <summary>
/// Draws a float field with a reset button.
/// </summary>
public static float DrawFloatField(string label, float value, float resetValue = 0f)
{
  GUILayout.BeginHorizontal();
  {
    value = EditorGUILayout.FloatField(label, value, GUILayout.MinWidth(20f));
    if (DrawResetButton()) value = resetValue;
  }
  GUILayout.EndHorizontal();

  return value;
}

You can change this to your desired variable type or add one for each control that you want to decorate with a useful reset button.

For the button itself, the following function suffices, which gets created in the if (DrawResetButton()) condition and returns, whether the button has been pressed, thus allowing us to reset the input to any value we want:

/// <summary>
/// Displays a button for resetting values. Optionally alter its size.
/// </summary>
public static bool DrawResetButton(float width = 20f, float height = 15f)
{
  if (GUILayout.Button("X", GUILayout.Width(width), GUILayout.Height(height)))
  {
    BlurFocus();
    return true;
  }
			
  return false;
}

We extracted the button to its own function because we probably want to use it on more than just one control type.

The width of 20 and height of 15 has been proven to be the ideal dimensions to display just one character of text inside our button (here a capital 'X') while also centering the button fairly in the middle of the rest of our ordinary control.

But in this case, you can also adjust the size of the button for each custom control differently.

The keen eye might have noticed that we are calling a function inside the button's on click behavior, that we haven't defined yet: BlurFocus(). This function makes sure that we remove the focus of any input field so that the changed values are being displayed properly, as Unity has this weird "bug/feature" in which the current value of any input stays the same when it's currently being focused, even if the value has already been changed outside of it.

So let's create the last piece of the puzzle:

/// <summary>
/// Removes the focus from any field to display changed values properly.
/// </summary>
public static void BlurFocus()
{
  //create an empty element outside of the current window,
  //apply an empty name to it and focus that element
  GUI.SetNextControlName("");
  GUI.Label(new Rect(-10, -10, 0, 0), "");
  GUI.FocusControl("");
}

Now we are getting really fancy by doing a little trickery I learned somewhere on the interwebz (probably StackOverflow), in which we create a new, temporary control OUTSIDE of the visible drawing area (the negative 10 in the x and y coordinates), give this control an inaudible name of an empty string and then put all the focus on that invisible control, thus removing the focus of any other field in the editor.

As this control also only exist for a short period of time, namely when pressing the reset button, it doesn't leave any empty controls that could eat up performance over time, even though that would also be negligible.

So there you have your newfangled reset-button.

---

As an added bonus and because it's fitting to the topic at hand, here's an example of how to achieve the same reset-any-value-back-to-default behavior by right-clicking on a custom control with an attribute drawer, where a button wouldn't really be a fitting option.

This example is from my custom know attribute that displays a dial/know that you can rotate to specify a certain angle between 0 and 360. But for our purpose, we just need to know that it has an outer Rect for the knob itself and an inner Rect for the directional indicator. We just need to get the current mouse position when doing a right-click and check if it overlaps with the outer Rect to perform our reset operation:

[CustomPropertyDrawer(typeof(KnobAttribute))]
public class KnobAttributeDrawer : PropertyDrawer
{
  private float angle;

  public override void OnGUI (Rect rect, SerializedProperty property, GUIContent label)
   {
    //prerequisites
    angle = property.floatValue;
    Event evt = Event.current;
    Vector2 mPos = evt.mousePosition;
  
    switch (evt.type)
     {
       case EventType.MouseDown:
         if (outerRect.Contains(mPos) && evt.button == 1) {
           BlurFocus();
           angle = 0;
           evt.Use();
         }

         break;
    }

    //…do something with angle…
  }
}

Here we check, if the current event is of type MouseDown, whether the right mouse button is being pressed (with Event.current.button == 1, when we want the left button we check for 0 (zero)) and the do the reset shebang (as we learned before by first blurring any input - because this attribute also has an extra float field for manual adjustment of the value - resetting the value and then using the event to prevent any further usage during that OnGUI() call.

Fri May 08 2020

Comments