Little gift if you want to use it. ![]()
When I started coding tools in Unity, one thing that annoyed me the most was the lack of modal windows. For some reason, the team behind Unity thought a simple Yes/No popup was enough and that nobody ever would need a modal windows to give the users a more complete choice. Modal Windows are useful because they allow you to give choices to the users while keeping integrity of the data that invoked the window in the first place.
Well, I disagree with them. So I set on to find a way to make one. I first found a way to popup a real .NET modal windows, but it needed System.Windows.Forms and that library just failed to work properly on Mac. Inter-platform support is one of my priority. On top, blocking Unityâs main thread tends to make it whine like a 6 years old kid which you stole his lollypops. sigh
If real modal windows were impossible, I still needed a way to give specific choices to the users. Popups, for this task, while not perfect, are cool because if they loses their focus, they simply go away. Therefore, nothing is broken because you didnât have time to change any data that invoked the popup. Sure, itâs annoying if you miss-click outside the popup, but I could live with that.
Another issue was communication. A normal modal window returns exactly to where the invoking thread called it so the communication is fairly straight forward. If any EditorWindow could call any kind of popup, that popup need a way to tells whoever summoned it that itâs now closed. On top, I like to have generic popup that works the same way from one window to another. As example, I have a âtoolboxâ that list a collection of choices. This popup is totally generic and any window can use it. Itâs invoked using CTRL+E. However, Unity doesnât like shortcut per-EditorWindow. So the invokation chain must be upside down⌠The toolbox must tell the currently focused window that it was invoked using a shortcut, and request it if it needs its services.
Frankly, itâs really not the best code or the best design. However, so far, itâs the best I could come up given the time I had, and it gives the result I needed. If you can improve over that, go ahead! (and tell me)
First, interface implementation that is added to any EditorWindow that want to use a modal popup;
/// <summary>
/// This EditorWindow can recieve and send Modal inputs.
/// </summary>
public interface IModal
{
/// <summary>
/// Called when the Modal shortcut is pressed.
/// The implementation should call Create if the condition are right.
/// </summary>
void ModalRequest(bool shift);
/// <summary>
/// Called when the associated modal is closed.
/// </summary>
void ModalClosed(ModalWindow window);
}
This is fairly straight forward. If you want a popup to handle its own shortcut invokation, the first method is there for that. In this case, I also pass down if âshiftâ is pressed because some of my popup change behaviour when this is the case. Of course, itâs not the popup that decide of this behaviour, but the EditorWindow that implements the interface.
Moving on;
public enum WindowResult
{
None,
Ok,
Cancel,
Invalid,
LostFocus
}
Fairly straight forward result of a window.
Next!
/// <summary>
/// Define a popup window that return a result.
/// Base class for IModal call implementation.
/// </summary>
public abstract class ModalWindow : EditorWindow
{
public const float TITLEBAR = 18;
protected IModal owner;
protected string title = "ModalWindow";
protected WindowResult result = WindowResult.None;
public WindowResult Result
{
get { return result; }
}
protected virtual void OnLostFocus()
{
result = WindowResult.LostFocus;
if (owner != null)
owner.ModalClosed(this);
}
protected virtual void Cancel()
{
result = WindowResult.Cancel;
if (owner != null)
owner.ModalClosed(this);
Close();
}
protected virtual void Ok()
{
result = WindowResult.Ok;
if (owner != null)
owner.ModalClosed(this);
Close();
}
private void OnGUI()
{
GUILayout.BeginArea(new Rect(0, 0, position.width, position.height));
GUILayout.BeginHorizontal(EditorStyles.toolbar);
GUILayout.Label(title);
GUILayout.EndHorizontal();
GUILayout.EndArea();
Rect content = new Rect(0, TITLEBAR, position.width, position.height - TITLEBAR);
Draw(content);
}
protected abstract void Draw(Rect region);
}
This is the base class I use for the popup window. I use this because it gives me a kind of title-bar and the standard method that all popup use are collected here in one place.
To draw your own stuff, just implement âDrawâ.
Example of one popup I made, which allows me to renamed one - or many - strings.
/// <summary>
/// The rename popup is a generic popup that allow the user to input a name or to rename an existing one.
/// You can pass a delegate to valide the currently input string.
/// </summary>
public class Rename : ModalWindow
{
public delegate bool ValidateName(string name);
public const float BUTTONS_HEIGHT = 30;
public const float FIELD_HEIGHT = 20;
public const float HEIGHT = 56;
public const float WIDTH = 250;
private static Texture cross;
public static Texture Cross
{
get
{
if (cross == null)
cross = Helper.Load(EditorResources.Cross);
return cross;
}
}
private static Texture check;
public static Texture Check
{
get
{
if (check == null)
check = Helper.Load(EditorResources.Check);
return check;
}
}
private string[] labels;
private string[] texts;
public string[] Texts
{
get { return texts; }
}
private ValidateName[] validate;
public static Rename Create(IModal owner, string title, string[] labels, string[] texts, Vector2 position)
{
return Create(owner, title, labels, texts, position, null);
}
public static Rename Create(IModal owner, string title, string[] labels, string[] texts, Vector2 position, ValidateName[] validate)
{
Rename rename = Rename.CreateInstance<Rename>();
rename.owner = owner;
rename.title = title;
rename.labels = labels;
rename.texts = texts;
rename.validate = validate;
float halfWidth = WIDTH / 2;
float x = position.x - halfWidth;
float y = position.y;
float height = HEIGHT + (labels.Length * FIELD_HEIGHT);
Rect rect = new Rect(x, y, 0, 0);
rename.position = rect;
rename.ShowAsDropDown(rect, new Vector2(WIDTH, height));
return rename;
}
protected override void Draw(Rect region)
{
bool valid = true;
if (validate != null)
{
for (int i = 0; i < validate.Length; i++)
{
if (validate[i] != null !validate[i](texts[i]))
{
valid = false;
break;
}
}
}
if (Event.current.type == EventType.KeyDown)
{
if (Event.current.keyCode == KeyCode.Return valid)
Ok();
if (Event.current.keyCode == KeyCode.Escape)
Cancel();
}
GUILayout.BeginArea(region);
GUILayout.Space(5);
for (int i = 0; i < texts.Length; i++)
{
GUILayout.BeginHorizontal();
if (validate != null validate[i] != null)
valid = validate[i](texts[i]);
if (valid)
{
GUI.color = Color.green;
GUILayout.Label(new GUIContent(Check), GUILayout.Width(18), GUILayout.Height(18));
}
else
{
valid = false;
GUI.color = Color.red;
GUILayout.Label(new GUIContent(Cross), GUILayout.Width(18), GUILayout.Height(18));
}
GUI.color = Color.white;
texts[i] = EditorGUILayout.TextField(texts[i]);
GUILayout.EndHorizontal();
}
GUILayout.Space(5);
GUILayout.BeginHorizontal();
GUI.enabled = valid;
if (GUILayout.Button("Ok"))
Ok();
GUI.enabled = true;
if (GUILayout.Button("Cancel"))
Cancel();
GUILayout.EndHorizontal();
GUILayout.EndArea();
}
}
Each string passed to the popup can have its own validate delegate to test if the string is valid or not. In our use case, it was use to create and modify localization string identification and text. For some obvious reason, the id has to be unique and with only standard (0-9, a-z, A-Z, _) characters. That localization would be passed down to an online database. The popup scale automatically for the number of text fields you pass it.
Important Note; If you use this script âas-isâ, it wonât compile on âHelper.Load(EditorResources.Check);â. In our framework, we have a .DLL holding all the texture we use in all our tools. Itâs easier to load and to deploy. You just need to replace that line by whatever way you want to load a texture. In our case, âCheckâ is a green check when the string is valid, and âCrossâ is a red cross when the string is not valid.
Hereâs an example of an implementation;
public class RenameObject : EditorWindow, IModal
{
private string text = "My Text";
[MenuItem("Window/Test Rename")]
static void Init()
{
RenameObject.GetWindow<RenameObject>();
}
public void ModalRequest(bool shift)
{
// Modal Rename doesn't have a MenuItem implementation. So this method is not used.
}
public void ModalClosed(ModalWindow window)
{
Rename rename = window as Rename;
if (rename == null || window.Result != WindowResult.Ok)
return;
text = rename.Texts[0];
Repaint();
}
private void OnGUI()
{
EditorGUILayout.LabelField(text);
if (GUILayout.Button("Edit Text"))
CreateModal();
}
private void CreateModal()
{
Rename.Create(this, "Rename My Text!", new string[] { "Text: " },
new string[] { text }, GUIUtility.GUIToScreenPoint(Event.current.mousePosition));
}
}
The result is;




Enjoy!
P.S.: I know the code displayed here is not âcleanâ. It was a very quickly put together - compared to what we are using - to give an example of what could be used. Feel free to improve it. Hopefully this wasnât a complete waste of time and someone may find it useful. ![]()
