Endless Nested Lists. Tracking Location in custom inspector

So, I am writing a custom inspector (c#) and have come into a theory to handle an organizational need for the project. What I can’t figure out is how to handle it in the inspector.

The theory is to create a self-referencing grouping class. Basically, I want to make a list of a custom class. Within this custom class is another list variable of that same class. A plausibly endless nest.

Here is an example snippet (untested of course) to help display what I mean:

//main class that Unity would use
public class Designer : Monobehavior {
  public List<Organizer> folder = new List<Organizer>(); //The list that would display in inspector
}

//Organizer class used to create groups and sub-groups to organize data
public class Organizer {
  public List<Organizer> file = new List<Organizer>(); //Allow for creation of sub-groups
  public string name; //what the user names this group (or sub-group)
  public int id; //a unique ID assigned to this group only
  public Data someData;
}

//Just for the sake of complete example, throw in a Data class
public class Data {
  public List<string> phrases = new List<string>(); //quotes of some kind, say.. from an author
}

Now, in this example the users working with this script should be able to create lists upon lists upon lists. ID’s should be auto-assigned. For the sake of argument, lets assume that ID’s are based only on the current list, and are assigned at time of creation by the custom inspector to be the highest_id+1. eg - if 3 items were created in a list, and the second item was removed, the next item created would still have an id of 3 (because 0, 1, 2 would of been the first 3. removing 1 means 2 is still the highest ID. 2+1 = 3).

Lets pretend the user is a librarian. Here’s how grouping would work:

  • Create a Folder (most outer layer). Name it “Fantasy”. No data is assigned to this group, it’s strictly for organizational purposes.
  • Create a file (sub-layer of “Fantasy”). Name it “A”. Again, no data is assigned here, it’s only an organizational layer.
  • Create a file (sub-layer of “A”). Name it “Richard Adams”. Again, no data is assigned.
  • Create a file (sub-layer of “Richard Adams”). Name it “Cinema”. No data assigned.
  • Create a file (sub-layer of “Cinema”). Name it “Watership Down”. Data is assigned to this group, and we may not need additional layers.

So, in this example, the Librarian would later be able to see the main group (fantasy) and know that within that group they will find an Alphabetized group of authors that are fantasy based. They would then be able to pick the letter of the authors last name, the author they want, and then decide if they want book quotes from movies that had theatrical releases, or not. Then find the book they want, and see the list of phrases.

Potentially, they could create another group that shows what phrases made it into the movie, or whatever.

So… the problem?

The custom class is designed to edit one group at a time. For example, they could select the group “Fantasy” and be provided with options like adding a sub-group, selecting a subgroup to edit, adding data to this group, or more etc. This is done by displaying a custom GUI using the various variables of the selected group.

What I need to do is find a way to open groups, and possibly “go up one level”. If all the data displayed is based on the currently available group, how do I access the nth group in the nest and how can I determine what the level above it is?

Example snippet (again untested, and is just the example of the display code) of what the display of a group might look like:

viewFolder = EditorGUILayout.Popup(viewFolder, script.folder.ToArray());//creates a dropdown list of folders (assume viewFolder is an int declared prior to the OnInspectorGUI() function. Also assume that 'script' has been predeclared as the script the custom inspector is editing)
GUILayout.BeginVertical(EditorStyles.textField); //create a visible boxed work space that will function as our group editor area
//This is where the group variables are going to be displayed/edited
GUILayout.BeginHorizontal();
if (GUILayout.Button("Add Subgroup", EditorStyles.miniButton, GUILayout.Width(100))) {
  script.folder[viewFolder].file.Add(); //make a subgroup to this group. Assume there is a similar button to make exterior groups prior to this area in the editor 
}
GUILayout.EndHorizontal();
GUILayout.BeginVertical();
for (int i=0;i<script.folder[viewFolder].files.Count;i++) {
  //list the subgroups and allow the user to select one
  //this would be horizontal layouts and such
}
GUILayout.EndVertical();
GUILayout.EndVertical(); //End the group editor area

In this example, I didn’t fill in the for loop, just commented what kind of things would go there. I also didn’t include the ability to edit other group variables. I’m not overly concerned with those specifics.

What I’m trying to theorize is a feasible manner to allow this kind of functionality to work. viewFolder wouldn’t work as a dropdown list because the group editor box should be populated with data from whatever the currently selected group is. That could be the nth subgroup of the folder list.

I hope I properly explained my question. I’m not at all certain how to proceed. I have no specific code beyond the examples listed because I haven’t written this yet. Trying to wrap my head around how exactly to do it.

Thanks in advance for your help!

Alright!

Please understand, the working example I post below is not efficient. I’m not worrying about that in this example, just focused on posting a working solution.

I solved this using recursion. That allows me to iterate through as many child layers as necessary until I find or identify the group I am looking for and then returns that group. I had to make it so that ID’s were controlled by an external int so that no group or subgroup could have the same ID.

The Name variable is the only editable variable in this example, as no other variables were necessary for testing.

First script I called Design.cs

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Design : MonoBehaviour {
  public List<Organize> folder = new List<Organize>();
}

[System.Serializable]
public class Organize {
  public List<Organize> subfolder = new List<Organize>();
  public string name = "";
  public int id;
  public int parentID;
}

It was tiny, so I did not bother commenting it.

Second script was placed in a folder called “Editor” (in case someone doesn’t know how custom inspectors work)

Second script was called DesignInspector.cs

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Collections;

[CustomEditor(typeof(Design))]
public class DesignInspector : Editor {
  Design design; //the script we are manipulating
  public int newID = 0; //variable used to give a group or subgroup its new ID
  public int viewFolder = 0; //Index of a dropdown list used to select primary, or most outer layer, groups
  public int folderID = -1; //ID of the current group being viewed

  public void Awake() {
  	newID = EditorPrefs.GetInt("newID",0); //Retrieve last stored value or use 0 if no value was saved
  }

  public override void OnInspectorGUI() {
  	design = (Design)target; //Assign actual script
  	//=== Start of "Add" button for primary groups and dropdown list to select a primary group
  	GUILayout.BeginHorizontal();
  	  if (GUILayout.Button("Add Folder", EditorStyles.miniButton, GUILayout.Width(100))) {
        design.folder.Add(new Organize() {id=newID,parentID=-1}); //add folder
        newID++; //increment ID so that this ID doesn't get reused
      }
  	  if (design.folder.Count > 0) { //make sure we have a folder or don't display anything
  	    string[] folderNames = new string[design.folder.Count+1]; //used for dropdown menu
  	    folderNames[0] = " "; //No name for first folder. Used if currently viewing subfolder (or no folder yet selected)
  	  	for (int i=0;i<design.folder.Count;i++) {
  	  	  if (design.folder_.name != "") {folderNames[i+1] = design.folder*.name;} //given a name, use that*_

_ else {folderNames[i+1] = “Folder “+design.folder*.id;} //needs a name, give it one based on ID*
* }
bool unsetID = false;
if (viewFolder != 0) {unsetID = true;}
viewFolder = EditorGUILayout.Popup(viewFolder,folderNames,GUILayout.Width(150)); //dropdown list*
* if (viewFolder != 0) {folderID = design.folder[(viewFolder-1)].id;} //set the id we are looking for*
* if ((unsetID) && (viewFolder == 0)) {folderID = -1;}
}
GUILayout.EndHorizontal();
//=== End of “Add” button + Dropdown Section*
* //=== Start of Group Editor*
* if ((design.folder.Count > 0) && (folderID != -1)) { //If there are no folders, display nothing*
* GUILayout.BeginVertical(EditorStyles.textField);
Organize thisFolder = null;
for (int i=0;i<design.folder.Count;i++) {
thisFolder = useRecursion(design.folder); //Search for the group we want using recursion method*

* if (thisFolder != null) {
break; //We got out group*

* }
}
if (thisFolder == null) {
Debug.Log(“Error! A folder with the ID “+folderID+” was not found.”);
viewFolder = 0;
folderID = -1;
return;
}
GUILayout.BeginHorizontal();
GUILayout.Label(“Name:”, GUILayout.Width(75)); //name label*

* thisFolder.name = GUILayout.TextField(thisFolder.name); //box to set name*
* if (thisFolder.parentID != -1) {
if (GUILayout.Button(new GUIContent(”^”,“Go up on level”),EditorStyles.miniButton,GUILayout.Width(25))) {
folderID = thisFolder.parentID;
viewFolder = 0;
}
}
GUILayout.Label(new GUIContent(thisFolder.id.ToString(),“This folders unique ID”),GUILayout.Width(30));//display ID*

* GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
if (GUILayout.Button(“Add Subfolder”, EditorStyles.miniButton, GUILayout.Width(100))) {
thisFolder.subfolder.Add(new Organize() {id=newID,parentID=thisFolder.id}); //add subgroup*

* newID++;//incrememnet ID*
* }
GUILayout.EndHorizontal();
if (thisFolder.subfolder.Count > 0){ //make sure we have subfolders before we bother with display*

* for (int i=0;i<thisFolder.subfolder.Count;i++) {
GUILayout.BeginHorizontal();
if (thisFolder.subfolder.name != “”) {GUILayout.Label(thisFolder.subfolder.name,GUILayout.Width(75));}
else {GUILayout.Label("Folder "+thisFolder.subfolder.id,GUILayout.Width(75));}
GUILayout.Space(3);
if (GUILayout.Button(“Open”,EditorStyles.miniButton,GUILayout.Width(50))) {
folderID = thisFolder.subfolder.id;
viewFolder=0;
}
GUILayout.EndHorizontal();
}
}
GUILayout.EndVertical();
//=== End of Group Editor*

* }*_

* //Save the one important pref we load when the script awakens*
EditorPrefs.SetInt(“newID”,newID);
}

//Create the recursion method (function) that will be used to get groups
public Organize useRecursion(Organize parent) {
* //first check to see if this group is the group we are looking for*
* if (parent.id == folderID) {*
* return parent;*
* }*
* foreach (Organize subfolder in parent.subfolder) {*
* Organize child = useRecursion(subfolder);*
* if (child != null) {*
* return child;*
* }*
* }*
* return null;*
}
}
It was a bit bigger and harder to outright read, so I did comment it.
For those that may need to solve the issue of endless or infinitely nested Lists, please keep in mind that in this example, there is no check to see if the ID was changed, which means that it iterates through groups each frame. You also don’t need to search for an ID if you are selecting from the drop down list, because you have the index of the actual item at the root level.
What that means is, when you declare thisFolder variable, you could actually check if viewFolder is 0. If it’s not you could set thisFolder = design.folder[viewFolder-1]. Then you can check to see if thisFolder is null before running the for loop to iterate through all groups.
Hope this helps someone :slight_smile: