Custom Editor for C# Dictionary

I’ve been working towards a convenient Dictionary<> GUI handler for adding and removing handles to virtual teams, and be able to view them for debugging. So far, I can add teams, remove teams, add remove players from a team, and view players on each team individually. So far this approach is working as hoped. One of my problems is that my scroll view isn’t recognizing overflowing content. Keep in mind, this code was designed for use with a script named ‘TeamHandler.cs’

[TeamHandler.cs]

...
public Dictionary<string, List<Transform>> teams
...

[TeamHandlerEditor.cs]

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

[CustomEditor(typeof(TeamHandler))]
public class TeamHandlerEditor : Editor {
	Vector2 scrollPos = Vector2.zero;
	bool showInput = false;
	string selectedTeam = "";
	string newTeam;
	Transform newPlyr;


	public override void OnInspectorGUI () {
		TeamHandler teamhandler = (TeamHandler) target;
		GUIStyle verticalScrollbar = GUI.skin.verticalScrollbar;
		Texture2D tmp = new Texture2D(5,5);
		tmp.SetPixel(0,0, Color.white);
		verticalScrollbar.onNormal.background = tmp;
		verticalScrollbar.fixedHeight = 100;
		verticalScrollbar.fixedWidth = 0;

		// display teams as buttons
		EditorGUILayout.BeginHorizontal();
			try {
				Dictionary<string, List<Transform>>.KeyCollection keys = teamhandler.teams.Keys;
				foreach(string key in keys) {
					if (GUILayout.Button(key)) {
						selectedTeam = key;
					}
					if (GUILayout.Button("X", GUILayout.Width(20))) {
						// delete team
						selectedTeam = "";
						teamhandler.teams.Remove(key);
					}
				}
				if (GUILayout.Button("+", GUILayout.Width(20))) {
					newTeam = "";
					showInput = true;
				}
			} catch {}
		EditorGUILayout.EndHorizontal();

		// display all players within selected team
		scrollPos = EditorGUILayout.BeginScrollView(scrollPos, false, true, GUI.skin.horizontalScrollbar, GUI.skin.verticalScrollbar, verticalScrollbar, GUILayout.Width(Screen.width-40), GUILayout.Height (100));
			EditorGUILayout.BeginVertical();
				if (teamhandler.teams.ContainsKey(selectedTeam)) {
					if (teamhandler.teams[selectedTeam].Count > 0) {
						foreach(Transform plyr in teamhandler.teams[selectedTeam]) {
							try {
									EditorGUILayout.BeginHorizontal();
										EditorGUILayout.LabelField(plyr.name);
										if (GUILayout.Button("X", GUILayout.Width(20))) {
											// delete team
											teamhandler.teams[selectedTeam].Remove(plyr);
											return; // recycle
										}
								EditorGUILayout.EndHorizontal();
							} catch {
								// ignore errors
							}
						}
					}
				}
			EditorGUILayout.EndVertical();
		EditorGUILayout.EndScrollView();

		if (!teamhandler.teams.ContainsKey(selectedTeam) || showInput) {
			EditorGUILayout.BeginHorizontal();
				EditorGUILayout.LabelField("Team:",GUILayout.Width(40));
				newTeam = EditorGUILayout.TextField(newTeam);
				if (GUILayout.Button("Add")) {
					if (newTeam != "") {
						teamhandler.teams.Add(newTeam, new List<Transform>());
						selectedTeam = newTeam;
						newTeam = "";
						showInput = false;
					}
				}
				if (GUILayout.Button("Cancel")) {
					showInput = false;
				}
			EditorGUILayout.EndHorizontal();
		} else {
			EditorGUILayout.BeginHorizontal();
				EditorGUILayout.LabelField("Player:",GUILayout.Width(50));
				newPlyr = (Transform) EditorGUILayout.ObjectField(newPlyr, typeof(Transform));
				if (GUILayout.Button("Insert")) {
					if (newPlyr != null) {
						if (!teamhandler.teams.ContainsKey(selectedTeam))
							teamhandler.teams.Add(selectedTeam, new List<Transform>() { newPlyr });
						else
							teamhandler.teams[selectedTeam].Add(newPlyr);
						newPlyr = null;
					}
				}
			EditorGUILayout.EndHorizontal();
		}
		/*myTarget.teams = EditorGUILayout.IntSlider(
			"Val-you", myTarget.MyValue, 1, 10);*/
	}
}

As I said, this looks, and feels, mostly as it should. Vertical scrolling still not working for overflow content. Bar is forced because it won’t appear by default.

