Search Unity

ReorderableList in the custom EditorWindow

Discussion in 'Immediate Mode GUI (IMGUI)' started by RIw, Feb 4, 2016.

  1. RIw

    RIw

    Joined:
    Jan 2, 2015
    Posts:
    90
    Hello!
    I'm trying to write a simple Inventory System, but I have a problem with ReorderableList which I want to implement in EditorWindow (not in the Editor).
    I read the article aboud ReorderableLists from this article: http://va.lent.in/unity-make-your-lists-functional-with-reorderablelist/

    and I know that instead of OnInspectorGUI function, I have OnGUI method, but when I'm implementing this, I'm able to see only the empty ReorderableList in my window (even if data source for list isn't empty),when I click this, the list disappears and I'm getting the hundreds of NullReferenceExceptions.
    I would be very grateful if someone could give me a simple code snippet about using those lists in the EditorWindows (Additionaly I need the drawElementcallback)
     
    Last edited: Feb 4, 2016
  2. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    That article has a complete, working example. There's a pastebin link at the bottom with the full source code. In the article, and in the source code, you'll see that you need to initialize the list first, usually in an OnEnable method.
     
  3. RIw

    RIw

    Joined:
    Jan 2, 2015
    Posts:
    90
    @TonyLi
    Yes yes,but as I said earlier, this article is only about the Editor, but I need an example on EditorWindow.
    I have solved a half of problem,because Now I'm displaying in my window the ReorderableList properly, I can even add and remove Items, but When I will close and open again my window, I'm getting NullReferenceException, and the list disappears, but when I reload the Unity, the list (and the Items) looks just like I want.

    The NullReferenceException is at this command:
    Code (CSharp):
    1. list.DoLayoutList();
     
    Last edited: Feb 5, 2016
  4. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    In your OnEnable method, you still need to initialize your list and assign callback methods. The article author uses anonymous methods, but you can just as easily assign named methods. You can use a static variable to keep track of the current selection, or check Selection.

    For example, say your editor window works on the currently-selected GameObject, and it edits a list inside a script named Inventory:
    Code (csharp):
    1. public class Inventory : MonoBehaviour {
    2.     public List<Item> items;
    You need an OnEnable like this example:
    Code (csharp):
    1. ReorderableList list = null;
    2.  
    3. void OnEnable() {
    4.     var inventory = (Selection.activeGameObject != null) ? Selection.activeGameObject.GetComponent<Inventory>() : null;
    5.     if (inventory != null) {
    6.         list = new ReorderableList(inventory.items, typeof(item), true, true, true, true);
    7.         // (Assign your callbacks here if needed.)
    8.     }
    9. }
    10.  
    11. void OnGUI() {
    12.     if (list != null) {
    13.         list.DoLayoutList();
    14.     } else {
    15.         EditorGUILayout.Label("Select a GameObject with an Inventory component.");
    16.     }
    17. }
     
  5. RIw

    RIw

    Joined:
    Jan 2, 2015
    Posts:
    90
    @TonyLi
    It doesn't work.
    Here is my complete source code of the Window:
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using UnityEditorInternal;
    4. using System.Collections;
    5. using System;
    6.  
    7.  
    8. public class InventoryEditorWindow : EditorWindow
    9. {
    10.     ItemDatabaseObject _DatabaseObject;
    11.     SerializedObject so;
    12.     public ReorderableList list = null;
    13.  
    14.     public static InventoryEditorWindow Instance { get;set; }
    15.     public static bool IsOpened
    16.     {
    17.         get { return Instance != null; }
    18.     }
    19.     public static void ShowWindow(ItemDatabaseObject _itemDatabaseObject)
    20.     {
    21.         GetWindow<InventoryEditorWindow>("InventoryEditor");
    22.         Instance._DatabaseObject = _itemDatabaseObject;
    23.     }
    24.  
    25.     void OnEnable()
    26.     {
    27.         Instance = this;
    28.             so = new SerializedObject(_DatabaseObject);
    29.             list = new ReorderableList(so, so.FindProperty("ItemList"));
    30.             list.drawElementCallback =
    31.             (Rect rect, int index, bool isActive, bool isFocused) =>
    32.             {
    33.                 var element = list.serializedProperty.GetArrayElementAtIndex(index);
    34.                     rect.y += 2;
    35.                     EditorGUI.PropertyField(
    36.                         new Rect(rect.x, rect.y, 60, EditorGUIUtility.singleLineHeight),
    37.                         element.FindPropertyRelative("Name"), GUIContent.none);
    38.             };
    39.     }
    40.  
    41.  
    42.     void OnDisable()
    43.     {
    44.     }
    45.  
    46.     void OnGUI()
    47.     {
    48.             so.Update();
    49.             list.DoLayoutList();
    50.             so.ApplyModifiedProperties();
    51.     }
    52. }
    The ItemDatabaseObject is just an ScriptableObject which is holding the Items.
    As I mentioned, the ReorderableList is looking fine, but when I close it,and then open again, It disappears until I will reload the script, but the Items of the List are saved just like I want.
     
    Last edited: Feb 5, 2016
    tokar_dev likes this.
  6. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    It's just an execution order problem. You shouldn't reference Instance in ShowWindow because it won't have been set correctly yet. It gets set afterward in OnEnable. Try something like this, which makes DatabaseObject static and sets it in ShowWindow:

    Code (csharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using UnityEditorInternal;
    4. using System.Collections;
    5. using System;
    6.  
    7.  
    8. public class InventoryEditorWindow : EditorWindow
    9. {
    10.     public static ItemDatabaseObject DatabaseObject { get; private set; }
    11.     SerializedObject so;
    12.     public ReorderableList list = null;
    13.  
    14.     public static InventoryEditorWindow Instance { get;set; }
    15.     public static bool IsOpened
    16.     {
    17.         get { return Instance != null; }
    18.     }
    19.     public static void ShowWindow(ItemDatabaseObject _itemDatabaseObject)
    20.     {
    21.         DatabaseObject = _itemDatabaseObject;
    22.         GetWindow<InventoryEditorWindow>("InventoryEditor");
    23.         //Instance._DatabaseObject = _itemDatabaseObject;
    24.     }
    25.  
    26.     void OnEnable()
    27.     {
    28.         Instance = this;
    29.             so = new SerializedObject(DatabaseObject);
    30.             list = new ReorderableList(so, so.FindProperty("ItemList"));
    31.             list.drawElementCallback =
    32.             (Rect rect, int index, bool isActive, bool isFocused) =>
    33.             {
    34.                 var element = list.serializedProperty.GetArrayElementAtIndex(index);
    35.                     rect.y += 2;
    36.                     EditorGUI.PropertyField(
    37.                         new Rect(rect.x, rect.y, 60, EditorGUIUtility.singleLineHeight),
    38.                         element.FindPropertyRelative("Name"), GUIContent.none);
    39.             };
    40.     }
    41.  
    42.  
    43.     void OnDisable()
    44.     {
    45.     }
    46.  
    47.     void OnGUI()
    48.     {
    49.             so.Update();
    50.             list.DoLayoutList();
    51.             so.ApplyModifiedProperties();
    52.     }
    53. }
     
  7. RIw

    RIw

    Joined:
    Jan 2, 2015
    Posts:
    90
    @TonyLi
    At first, your solution wasn't helpful, because after saving the code, and reloading the Unity, everything were the same, I don't know, maybe It's Visual Studio's fault, but now everything works and I hope It will work forever.
    Thank you for your help :) .
     
  8. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Happy to help! One last tip: Before using a variable, make sure it's not null. For example:

    Code (csharp):
    1. void OnGUI()
    2. {
    3.     if (so == null)
    4.     {
    5.         Debug.LogError("ScriptableObject is null");
    6.     }
    7.     else if (list == null)
    8.     {
    9.         Debug.LogError("ReorderableList is null");
    10.     }
    11.     else if (list.list == null)
    12.     {
    13.         Debug.LogError("ReorderableList points to a null list");
    14.     }
    15.     else
    16.     {
    17.         so.Update();
    18.         list.DoLayoutList();
    19.         so.ApplyModifiedProperties();
    20.     }
    21. }
     
    RIw likes this.
  9. RIw

    RIw

    Joined:
    Jan 2, 2015
    Posts:
    90
    @TonyLi
    I'm sorry but I have to ask another question, but also about ReorderableLists.
    I want to display in ReorderableList fields of ItemProperty, which has the Item Selected in the another ReorderableList.

    I'm initializing the new ReorderableList like this:
    Code (CSharp):
    1.  
    2.         itemDataPropertyList = new ReorderableList(EditedItem.Properties, typeof(ItemProperty), true, true, true, true);
    3.  
    But I'm getting the NullReferenceExceptions (again, I really hate them), and the ReorderableList doesn't display.
    I also need to Reinitialize the ReorderableList, but you said that I have to do this in OnEnable();
    (EditedItem is null at the beginning, but It's getting a value when I will select some item from another ReorderableList)
     
  10. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    How about if, in OnGUI, you check if EditedItems.Properties has changed. If so, reinitialize itemDataPropertyList. Something like this:

    Code (csharp):
    1. void OnGUI()
    2. {
    3.     // (Omitting null checks from my previous post for easier reading)
    4.     if (itemDataPropertyList == null || itemDataPropertyList.list != EditedItem.Properties)
    5.     {
    6.         itemDataPropertyList = new ReorderableList(EditedItem.Properties, typeof(ItemProperty), true, true, true, true);
    7.     }
    8.     itemDataPropertyList.DoLayoutList();
    9. }
     
  11. RIw

    RIw

    Joined:
    Jan 2, 2015
    Posts:
    90
    @TonyLi
    Ok, and the callbacks as usual in the OnEnable ?
     
  12. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    I usually bundle them together in a method. So it might look something like this:

    Code (csharp):
    1. void OnGUI()
    2. {
    3.     PrepareList();
    4.     itemDataPropertyList.DoLayoutList();
    5. }
    6.  
    7. void PrepareList()
    8. {
    9.     if (itemDataPropertyList == null || itemDataPropertyList.list != EditedItem.Properties)
    10.     {
    11.         itemDataPropertyList = new ReorderableList(EditedItem.Properties, typeof(ItemProperty), true, true, true, true);
    12.         itemDataPropertList.OnWhateverCallback = blahblahblah; //etc.
    13.     }
    14. }
     
    RIw likes this.
  13. RIw

    RIw

    Joined:
    Jan 2, 2015
    Posts:
    90
    @TonyLi thanks,
    I'm very grateful.
     
  14. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Glad I could help!
     
    RIw likes this.
  15. RIw

    RIw

    Joined:
    Jan 2, 2015
    Posts:
    90
    @TonyLi
    Ummm...I don't know why but your last sample isn't working properly.
    Here:
    Code (CSharp):
    1. void OnGUI()
    2. {
    3.     PrepareList();
    4.     itemDataPropertyList.DoLayoutList();
    5. }
    6. void PrepareList()
    7. {
    8.     if (itemDataPropertyList == null || itemDataPropertyList.list != EditedItem.Properties)
    9.     {
    10.         itemDataPropertyList = new ReorderableList(EditedItem.Properties, typeof(ItemProperty), true, true, true, true);
    11.         itemDataPropertList.OnWhateverCallback = blahblahblah; //etc.
    12.     }
    13. }
    itemDataPropertyList.serializedProperty is null :(
     
  16. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Try using an additional variable to keep track of which list is assigned to itemDataPropertyList. For example:
    Code (csharp):
    1. private List<ItemProperty> currentList = null;
    2.  
    3. void OnGUI()
    4. {
    5.     PrepareList();
    6.     itemDataPropertyList.DoLayoutList();
    7. }
    8.  
    9. void PrepareList()
    10. {
    11.     if (EditedItem.Properties != currentList)
    12.     {
    13.         currentList = EditedItem.Properties;
    14.         itemDataPropertyList = new ReorderableList(currentList, typeof(ItemProperty), true, true, true, true);
    15.         itemDataPropertList.OnWhateverCallback = blahblahblah; //etc.
    16.     }
    17. }
     
  17. RIw

    RIw

    Joined:
    Jan 2, 2015
    Posts:
    90
    @TonyLi
    Nothing changes :(
    It's throwing an error on this line:
    Code (CSharp):
    1. var element = itemDataPropertyList.serializedProperty.GetArrayElementAtIndex(index);
    2.  
     
  18. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Maybe check that the index is valid first:
    Code (csharp):
    1. if (0 <= itemDataPropertyList.index && itemDataPropertyList.index < itemDataPropertyList.count)
    2. {
    3.     var element = itemDataPropertyList.serializedProperty.GetArrayElementAtIndex(index);
    4. }
     
  19. RIw

    RIw

    Joined:
    Jan 2, 2015
    Posts:
    90
    @TonyLi
    Now List is displaying but I can't see the Fields, which I'm drawing in the drawElementCallback
     
    Last edited: Feb 11, 2016
  20. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    It sounds like itemDataPropertyList isn't getting set properly then.
     
  21. RIw

    RIw

    Joined:
    Jan 2, 2015
    Posts:
    90
    @TonyLi

    I think i'm initializing it properly:
    Code (CSharp):
    1. itemDataPropertyList = new ReorderableList(EditedItem.Properties,typeof(ItemProperty),true,true,true,true);
    And here's ItemProperty:
    Code (CSharp):
    1. [Serializable]
    2. public class ItemProperty
    3. {
    4. public string PropertyName;
    5. public string PropertyValue;
    6. }
     
  22. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Whenever EditedItem.Properties changes, does your editor script create a new ReorderableList?
     
  23. RIw

    RIw

    Joined:
    Jan 2, 2015
    Posts:
    90
    @TonyLi
    Yeah, but the result is looking like before that. :(
     

    Attached Files:

  24. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    @Rlw - Sorry, I missed the alert about your reply! Here's an example I put together. Hopefully it'll help.

    I used this test class for the ScriptableObject:

    Code (csharp):
    1. using UnityEngine;
    2. using System;
    3. using System.Collections.Generic;
    4.  
    5. public class ItemDatabaseObject : ScriptableObject
    6. {
    7.  
    8.     [Serializable]
    9.     public class Property
    10.     {
    11.         public string name = string.Empty;
    12.         public string value = string.Empty;
    13.     }
    14.  
    15.     [Serializable]
    16.     public class Item
    17.     {
    18.         public List<Property> properties = new List<Property>();
    19.     }
    20.  
    21.     public List<Item> itemList = new List<Item>();
    22. }
    And I used this editor script:

    Code (csharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using UnityEditorInternal;
    4. using System.IO;
    5.  
    6.  
    7. public class InventoryEditorWindow : EditorWindow
    8. {
    9.  
    10.     private ReorderableList m_itemList = null;
    11.     private ReorderableList m_propertiesList = null;
    12.     private SerializedObject so = null;
    13.  
    14.  
    15.     [MenuItem("Window/ItemObjectDatabase Editor")]
    16.     public static void OpenWindow()
    17.     {
    18.         ShowWindow(Selection.activeObject as ItemDatabaseObject);
    19.     }
    20.  
    21.     public static void ShowWindow(ItemDatabaseObject itemDatabaseObject)
    22.     {
    23.         var window = GetWindow<InventoryEditorWindow>("InventoryEditor");
    24.         window.SelectDatabaseObject(itemDatabaseObject);
    25.     }
    26.  
    27.     void OnSelectionChange()
    28.     {
    29.         var itemDatabaseObject = Selection.activeObject as ItemDatabaseObject;
    30.         if (itemDatabaseObject != null) SelectDatabaseObject(itemDatabaseObject);
    31.     }
    32.  
    33.     void SelectDatabaseObject(ItemDatabaseObject itemDatabaseObject)
    34.     {
    35.         if (itemDatabaseObject == null)
    36.         {
    37.             m_itemList = null;
    38.             so = null;
    39.         }
    40.         else
    41.         {
    42.             m_itemList = new ReorderableList(itemDatabaseObject.itemList, typeof(ItemDatabaseObject.Item), true, true, true, true);
    43.             m_itemList.onAddCallback += AddItem;
    44.             m_itemList.onRemoveCallback += RemoveItem;
    45.             m_itemList.onSelectCallback += SelectItem;
    46.             m_itemList.drawHeaderCallback = (Rect rect) => { EditorGUI.LabelField(rect, "Items"); };
    47.             so = new SerializedObject(itemDatabaseObject);
    48.         }
    49.         m_propertiesList = null;
    50.         Repaint();
    51.     }
    52.  
    53.     void OnGUI()
    54.     {
    55.         if (so != null && m_itemList != null)
    56.         {
    57.             so.Update();
    58.             m_itemList.DoLayoutList();
    59.             if (m_propertiesList != null)
    60.             {
    61.                 m_propertiesList.DoLayoutList();
    62.             }
    63.             so.ApplyModifiedProperties();
    64.         }
    65.     }
    66.  
    67.     void AddItem(ReorderableList itemList)
    68.     {
    69.         // When we add an item, select that item:
    70.         itemList.list.Add(new ItemDatabaseObject.Item());
    71.         itemList.index = itemList.count - 1;
    72.         SelectItem(itemList);
    73.     }
    74.  
    75.     void RemoveItem(ReorderableList itemList)
    76.     {
    77.         // When we remove an item, clear the properties list:
    78.         ReorderableList.defaultBehaviours.DoRemoveButton(itemList);
    79.         m_propertiesList = null;
    80.         Repaint();
    81.     }
    82.  
    83.     void SelectItem(ReorderableList itemList)
    84.     {
    85.         // We when select an item, init the properties list for that item:
    86.         if (0 <= itemList.index && itemList.index < itemList.count)
    87.         {
    88.             var item = itemList.list[itemList.index] as ItemDatabaseObject.Item;
    89.             if (item != null)
    90.             {
    91.                 m_propertiesList = new ReorderableList(item.properties, typeof(string), true, true, true, true);
    92.                 m_propertiesList.drawElementCallback = DrawProperty;
    93.             }
    94.             Repaint();
    95.         }
    96.     }
    97.  
    98.     void DrawProperty(Rect rect, int index, bool isActive, bool isFocused)
    99.     {
    100.         // Added tons of debugging to help if you have issues:
    101.         var itemListSerializedProperty = so.FindProperty("itemList");
    102.         if (itemListSerializedProperty == null) { Debug.Log("itemList is null!"); return; }
    103.         if (!itemListSerializedProperty.isArray) { Debug.Log("itemList is not an array!"); return; }
    104.         if (!(0 <= m_itemList.index && m_itemList.index < itemListSerializedProperty.arraySize)) { Debug.Log("itemList[" + m_itemList.index + "] is outside array bounds!"); return; }
    105.         if (0 <= m_itemList.index && m_itemList.index < itemListSerializedProperty.arraySize)
    106.         {
    107.             var itemSerializedProperty = itemListSerializedProperty.GetArrayElementAtIndex(m_itemList.index);
    108.             if (itemSerializedProperty == null) { Debug.Log("itemSerializedProperty[" + m_itemList.index + "] is null!"); return; }
    109.  
    110.             var propertiesListSerializedProperty = itemSerializedProperty.FindPropertyRelative("properties");
    111.             if (propertiesListSerializedProperty == null) { Debug.Log("propertiesListSerializedProperty is null!"); return; }
    112.             if (!propertiesListSerializedProperty.isArray) { Debug.Log("propertiesListSerializedProperty is not an array!"); return; }
    113.  
    114.             if (0 <= index && index < propertiesListSerializedProperty.arraySize)
    115.             {
    116.                 var propertySerializedProperty = propertiesListSerializedProperty.GetArrayElementAtIndex(index);
    117.                 if (propertySerializedProperty == null) { Debug.Log("propertySerializedProperty[" + index + "] is null!"); return; }
    118.  
    119.                 // If you have a custom property drawer, you can use PropertyField:
    120.                 //---EditorGUI.PropertyField(rect, propertySerializedProperty);
    121.  
    122.                 // I didn't bother with one, so I just use TextField:
    123.                 propertySerializedProperty.FindPropertyRelative("name").stringValue =
    124.                     EditorGUI.TextField(new Rect(rect.x, rect.y, rect.width / 2, rect.height),
    125.                     propertySerializedProperty.FindPropertyRelative("name").stringValue);
    126.                 propertySerializedProperty.FindPropertyRelative("value").stringValue =
    127.                     EditorGUI.TextField(new Rect(rect.x + rect.width / 2, rect.y, rect.width / 2, rect.height),
    128.                     propertySerializedProperty.FindPropertyRelative("value").stringValue);
    129.             }
    130.         }
    131.     }
    132.  
    133.     [MenuItem("Assets/Create/ItemObjectDatabase")]
    134.     public static void CreateAsset()
    135.     {
    136.         CreateAsset<ItemDatabaseObject>();
    137.     }
    138.  
    139.     public static void CreateAsset<T>() where T : ScriptableObject
    140.     {
    141.         T asset = ScriptableObject.CreateInstance<T>();
    142.         string path = AssetDatabase.GetAssetPath(Selection.activeObject);
    143.         if (path == "")
    144.         {
    145.             path = "Assets";
    146.         }
    147.         else if (Path.GetExtension(path) != "")
    148.         {
    149.             path = path.Replace(Path.GetFileName(AssetDatabase.GetAssetPath(Selection.activeObject)), "");
    150.         }
    151.         string assetPathAndName = AssetDatabase.GenerateUniqueAssetPath(path + "/New " + typeof(T).ToString() + ".asset");
    152.         AssetDatabase.CreateAsset(asset, assetPathAndName);
    153.         AssetDatabase.SaveAssets();
    154.         AssetDatabase.Refresh();
    155.         EditorUtility.FocusProjectWindow();
    156.         Selection.activeObject = asset;
    157.     }
    158. }
    159.  
     
    creepteks, G8S and AntFitch like this.