- Home /
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.
Yeah, got that done pretty quickly:
Rect drop_area = GUILayoutUtility.GetLastRect ();
Event evt = Event.current;
if (evt.type == EventType.DragUpdated) {
if (!drop_area.Contains(evt.mousePosition))
return; // not in drop zone
if (!$$anonymous$$mhandler.$$anonymous$$ms.Contains$$anonymous$$ey(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
}
// acceptable item
DragAndDrop.visual$$anonymous$$ode = DragAndDropVisual$$anonymous$$ode.Copy;
}
if (evt.type == EventType.DragPerform) {
DragAndDrop.AcceptDrag();
foreach (Object dragged_object in DragAndDrop.objectReferences) {
if (dragged_object.GetType() == typeof(GameObject)) {
GameObject handle = (GameObject)dragged_object;
Debug.Log("Accepted "+handle.name+" Dropped item");
$$anonymous$$mhandler.$$anonymous$$ms[selectedTeam].Add(handle.transform);
} else {
Debug.Log("Rejected dropped item of type "+dragged_object.GetType().ToString()+".");
}
}
}
Works on pretty much any GUI object when used on it's closing.
Could you elaborate on your problem? I'm not sure how to interpret "my scroll view isn't recognizing overflowing content". And that's the only place you mention or describe the problem.
Does it scroll and go blank? Does it just stop adding items? What have you tried? Have you made a simple test-case?
Also the 2 pictures show 2 entirely different editors from the looks of it. What are they meant to describe? $$anonymous$$aybe pictures showing the exact problem would help? Or am I misunderstanding your pics?
Sorry to be picky, but it's hard to help with so little information to go on.
At least you requested more details, noticed I completely missed asking that question in the first place. Usually, with GUI scroll views, you depict 2 sizes. Visual size, and space size. You view the Visual size, and pan around the space size. With this scroll view, I seem to be allowed to set the view size, but the space size seems to be automatically managed based on content. Both images are of the same object. Hit the [+] and you can add more $$anonymous$$ms, which systematically adds the $$anonymous$$m name as a button, and an X for deletion. Adding players to the list will add their Transform name, and an X for deletion.
This list barely fits 5 players in a $$anonymous$$m before it expands past the designated view area. I expected the scroll view to show the vertical scroll bar, and let me pan down through them, but it does not. I can only see the first 5 players in any $$anonymous$$m.
Also, from what I can tell, these settings aren't being saved, or 'updated'. I'm sure it has something to do with handling the TeamHandler $$anonymous$$mhandler = (TeamHandler) target; up at the beginning, or maybe I'm missing some kind of function to apply the changes.
[Accidentally added as an answer]
You're right on the money, had already found out that Dictionary elements aren't serializable, and have a few sources I can strip to design my own. As this is my first run through of trying to get this working, I end up with dirty code. The vertical inside the scroll view actually prevents an error I was getting, though I will eventually hunt it down more explicitly. The return's were my rough attempt at saying 'redo the GUI'. Didn't find an actual function to do the same, which I'm assu$$anonymous$$g is what SetDirty does?(i'll research later) Not even sure if I need it.
As the code is a 3-step process, and all the bits are nice and neatly in their own sections, I don't really see the function of stripping it down just yet. Been making considerable progress with my AI mechanics, so thank you a lot for helping me with this.
Answer by Komak57 · Dec 07, 2013 at 09:31 PM
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.
Sooo I was right but you wanted to horde all the sweet karma for yourself... I seeee hoow it isss.... :P
Answer by OP_toss · Dec 02, 2013 at 08:46 PM
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!
Setting the Dirty flag does nothing for me. The return line wasn't causing any problems for me. Seems all of my problems revolved around the serialization of my Dictionary $$anonymous$$ms. Created my own variable from scratch and it seems that everything works nice and smoothly now. $$anonymous$$ust be something the listview recognizes from data that will be empty upon running (aka, non-serialized data). Now I can resume the course of tidying up the code to work more efficiently without the try/catch clauses. I'll be posting the new snippets as the answer shortly enough.