[FIXED] One extra tweak I would like, is the availability to simply drag and drop a transform to the list to add it. Not sure how I’m going to do that, though.

Clean Filled

Ahh thanks for the additional information.

So, first off, you’re doing alot up there. I’d suggest removing some of the complexity to get familiar with the quirks of scroll views.

Try getting rid of the Vertical you add inside the scroll view? It’s not really necessary and it could be causing problems.

I’d also remove the Horizontal from the try block, because if you do happen to hit an error, you won’t close the Horizontal and you’ll get another error outside of the block.

Why is there a return statement inside the try block’s button? That will also cause an error as you don’t close your Horizontal, Vertical, Scroll etc.

I think you need to reorganize your code in a way that avoids the try-catch entirely, and always cleanly exits the inspector method with properly matched Begins and Ends.

As for the changing, try:

if (GUI.changed)
    EditorUtility.SetDirty( target );

But if that doesn’t work, you may be in trouble. Sounds like a serialization issue, which will need you to understand which data types are serializable and how to make them work. You will probably need to refactor your dictionary to use 2 lists, or a list of a custom class with key and value vars. Common problem, look it up.

Hope this helped!

Alright. With everything operating quite nicely, I thought it about time to explain what changes were made, and how to do them. Thanks to OP_toss and iwaldrop for their comments, used or not.

First up, it seems the original issue with scrolling is due to the improper try{}catch{}'s not ending started tags. Organizing information, and either routing around specific errors, or closing tags in the catch will solve the scrolling issues. The errors I was catching seemed to have been cleared up when I serialized the data, go figure.


Next up, Dictionary is not serializable, even if the content (in this case, string and Transform) is. To solve this, creating a custom dictionary was necessary. I wanted to create a universal, but decided to go with a semi-limited instead.

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

[System.Serializable]
public class Teams {
	[SerializeField]
	public List<string> Keys = new List<string>();
	[SerializeField]
	private List<Team> data = new List<Team>();

	public bool ContainsKey(string key) {
		return Keys.Contains(key);
	}

	public bool ContainsValue(Transform value) {
		foreach(Team obj in data) {
			if (obj.Values.Contains(value))
				return true;
		}
		return false;
	}

	public Team GetKey(string key) {
		if (Keys.Contains(key)) {
			foreach(Team obj in data) {
				if (obj.Key == key)
					return obj;
			}
		}
		return null;
	}

	public void Add(string key) {
		if (!Keys.Contains(key)) {
			Keys.Add(key);
			data.Add(new Team(key));
		}
	}

	public void Add(string key, Transform value) {
		if (!Keys.Contains(key)) {
			// create
			Keys.Add(key);
			data.Add(new Team(key, new List<Transform>(new Transform[] {value})));
		} else {
			// use existing
			foreach(Team obj in data) {
				if (obj.Key == key) {
					obj.Add(value);
					return;
				}
			}
		}
	}

	public void Remove(string key) {
		if (Keys.Contains(key)) {
			foreach(Team obj in data) {
				if (obj.Key == key) {
					data.Remove(obj);
					break;
				}
			}
			Keys.Remove(key);
		}
	}
}

[System.Serializable]
public class Team {
	[SerializeField]
	public string Key;
	
	[SerializeField]
	public List<Transform> Values;

	public int Count {
		get { return Values.Count; }
	}
	public Team(string key) {
		Key = key;
		Values = new List<Transform>();
	}

	public Team(string key, List<Transform> values) {
		Key = key;
		Values = values;
	}
	public void Add(Transform value) {
		if (!Values.Contains(value))
			Values.Add(value);
	}
	public void Remove(Transform value) {
		if (Values.Contains(value))
			Values.Remove(value);
	}
}

As for drag and drop, this was easier moved after the said object was drawn.

Rect drop_area = GUILayoutUtility.GetLastRect ();
Event evt = Event.current;
if (evt.type == EventType.DragUpdated) {
	// determine if the dropped item meets our criteria
	if (!drop_area.Contains(evt.mousePosition))
		return; // not in drop zone

	if (!teamhandler.teams.ContainsKey(selectedTeam))
		return; // can't drop

	foreach (Object dragged_object in DragAndDrop.objectReferences) {
		if (dragged_object.GetType() != typeof(GameObject))
			return; // one or more items is not a gameobject
		if (((GameObject) dragged_object).transform == null)
			return; // one or more items had no transform attached
	}
	// acceptable item
	DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
}

Dragging a transform will give a GameObject handle with a handle to the transform, and dragging a prefab will give a GameObject with no transform.