Search Unity

Editor Modal Window

Discussion in 'Scripting' started by LightStriker, Jan 11, 2014.

  1. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,483
    Little gift if you want to use it. :p

    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;

    Code (csharp):
    1.  
    2. /// <summary>
    3. /// This EditorWindow can recieve and send Modal inputs.
    4. /// </summary>
    5. public interface IModal
    6. {
    7.     /// <summary>
    8.     /// Called when the Modal shortcut is pressed.
    9.     /// The implementation should call Create if the condition are right.
    10.     /// </summary>
    11.     void ModalRequest(bool shift);
    12.  
    13.     /// <summary>
    14.     /// Called when the associated modal is closed.
    15.     /// </summary>
    16.     void ModalClosed(ModalWindow window);
    17. }
    18.  
    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;

    Code (csharp):
    1.  
    2. public enum WindowResult
    3. {
    4.     None,
    5.     Ok,
    6.     Cancel,
    7.     Invalid,
    8.     LostFocus
    9. }
    10.  
    Fairly straight forward result of a window.

    Next!

    Code (csharp):
    1.  
    2. /// <summary>
    3. /// Define a popup window that return a result.
    4. /// Base class for IModal call implementation.
    5. /// </summary>
    6. public abstract class ModalWindow : EditorWindow
    7. {
    8.     public const float TITLEBAR = 18;
    9.  
    10.     protected IModal owner;
    11.  
    12.     protected string title = "ModalWindow";
    13.  
    14.     protected WindowResult result = WindowResult.None;
    15.  
    16.     public WindowResult Result
    17.     {
    18.         get { return result; }
    19.     }
    20.  
    21.     protected virtual void OnLostFocus()
    22.     {
    23.         result = WindowResult.LostFocus;
    24.  
    25.         if (owner != null)
    26.             owner.ModalClosed(this);
    27.     }
    28.  
    29.     protected virtual void Cancel()
    30.     {
    31.         result = WindowResult.Cancel;
    32.  
    33.         if (owner != null)
    34.             owner.ModalClosed(this);
    35.  
    36.         Close();
    37.     }
    38.  
    39.     protected virtual void Ok()
    40.     {
    41.         result = WindowResult.Ok;
    42.  
    43.         if (owner != null)
    44.             owner.ModalClosed(this);
    45.  
    46.         Close();
    47.     }
    48.  
    49.     private void OnGUI()
    50.     {
    51.         GUILayout.BeginArea(new Rect(0, 0, position.width, position.height));
    52.         GUILayout.BeginHorizontal(EditorStyles.toolbar);
    53.  
    54.         GUILayout.Label(title);
    55.  
    56.         GUILayout.EndHorizontal();
    57.         GUILayout.EndArea();
    58.  
    59.         Rect content = new Rect(0, TITLEBAR, position.width, position.height - TITLEBAR);
    60.         Draw(content);
    61.     }
    62.  
    63.     protected abstract void Draw(Rect region);
    64. }
    65.  
    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.

    Code (csharp):
    1.  
    2. /// <summary>
    3. /// The rename popup is a generic popup that allow the user to input a name or to rename an existing one.
    4. /// You can pass a delegate to valide the currently input string.
    5. /// </summary>
    6. public class Rename : ModalWindow
    7. {
    8.     public delegate bool ValidateName(string name);
    9.  
    10.     public const float BUTTONS_HEIGHT = 30;
    11.  
    12.     public const float FIELD_HEIGHT = 20;
    13.  
    14.     public const float HEIGHT = 56;
    15.     public const float WIDTH = 250;
    16.  
    17.     private static Texture cross;
    18.  
    19.     public static Texture Cross
    20.     {
    21.         get
    22.         {
    23.             if (cross == null)
    24.                 cross = Helper.Load(EditorResources.Cross);
    25.  
    26.             return cross;
    27.         }
    28.     }
    29.  
    30.     private static Texture check;
    31.  
    32.     public static Texture Check
    33.     {
    34.         get
    35.         {
    36.             if (check == null)
    37.                 check = Helper.Load(EditorResources.Check);
    38.  
    39.             return check;
    40.         }
    41.     }
    42.  
    43.     private string[] labels;
    44.  
    45.     private string[] texts;
    46.  
    47.     public string[] Texts
    48.     {
    49.         get { return texts; }
    50.     }
    51.  
    52.     private ValidateName[] validate;
    53.  
    54.     public static Rename Create(IModal owner, string title, string[] labels, string[] texts, Vector2 position)
    55.     {
    56.         return Create(owner, title, labels, texts, position, null);
    57.     }
    58.  
    59.     public static Rename Create(IModal owner, string title, string[] labels, string[] texts, Vector2 position, ValidateName[] validate)
    60.     {
    61.         Rename rename = Rename.CreateInstance<Rename>();
    62.  
    63.         rename.owner = owner;
    64.         rename.title = title;
    65.         rename.labels = labels;
    66.         rename.texts = texts;
    67.         rename.validate = validate;
    68.  
    69.         float halfWidth = WIDTH / 2;
    70.  
    71.         float x = position.x - halfWidth;
    72.         float y = position.y;
    73.  
    74.         float height = HEIGHT + (labels.Length * FIELD_HEIGHT);
    75.  
    76.         Rect rect = new Rect(x, y, 0, 0);
    77.         rename.position = rect;
    78.         rename.ShowAsDropDown(rect, new Vector2(WIDTH, height));
    79.  
    80.         return rename;
    81.     }
    82.  
    83.     protected override void Draw(Rect region)
    84.     {
    85.         bool valid = true;
    86.  
    87.         if (validate != null)
    88.         {
    89.             for (int i = 0; i < validate.Length; i++)
    90.             {
    91.                 if (validate[i] != null  !validate[i](texts[i]))
    92.                 {
    93.                     valid = false;
    94.                     break;
    95.                 }
    96.             }
    97.         }
    98.  
    99.         if (Event.current.type == EventType.KeyDown)
    100.         {
    101.             if (Event.current.keyCode == KeyCode.Return  valid)
    102.                 Ok();
    103.  
    104.             if (Event.current.keyCode == KeyCode.Escape)
    105.                 Cancel();
    106.         }
    107.  
    108.         GUILayout.BeginArea(region);
    109.  
    110.         GUILayout.Space(5);
    111.  
    112.         for (int i = 0; i < texts.Length; i++)
    113.         {
    114.             GUILayout.BeginHorizontal();
    115.  
    116.             if (validate != null  validate[i] != null)
    117.                 valid = validate[i](texts[i]);
    118.  
    119.             if (valid)
    120.             {
    121.                 GUI.color = Color.green;
    122.                 GUILayout.Label(new GUIContent(Check), GUILayout.Width(18), GUILayout.Height(18));
    123.             }
    124.             else
    125.             {
    126.                 valid = false;
    127.                 GUI.color = Color.red;
    128.                 GUILayout.Label(new GUIContent(Cross), GUILayout.Width(18), GUILayout.Height(18));
    129.             }
    130.  
    131.             GUI.color = Color.white;
    132.             texts[i] = EditorGUILayout.TextField(texts[i]);
    133.  
    134.             GUILayout.EndHorizontal();
    135.         }
    136.  
    137.         GUILayout.Space(5);
    138.  
    139.         GUILayout.BeginHorizontal();
    140.  
    141.         GUI.enabled = valid;
    142.  
    143.         if (GUILayout.Button("Ok"))
    144.             Ok();
    145.  
    146.         GUI.enabled = true;
    147.  
    148.         if (GUILayout.Button("Cancel"))
    149.             Cancel();
    150.  
    151.         GUILayout.EndHorizontal();
    152.         GUILayout.EndArea();
    153.     }
    154. }
    155.  
    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;

    Code (csharp):
    1.  
    2. public class RenameObject : EditorWindow, IModal
    3. {
    4.     private string text = "My Text";
    5.  
    6.     [MenuItem("Window/Test Rename")]
    7.     static void Init()
    8.     {
    9.         RenameObject.GetWindow<RenameObject>();
    10.     }
    11.  
    12.     public void ModalRequest(bool shift)
    13.     {
    14.         // Modal Rename doesn't have a MenuItem implementation. So this method is not used.
    15.     }
    16.  
    17.     public void ModalClosed(ModalWindow window)
    18.     {
    19.         Rename rename = window as Rename;
    20.  
    21.         if (rename == null || window.Result != WindowResult.Ok)
    22.             return;
    23.  
    24.         text = rename.Texts[0];
    25.         Repaint();
    26.     }
    27.  
    28.     private void OnGUI()
    29.     {
    30.         EditorGUILayout.LabelField(text);
    31.  
    32.         if (GUILayout.Button("Edit Text"))
    33.             CreateModal();
    34.     }
    35.  
    36.     private void CreateModal()
    37.     {
    38.         Rename.Create(this, "Rename My Text!", new string[] { "Text: " },
    39.             new string[] { text }, GUIUtility.GUIToScreenPoint(Event.current.mousePosition));
    40.     }
    41. }
    42.  
    The result is;

    $s3b133D.png

    $1ZsYhyA.png

    $2H6rz4m.png

    $fcCNjzP.png

    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. :p
     
    Last edited: Jan 11, 2014
    Mauri, holdingjupiter and Gustave like this.
  2. Gustave

    Gustave

    Joined:
    Nov 16, 2012
    Posts:
    1
    Works like a charm thanks. :D
     
  3. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,483
    Happy someone got to use it... even if it's 8 months later. ;)
     
  4. Orion

    Orion

    Joined:
    Mar 31, 2008
    Posts:
    207
    Great job, thanks!

    (hint for others: there are a few errors showing, because of missing & operators)
     
  5. holdingjupiter

    holdingjupiter

    Joined:
    Oct 27, 2014
    Posts:
    20
  6. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,483
    Then you've seen nothing. :p
     
    holdingjupiter likes this.
  7. HonorableDaniel

    HonorableDaniel

    Joined:
    Feb 28, 2007
    Posts:
    2,812
    So it's only modal in terms of the base window, it doesn't block the whole Unity editor I take it?
     
  8. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,483
    Yup, you can't block the whole editor without it doing some crazy things. Probably because lot of it is multi-threaded and assume the main thread is never blocked.

    This is the next best thing; a window that act like a modal one, but if you click or interact with anything outside it is dismissed.
     
  9. holdingjupiter

    holdingjupiter

    Joined:
    Oct 27, 2014
    Posts:
    20
    I ended up using this

    EditorWindow.FocusWindowIfItsOpen<namingWindow>();

    on the window('naming the dialogue asset' window), and then throwing .enabled bools for all the nodes and the main window on the "save" and "finish saving" buttons. My one bug I've found and not solved is I have to close the window(start the new dialogue over) if I click anywhere else but the editor window. It doesn't make all of unity unselectable, but it works for the editor window itself, which is enough for me.
     
  10. GearKlik

    GearKlik

    Joined:
    Sep 21, 2015
    Posts:
    58
    This will give you EXACTLY the same functionality as the built in Unity modal dialogs. Unfortunately is also gives you the same bug where Unity's focus is lost when the dialog is closed. I quickly tried to set the focus of the main window and failed. I left the code in in case someone has time to work it out.

    Code (CSharp):
    1.  
    2. public class EditorWindowModal : EditorWindow
    3. {
    4.  
    5.  
    6.     [MenuItem("Tools/Test Modal Editor Window")]
    7.     private static void showWindow()
    8.     {      
    9.         EditorWindow.CreateInstance<EditorWindowModal>().showModal();
    10.     }
    11.  
    12.     void OnEnable()
    13.     {
    14.  
    15.     }
    16.  
    17.     void OnDisable()
    18.     {
    19.         //setMainWindowFocus();
    20.     }
    21.  
    22.     void OnGUI()
    23.     {
    24.         if (GUILayout.Button("Close")) this.Close();
    25.     }
    26.  
    27.  
    28.     void showModal()
    29.     {
    30.         MethodInfo dynShowModal = this.GetType().GetMethod("ShowModal", BindingFlags.NonPublic | BindingFlags.Instance);
    31.         dynShowModal.Invoke(this, new object[] { });
    32.     }
    33.  
    34.     // Needs more work
    35.     /*
    36.     public void setMainWindowFocus()
    37.     {
    38.         UnityEngine.Object window = null;
    39.         Type winType = typeof(UnityEditor.EditorWindow).Assembly.GetType("UnityEditor.ContainerWindow");
    40.         if (winType != null)
    41.         {
    42.             foreach (UnityEngine.Object w in Resources.FindObjectsOfTypeAll(winType))
    43.             {
    44.                 object parent = winType.InvokeMember("get_mainView", System.Reflection.BindingFlags.InvokeMethod | System.Reflection.BindingFlags.Instance, null, w, null);
    45.                 if (parent != null && parent.GetType().Name.Contains("MainWindow"))
    46.                 {
    47.                     window = w;                
    48.  
    49.                     FieldInfo _m_RootView = winType.GetField("m_RootView", BindingFlags.NonPublic | BindingFlags.Instance);
    50.                     Type viewType = typeof(UnityEditor.EditorWindow).Assembly.GetType("UnityEditor.GUIView");
    51.  
    52.                     var view = Convert.ChangeType(_m_RootView.GetValue(w), viewType);
    53.  
    54.                     MethodInfo dynFocus = viewType.GetMethod("Focus", BindingFlags.NonPublic | BindingFlags.Instance);
    55.                     dynFocus.Invoke(view, new object[] { });
    56.                  
    57.                     break;
    58.                 }
    59.             }
    60.         }
    61.         if (window == null)
    62.             Debug.LogWarning("Unable to find main window.\nMaybe you'll need to update the MainWindow constructor for your version of Unity.");
    63.  
    64.         //return window;
    65.     }
    66.     */
    67.  
    68. }
    69.  
     
    Kroc_ and Xarbrough like this.
  11. richardkettlewell

    richardkettlewell

    Unity Technologies

    Joined:
    Sep 9, 2015
    Posts:
    1,152
  12. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,483
  13. richardkettlewell

    richardkettlewell

    Unity Technologies

    Joined:
    Sep 9, 2015
    Posts:
    1,152
    Warning: I just used it and doesn't seem to be as "Modal" as i would have liked... but anyway, it's good to have it linked from this thread, for other readers :)
     
  14. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,483
    By the way, if you meet whoever thought CSS and XML was a good idea in UIElement, bitch slap him for me will you?

     
  15. elmar1028

    elmar1028

    Joined:
    Nov 21, 2013
    Posts:
    2,175
    I personally like it because it finally lets you abstract view from backend, which scales well in larger projects. WPF is one of the UI Frameworks which uses XML (or XAML) and it's quite nice.
     
    richardkettlewell likes this.
  16. HarryCodder

    HarryCodder

    Joined:
    Feb 20, 2015
    Posts:
    68
    As far as I can tell, ShowModalUtility doesn't work at all like it's supposed to (according to the documentation at least).
    And this issue on the tracker seems to say that Unity doesn't care.
    So either there is a way of making it work outside of a simple
    Code (CSharp):
    1. var window = CreateInstance<>();
    2. window.ShowModalUtility();
    or this method should just be removed (although it is deeply needed).
     
    Orion likes this.