Search Unity

Simple dynamic list editor

Discussion in 'Immediate Mode GUI (IMGUI)' started by unimechanic, Jun 28, 2013.

  1. unimechanic

    unimechanic

    Joined:
    Jan 9, 2013
    Posts:
    155
    Many people have had problems with GUI.SetNextControlName() and dynamic GUI's. Here is a simple example on how to create a list editor, that allows adding, modifying and deleting items with the keyboard:

    Code (csharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using System.Collections.Generic;
    4.  
    5. public class ListEditor : EditorWindow {
    6.    
    7.     List<string> values;
    8.    
    9.     string editingValue;
    10.     string lastFocusedControl;
    11.    
    12.     [MenuItem("Window/List Editor")]
    13.     static void ShowEditor() {
    14.         ListEditor editor = EditorWindow.GetWindow<ListEditor>();  
    15.         editor.Init();
    16.     }
    17.    
    18.     void Init() {
    19.         values = new List<string>();   
    20.     }
    21.    
    22.     void OnGUI() {
    23.         EditorGUILayout.HelpBox("Simple dynamic list editor.\nPress Enter to apply field changes.", MessageType.Info);
    24.         List<string> editedValues = new List<string>();
    25.         string newValue;
    26.        
    27.         foreach (string val in values) {
    28.             newValue = val;
    29.            
    30.             if (ShowField("field " + val, ref newValue)) {
    31.                 if (string.IsNullOrEmpty(newValue))
    32.                     continue;
    33.                
    34.                 if (values.IndexOf(newValue) >= 0)
    35.                     newValue = val;
    36.             }
    37.            
    38.             editedValues.Add(newValue);
    39.         }
    40.        
    41.         newValue = "";
    42.        
    43.         if (ShowField("new field", ref newValue)) {
    44.             if (!string.IsNullOrEmpty(newValue)  values.IndexOf(newValue) < 0)
    45.                 editedValues.Add(newValue);
    46.         }
    47.        
    48.         values = editedValues;
    49.     }
    50.    
    51.     bool ShowField(string name, ref string val) {
    52.         GUI.SetNextControlName(name);
    53.        
    54.         if (GUI.GetNameOfFocusedControl() != name) {
    55.            
    56.             if (Event.current.type == EventType.Repaint  string.IsNullOrEmpty(val)) {
    57.                 GUIStyle style = new GUIStyle(GUI.skin.textField);
    58.                 style.normal.textColor = new Color(0.5f, 0.5f, 0.5f, 0.75f);
    59.                 EditorGUILayout.TextField("Enter a new item", style);
    60.             }
    61.             else
    62.                 EditorGUILayout.TextField(val);
    63.            
    64.             return false;
    65.         }
    66.  
    67. //Debug.Log("Focusing " + GUI.GetNameOfFocusedControl());   // Uncomment to show which control has focus.
    68.  
    69.         if (lastFocusedControl != name) {
    70.             lastFocusedControl = name;
    71.             editingValue = val;
    72.         }
    73.        
    74.         bool applying = false;
    75.        
    76.         if (Event.current.isKey) {
    77.             switch (Event.current.keyCode) {
    78.                 case KeyCode.Return:
    79.                 case KeyCode.KeypadEnter:
    80.                 val = editingValue;
    81.                 applying = true;
    82.                 Event.current.Use();    // Ignore event, otherwise there will be control name conflicts!
    83.                 break;
    84.             }
    85.         }
    86.        
    87.         editingValue = EditorGUILayout.TextField(editingValue);    
    88.         return applying;
    89.     }
    90. }
    91.  
    Add to Assets/Editor folder as "ListEditor.cs".

    UPDATE: Added a text hint in the TextField.

    UPDATE: An image of the editor extension:

    $ListEditor.png
     
    Last edited: Jul 9, 2013
  2. Patico

    Patico

    Joined:
    May 21, 2013
    Posts:
    886
    Thanks a lot!
     
  3. unimechanic

    unimechanic

    Joined:
    Jan 9, 2013
    Posts:
    155
    You are welcome :)

    I just updated the example code with a text hint for the TextField, that will show "Enter a new item" and disappear when the field is focused.
     
  4. Terikon

    Terikon

    Joined:
    Oct 12, 2012
    Posts:
    6
    Let's say I wish to make this list a little more advanced. I would like to provide functionality for clearing the list on press of a button.

    Actually, I'm facing serious problem with this functionality.

    See yourself. I will be very grateful if you will explain me how to achieve this, or fix bug in Unity that prevents such possibility for me.
    That change that I would do for clearing the list looks simple:
    Code (csharp):
    1. EditorGUILayout.HelpBox("Simple dynamic list editor.\nPress Enter to apply field changes.", MessageType.Info);
    2.  
    3. if (GUILayout.Button("Clear"))
    4. {
    5.     values.Clear();
    6. }
    7.  
    8. List<string> editedValues = new List<string>();
    I just add a button that cleans values when pressed. This code doesn't work. It destroys the whole dynamic list you presented - once cleared, you will not be able to add no more items to the list. Try running it yourself.

    There's unresolved problem discussed in this thread: http://answers.unity3d.com/questions/26033/problem-using-guigetnameoffocusedcontrol-with-dyna.html . For me, because of this problem, my little wish turned out impossible to implement.

    I have a project where I build a list, and sometimes I refresh it (by clearing). It stops working as soon as refresh happens, in very same way as in your modified example. And this was not fixed in Unity since version 3.

    Can anybody give a clue?
     
  5. Terikon

    Terikon

    Joined:
    Oct 12, 2012
    Posts:
    6
    Ok, took me a lot of time, but I found the solution.
    First, the problem with dynamic list that presented here (from my experience) is that it is dynamic in limited way.
    In simple scenario described here the add/remove functionality works, but if you'd wish to clear the list, the list will be broken. Problems might occur if you'll try to access controls by their names (with GUI.GetNameOfFocusedControl method).

    Here what you can do to make this list dynamic and extendable (good not only for tutorial but also for real project):

    1. Don't modify the list in OnGUI method. As far as I understand, this is advised by Unity documentation. Don't modify on OnGUI even if user presses a button.
    2. OnGUI is being called several times for each frame update, for different reasons. For repaint, for layout, for mouse/keyboard events. Looks like you need to keep drawn gui elements persistent for each call.
    3. Modify the list in Update method. Just reroute the execution to that method, using boolean flags or some more maintainable method.

    What I've done to enable button that clears all the elements as follows. For me, this looks like very common method to use GUI class. I hope some practical advice of this kind will be added to Unity documentation.

    1. Add some queue of actions as member of your MonoBehaviour:
    Code (csharp):
    1. Queue<Action> guiEvents = new Queue<Action>();
    2. Each time you perform check for button press inside OnGUI, don't perform the operation itself. Instead, add the operation as delegate to the gui queue:
    Code (csharp):
    1. //We're inside OnGUI
    2. if (GUILayout.Button ("Clear"))
    3. {
    4.    guiEvents.Enqueue(()=>
    5.    {
    6.       //your usual code goes here
    7.       values.Clear();
    8.    });
    9. }
    10.  
    3. In Update method, process the queue:
    Code (csharp):
    1. //We're inside Update
    2. while (guiEvents.Count>0)
    3. {
    4.    guiEvents.Dequeue().Invoke();
    5. }
    You need doing the same for adding/deleting either, but the principle is clear.
    This technique solved me a lot of headache, and I see quiet a few people in forums who have problems with dynamic gui screens. Hope this solution will help someone.
     
  6. dkozar

    dkozar

    Joined:
    Nov 30, 2009
    Posts:
    1,410
    I've also had a huge fight with this issue.

    Since I finally resolved it, I want to share the solution with others.

    My problem started appearing when focusing dynamically created text fields - inside of the popup dialogs.

    Note that I'm not using Unity's GUI.Window but my own dialogs instead. However, it has no importance to this issue and you could just approximate the whole problem with:

    1. Creating two text fields dynamically
    2. Focusing the first text field

    $window.png

    My code is very complex, but in a nutshell it is something similar to:

    Code (csharp):
    1. void OnGUI () {
    2.  
    3.     // text field 1 (focused)
    4.     GUI.SetNextControlName("tf1");
    5.     _text1 = GUI.TextField(_rect1, _text1);
    6.     GUI.FocusControl("tf1"); // <- focusing here
    7.  
    8.     // text field 2 (NOT focused)
    9.     GUI.SetNextControlName("tf2");
    10.     _text2 = GUI.TextField(_rect2, _text2);
    11. }
    This made focus "stack" in the upper text field and I've been unable to move it to the lower field via tabbing or mouse-click.

    The funny thing is that the text selection moved to the second text field, but I was not able to edit the text.

    What I found out to be the solution is to not to call GUI.FocusControl before all the controls have been rendered.

    So I moved the line to the end of the rendering cycle:

    Code (csharp):
    1. void OnGUI () {
    2.  
    3.     // text field 1 (focused)
    4.     GUI.SetNextControlName("tf1");
    5.     _text1 = GUI.TextField(_rect1, _text1);
    6.    
    7.     // text field 2 (NOT focused)
    8.     GUI.SetNextControlName("tf2");
    9.     _text2 = GUI.TextField(_rect2, _text2);
    10.  
    11.     GUI.FocusControl("tf1"); // <- focusing handling at the end of the rendering cycle
    12. }
    This seems to have fixed the problem.

    Another thing: it's not necessary nor healthy to run the GUI.FocusControl() command in each OnGUI loop.

    You just have to call it when changing focus. For example:

    Code (csharp):
    1. private bool _focused1;
    2.  
    3. void OnGUI () {
    4.  
    5.     // text field 1 (focused)
    6.     GUI.SetNextControlName("tf1");
    7.     _text1 = GUI.TextField(_rect1, _text1);
    8.    
    9.     // text field 2 (NOT focused)
    10.     GUI.SetNextControlName("tf2");
    11.     _text2 = GUI.TextField(_rect2, _text2);
    12.  
    13.     if (!_focused1)
    14.     {
    15.         GUI.FocusControl("tf1"); // <- focusing handling ONCE at the end of the rendering cycle
    16.         _focused1 = true;
    17.     }
    18. }
    As I said, my code is pretty complex, and these 2 recepies fixed it.

    However, I didn't manage to reproduce it with a simple script.

    Here's my bug reproduction attempt that's clearly working OK (the first field is focused by default and you can change focus using buttons):

    $focus.png

    Code (csharp):
    1. using UnityEngine;
    2.  
    3. public class FocusTest : MonoBehaviour
    4. {
    5.     string _text1 = "Username";
    6.     string _text2 = "Password";
    7.  
    8.     readonly Rect _rect1 = new Rect(10, 10, 120, 20);
    9.     readonly Rect _rect2 = new Rect(10, 40, 120, 20);
    10.  
    11.     private bool _focused1;
    12.    
    13.     void OnGUI () {
    14.  
    15.         // text field 1 (focused)
    16.         GUI.SetNextControlName("tf1");
    17.         _text1 = GUI.TextField(_rect1, _text1);
    18.  
    19.         if (!_focused1)
    20.         {
    21.             GUI.FocusControl("tf1"); // <- focusing ft1 (default), it works although not at the end of the rendering cycle (?)
    22.             _focused1 = true;
    23.         }
    24.  
    25.         if (GUI.Button(new Rect(140, 10, 60, 20), "Focus"))
    26.         {
    27.             GUI.FocusControl("tf1"); // <- focusing tf2
    28.         }
    29.  
    30.         // text field 2 (NOT focused)
    31.         GUI.SetNextControlName("tf2");
    32.         _text2 = GUI.TextField(_rect2, _text2);
    33.  
    34.         if (GUI.Button(new Rect(140, 40, 60, 20), "Focus"))
    35.         {
    36.             GUI.FocusControl("tf2"); // <- focusing tf2
    37.         }
    38.     }
    39. }
    I'll post any news on the subject here.
     
  7. dkozar

    dkozar

    Joined:
    Nov 30, 2009
    Posts:
    1,410
    Style trying to do a repro.

    For now, I have the simple example which is more similar to my use-case (grid rows having text fields and a popup window rendering new text fields).

    However, something strange started to happen (different to my popup window) - the first text field in the "popup" that should be focused automatically isn't focused by default (GUI.SetFocus called at the end of the rendering cycle):

    $popup1.png

    $popup2.png

    Fields are still focusable via buttons:

    $popup3.png

    Here's my current code:

    Code (csharp):
    1. using UnityEngine;
    2.  
    3. public class FocusTest : MonoBehaviour
    4. {
    5.     string _text0 = "Dummy text";
    6.     string _text1 = "Username";
    7.     string _text2 = "Password";
    8.  
    9.     readonly Rect _rect0 = new Rect(10, 10, 120, 20);
    10.     readonly Rect _rect1 = new Rect(10, 70, 120, 20);
    11.     readonly Rect _rect2 = new Rect(10, 100, 120, 20);
    12.  
    13.     private bool _showPopup;
    14.     private bool _focused1;
    15.    
    16.     void OnGUI () {
    17.  
    18.         // dummy text field
    19.         _text0 = GUI.TextField(_rect0, _text0);
    20.  
    21.         var oldShowPopup = _showPopup;
    22.         _showPopup = GUI.Toggle(new Rect(140, 10, 60, 20), oldShowPopup, "Popup");
    23.  
    24.         if (_showPopup != oldShowPopup  _showPopup)
    25.             _focused1 = false; // re-focus first field in popup
    26.  
    27.         if (!_showPopup)
    28.             return;
    29.  
    30.         // popup with 2 text fields
    31.         GUI.Label(new Rect(10, 40, 350, 20), "========== POPUP ==========");
    32.  
    33.         // text field 1 (focused)
    34.         GUI.SetNextControlName("tf1");
    35.         _text1 = GUI.TextField(_rect1, _text1);
    36.  
    37.         if (GUI.Button(new Rect(140, 70, 60, 20), "Focus"))
    38.         {
    39.             Debug.Log("Focusing tf1");
    40.             GUI.FocusControl("tf1"); // <- focusing tf2
    41.         }
    42.  
    43.         // text field 2 (NOT focused)
    44.         GUI.SetNextControlName("tf2");
    45.         _text2 = GUI.TextField(_rect2, _text2);
    46.  
    47.         if (GUI.Button(new Rect(140, 100, 60, 20), "Focus"))
    48.         {
    49.             Debug.Log("Focusing tf2");
    50.             GUI.FocusControl("tf2"); // <- focusing tf2
    51.         }
    52.  
    53.         if (!_focused1)
    54.         {
    55.             Debug.Log("Focusing tf1");
    56.             GUI.FocusControl("tf1"); // <- focusing ft1 (default), doesn't work - although at the end of the rendering cycle
    57.             _focused1 = true;
    58.         }
    59.     }
    60. }
     
  8. darkoleptiko

    darkoleptiko

    Joined:
    Dec 2, 2013
    Posts:
    2
    When using a language IME (Input method) for example Japanese. The user starts writing some characters and the IME kicks in.
    During this mode the user types characters and converts them to ideographs (not alphabet), when the word is done, the user presses the
    Return key and the word is "committed/converted".

    Do you know if there is a way to tell if the user "committed" a word using IME or just pressed Return?

    I need this to be able to run some action when the user presses Return, but not when the user "commits/converts" a word because they might want to type more stuff before pressing Return to for example add the item to the list.