440 lines
18 KiB
C#
440 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using UnityObject = UnityEngine.Object;
|
|
using System.Reflection;
|
|
|
|
public abstract class DictionaryDrawer<TK, TV> : PropertyDrawer
|
|
{
|
|
private Dictionary<TK, TV> _Dictionary;
|
|
private bool _Foldout;
|
|
private const float kButtonWidth = 18f;
|
|
private static float lineHeight = EditorGUIUtility.singleLineHeight + 4;
|
|
private float spacing = 12f;
|
|
private float fieldPadding = 1f;
|
|
|
|
private GUIStyle addEntryStyle;
|
|
private GUIContent addEntryContent;
|
|
private GUIStyle clearDictionaryStyle;
|
|
private GUIContent clearDictionaryContent;
|
|
//reuses clearDictionaryStyle. I am adding it for readability
|
|
private GUIStyle removeEntryStyle;
|
|
private GUIContent removeEntryContent;
|
|
|
|
private GUIStyle HeaderStyle;
|
|
|
|
private Rect buttonRect;
|
|
|
|
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
|
{
|
|
CheckInitialize(property, label);
|
|
if (_Foldout)
|
|
{
|
|
//Height of the main Header and the two column headers + height of all the drawn dictionary entries + a little padding on the bottom.
|
|
return (GetDictionaryElementsHeight() + (lineHeight * 2)) + 14f;
|
|
}
|
|
return lineHeight+ 4f;
|
|
}
|
|
|
|
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
|
{
|
|
CheckInitialize(property, label);
|
|
|
|
position.height = 20f;
|
|
DrawHeader(position, property, label);
|
|
|
|
|
|
if (!_Foldout)
|
|
return;
|
|
position.y += 5f + lineHeight * 2;
|
|
foreach (var item in _Dictionary)
|
|
{
|
|
var key = item.Key;
|
|
var value = item.Value;
|
|
|
|
var keyRect = position;
|
|
keyRect.width /= 3;
|
|
keyRect.x += 10;
|
|
//Apply vertical padding
|
|
keyRect.y += fieldPadding;
|
|
keyRect.height -= fieldPadding * 2;
|
|
|
|
EditorGUI.BeginChangeCheck();
|
|
var newKey = DoField(keyRect, typeof(TK), (TK)key);
|
|
if (EditorGUI.EndChangeCheck())
|
|
{
|
|
try
|
|
{
|
|
_Dictionary.Remove(key);
|
|
_Dictionary.Add(newKey, value);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_Dictionary.Remove(key);
|
|
Debug.Log(e.Message);
|
|
}
|
|
break;
|
|
}
|
|
|
|
var valueRect = position;
|
|
valueRect.x = keyRect.xMax + spacing;
|
|
valueRect.y += fieldPadding;
|
|
//Apply vertical padding
|
|
valueRect.height -= fieldPadding * 2;
|
|
valueRect.width = (position.width - keyRect.width) - ((kButtonWidth + 2) * 2f) - valueRect.size.y - (spacing* 2.5f);
|
|
EditorGUI.BeginChangeCheck();
|
|
value = DoField(valueRect, typeof(TV), (TV)value);
|
|
|
|
|
|
Rect changeValueRect = new Rect(new Vector2(buttonRect.x - 2f, valueRect.position.y), new Vector2(kButtonWidth, valueRect.size.y));
|
|
value = ChangeValueType(changeValueRect, key, value);
|
|
|
|
if (EditorGUI.EndChangeCheck())
|
|
{
|
|
_Dictionary[key] = value;
|
|
break;
|
|
}
|
|
EditorGUIUtility.AddCursorRect(changeValueRect, MouseCursor.Link);
|
|
|
|
var removeRect = valueRect;
|
|
removeRect.x = buttonRect.x + kButtonWidth;
|
|
removeRect.width = kButtonWidth;
|
|
if (GUI.Button(removeRect, removeEntryContent, removeEntryStyle))
|
|
{
|
|
RemoveItem(key);
|
|
break;
|
|
}
|
|
EditorGUIUtility.AddCursorRect(removeRect, MouseCursor.Link);
|
|
position.y += Mathf.Max(GetEntryHeight(key) ,GetEntryHeight(value));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the combined height of all dictionary elements
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
private float GetDictionaryElementsHeight()
|
|
{
|
|
float height = 0;
|
|
foreach(var item in _Dictionary)
|
|
{
|
|
var key = item.Key;
|
|
var value = item.Value;
|
|
height += Mathf.Max(GetEntryHeight(key), GetEntryHeight(value));
|
|
}
|
|
|
|
return height;
|
|
}
|
|
|
|
private void DrawColumn(Rect position, GUIStyle style)
|
|
{
|
|
Rect columnRect = new Rect(position.x, position.yMax - 1, position.width, GetDictionaryElementsHeight() + 12f);
|
|
GUI.Box(columnRect, GUIContent.none, style);
|
|
}
|
|
|
|
private void DrawHeader(Rect position, SerializedProperty property, GUIContent label)
|
|
{
|
|
Rect headerRect = new Rect(position.position, new Vector2(position.size.x - kButtonWidth * 1.5f, lineHeight));
|
|
GUI.Box(headerRect, GUIContent.none, HeaderStyle);
|
|
var foldoutRect = position;
|
|
foldoutRect.x += 4f;
|
|
foldoutRect.width -= 2 * kButtonWidth;
|
|
EditorGUI.BeginChangeCheck();
|
|
if(_Dictionary.Count > 0)
|
|
{
|
|
_Foldout = EditorGUI.Foldout(foldoutRect, _Foldout, label, true);
|
|
}
|
|
else
|
|
{
|
|
foldoutRect.x += 4f;
|
|
EditorGUI.LabelField(foldoutRect, label);
|
|
_Foldout = false;
|
|
}
|
|
if (EditorGUI.EndChangeCheck())
|
|
{
|
|
EditorPrefs.SetBool(label.text, _Foldout);
|
|
}
|
|
|
|
//Draw the Add Item Button
|
|
buttonRect = position;
|
|
buttonRect.x = position.width - 20 - kButtonWidth + position.x + 1;
|
|
buttonRect.width = kButtonWidth;
|
|
|
|
GUIStyle headerButtonStyle = new GUIStyle(HeaderStyle);
|
|
headerButtonStyle.padding = new RectOffset(0, 0, 0, 0);
|
|
Rect headerButtonRect = new Rect(buttonRect.position, new Vector2(kButtonWidth * 1.5f, lineHeight));
|
|
if (GUI.Button(headerButtonRect, addEntryContent, headerButtonStyle))
|
|
{
|
|
AddNewItem();
|
|
}
|
|
EditorGUIUtility.AddCursorRect(headerButtonRect, MouseCursor.Link);
|
|
buttonRect.x -= kButtonWidth;
|
|
|
|
//Draw the Item count label
|
|
GUIStyle headerItemCountLabelStyle = new GUIStyle("MiniLabel");
|
|
GUIContent headerItemCountLabelContent = new GUIContent();
|
|
if(_Dictionary.Count == 0)
|
|
{
|
|
headerItemCountLabelContent = new GUIContent("Empty");
|
|
}
|
|
else
|
|
{
|
|
headerItemCountLabelContent = new GUIContent($"{_Dictionary.Count} Item{(_Dictionary.Count == 1 ? "" : "s")}");
|
|
}
|
|
|
|
GUI.Label(new Rect(buttonRect.x - 30f, buttonRect.y, 50f, headerRect.height), headerItemCountLabelContent, headerItemCountLabelStyle);
|
|
|
|
|
|
//Draw the header labels (Keys - Values)
|
|
if(_Foldout)
|
|
{
|
|
//Draw "Keys" header
|
|
position.y += headerRect.height;
|
|
Rect keyHeaderRect = new Rect(position.x, position.y - 1, position.width /3f + kButtonWidth - 1, headerRect.height);
|
|
GUIStyle columnHeaderStyle = new GUIStyle("GroupBox");
|
|
columnHeaderStyle.padding = new RectOffset(0, 0, 0, 0);
|
|
columnHeaderStyle.contentOffset = new Vector2(0, 3f);
|
|
GUI.Box(keyHeaderRect, new GUIContent("Keys"), columnHeaderStyle);
|
|
|
|
//Draw "Values" header
|
|
Rect valuesHeaderRect = new Rect(keyHeaderRect.xMax - 1, keyHeaderRect.y, (position.width - keyHeaderRect.width - kButtonWidth * 0.5f), keyHeaderRect.height);
|
|
GUI.Box(valuesHeaderRect, new GUIContent("Values"), columnHeaderStyle);
|
|
//Draw the Columns for the keys and values.
|
|
DrawColumn(keyHeaderRect, columnHeaderStyle);
|
|
DrawColumn(valuesHeaderRect, columnHeaderStyle);
|
|
|
|
position.y += headerRect.height;
|
|
}
|
|
|
|
/*
|
|
if (GUI.Button(buttonRect, clearDictionaryContent, clearDictionaryStyle))
|
|
{
|
|
ClearDictionary();
|
|
}
|
|
*/
|
|
}
|
|
|
|
#region TypeControls
|
|
private static float GetEntryHeight<T>(T value)
|
|
{
|
|
switch (value)
|
|
{
|
|
case Bounds: return lineHeight * 2;
|
|
case BoundsInt: return lineHeight * 2;
|
|
case Rect: return lineHeight * 2;
|
|
case RectInt: return lineHeight * 2;
|
|
default: return lineHeight;
|
|
}
|
|
}
|
|
|
|
private static T DoField<T>(Rect rect, Type type, T value)
|
|
{
|
|
if (typeof(UnityObject).IsAssignableFrom(type))
|
|
return (T)(object)EditorGUI.ObjectField(rect, (UnityObject)(object)value, type, true);
|
|
switch (value)
|
|
{
|
|
case null: EditorGUI.LabelField(rect, "null"); return value;
|
|
case long: return (T)(object)EditorGUI.LongField(rect, (long)(object)value);
|
|
case int: return (T)(object)EditorGUI.IntField(rect, (int)(object)value);
|
|
case float: return (T)(object)EditorGUI.FloatField(rect, (float)(object)value);
|
|
case double: return (T)(object)EditorGUI.DoubleField(rect, (double)(object)value);
|
|
case string: return (T)(object)EditorGUI.TextField(rect, (string)(object)value);
|
|
case bool: return (T)(object)EditorGUI.Toggle(rect, (bool)(object)value);
|
|
case Vector2Int: return (T)(object)EditorGUI.Vector2IntField(rect, GUIContent.none, (Vector2Int)(object)value);
|
|
case Vector3Int: return (T)(object)EditorGUI.Vector3IntField(rect, GUIContent.none, (Vector3Int)(object)value);
|
|
case Vector2: return (T)(object)EditorGUI.Vector2Field(rect, GUIContent.none, (Vector2)(object)value);
|
|
case Vector3: return (T)(object)EditorGUI.Vector3Field(rect, GUIContent.none, (Vector3)(object)value);
|
|
case Vector4: return (T)(object)EditorGUI.Vector4Field(rect, GUIContent.none, (Vector4)(object)value);
|
|
case BoundsInt: return (T)(object)EditorGUI.BoundsIntField(rect, (BoundsInt)(object)value);
|
|
case Bounds: return (T)(object)EditorGUI.BoundsField(rect, (Bounds)(object)value);
|
|
case RectInt: return (T)(object)EditorGUI.RectIntField(rect, (RectInt)(object)value);
|
|
case Rect: return (T)(object)EditorGUI.RectField(rect, (Rect)(object)value);
|
|
case Color: return (T)(object)EditorGUI.ColorField(rect, (Color)(object)value);
|
|
case AnimationCurve: return (T)(object)EditorGUI.CurveField(rect, (AnimationCurve)(object)value);
|
|
case Gradient: return (T)(object)EditorGUI.GradientField(rect, (Gradient)(object)value);
|
|
case UnityObject: return (T)(object)EditorGUI.ObjectField(rect, (UnityObject)(object)value, type, true);
|
|
}
|
|
|
|
if (value.GetType().IsEnum)
|
|
{
|
|
if (Enum.TryParse(value.GetType(), value.ToString(), out object enumValue))
|
|
{
|
|
return (T)(object)EditorGUI.EnumPopup(rect, (Enum)enumValue);
|
|
}
|
|
}
|
|
|
|
//Setup GUIStyle and GUIContent for the "Clear Dictionary" button
|
|
GUIStyle style = new GUIStyle(EditorStyles.miniButton);
|
|
style.padding = new RectOffset(2, 2, 2, 2);
|
|
GUIContent content = new GUIContent(EditorGUIUtility.IconContent("d_UnityEditor.ConsoleWindow@2x"));
|
|
content.tooltip = "Debug Values";
|
|
|
|
Type fieldType = value.GetType();
|
|
bool isStruct = fieldType.IsValueType && !fieldType.IsEnum;
|
|
EditorGUI.LabelField(rect, $"{fieldType.ToString().Replace("+", ".")} {(isStruct ? "struct" : "class")} instance");
|
|
if( GUI.Button(new Rect(rect.xMax - kButtonWidth, rect.y, kButtonWidth, kButtonWidth), content, style))
|
|
{
|
|
Debug.Log(JsonUtility.ToJson(value));
|
|
}
|
|
|
|
//DrawSerializableObject(rect, value);
|
|
return value;
|
|
}
|
|
|
|
//Unfinished
|
|
/*
|
|
public static void DrawSerializableObject(Rect rect, object obj)
|
|
{
|
|
if (obj == null)
|
|
{
|
|
Console.WriteLine("Object is null.");
|
|
return;
|
|
}
|
|
|
|
Type type = obj.GetType();
|
|
FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
|
|
|
foreach (FieldInfo field in fields)
|
|
{
|
|
object value = field.GetValue(obj);
|
|
rect.y += GetEntryHeight(value);
|
|
Debug.Log($"{field.Name}: {value}");
|
|
if(value != null)
|
|
{
|
|
DoField(rect, value.GetType(), value);
|
|
}
|
|
}
|
|
|
|
}
|
|
*/
|
|
|
|
private TV ChangeValueType(Rect rect, TK key, TV value)
|
|
{
|
|
GUIContent content = EditorGUIUtility.IconContent("_Popup");
|
|
content.tooltip = "Change Value Type";
|
|
GUIStyle changeItemStyle = new GUIStyle(EditorStyles.miniButton);
|
|
changeItemStyle.padding = new RectOffset(2, 2, 2, 2);
|
|
|
|
if (GUI.Button(rect, content, changeItemStyle))
|
|
{
|
|
GenericMenu genericMenu = new GenericMenu();
|
|
genericMenu.AddItem(new GUIContent("Numbers/int"), value is int, () => { _Dictionary[key] = (TV)(object)default(int); });
|
|
genericMenu.AddItem(new GUIContent("Numbers/float"), value is float, () => { _Dictionary[key] = (TV)(object)default(float); });
|
|
genericMenu.AddItem(new GUIContent("Numbers/double"), value is double, () => { _Dictionary[key] = (TV)(object)default(double); });
|
|
genericMenu.AddItem(new GUIContent("Numbers/long"), value is long, () => { _Dictionary[key] = (TV)(object)default(long); });
|
|
genericMenu.AddItem(new GUIContent("Vectors/Vector2"), (value is Vector2 && !(value is Vector2Int)), () => { _Dictionary[key] = (TV)(object)default(Vector2); });
|
|
genericMenu.AddItem(new GUIContent("Vectors/Vector3"), (value is Vector3 && !(value is Vector3Int)), () => { _Dictionary[key] = (TV)(object)default(Vector3); });
|
|
genericMenu.AddItem(new GUIContent("Vectors/Vector4"), value is Vector4, () => { _Dictionary[key] = (TV)(object)default(Vector4); });
|
|
genericMenu.AddItem(new GUIContent("Vectors/Vector2Int"), value is Vector2Int, () => { _Dictionary[key] = (TV)(object)default(Vector2Int); });
|
|
genericMenu.AddItem(new GUIContent("Vectors/Vector3Int"), value is Vector3Int, () => { _Dictionary[key] = (TV)(object)default(Vector3Int); });
|
|
genericMenu.AddItem(new GUIContent("Bounds/Bounds"), value is Bounds && value is not BoundsInt, () => { _Dictionary[key] = (TV)(object)default(Bounds); });
|
|
genericMenu.AddItem(new GUIContent("Bounds/BoundsInt"), value is BoundsInt, () => { _Dictionary[key] = (TV)(object)default(BoundsInt); });
|
|
genericMenu.AddItem(new GUIContent("Rects/Rect"), value is Rect && value is not RectInt, () => { _Dictionary[key] = (TV)(object)default(Rect); });
|
|
genericMenu.AddItem(new GUIContent("Rects/RectInt"), value is RectInt, () => { _Dictionary[key] = (TV)(object)default(RectInt); });
|
|
genericMenu.AddItem(new GUIContent("string"), value is string, () => { _Dictionary[key] = (TV)(object)""; });
|
|
genericMenu.AddItem(new GUIContent("bool"), value is bool, () => { _Dictionary[key] = (TV)(object)default(bool); });
|
|
genericMenu.AddItem(new GUIContent("Color"), value is Color, () => { _Dictionary[key] = (TV)(object)default(Color); });
|
|
genericMenu.AddItem(new GUIContent("AnimationCurve"), value is AnimationCurve, () => { _Dictionary[key] = (TV)(object)(new AnimationCurve()); });
|
|
genericMenu.AddItem(new GUIContent("Gradient"), value is Gradient, () => { _Dictionary[key] = (TV)(object)(new Gradient()); });
|
|
genericMenu.AddItem(new GUIContent("Unity Object"), value is UnityObject, () => { _Dictionary[key] = (TV)(object)(new UnityObject()); });
|
|
genericMenu.ShowAsContext();
|
|
}
|
|
|
|
return (TV)value;
|
|
}
|
|
#endregion
|
|
|
|
private void RemoveItem(TK key)
|
|
{
|
|
_Dictionary.Remove(key);
|
|
}
|
|
|
|
private void CheckInitialize(SerializedProperty property, GUIContent label)
|
|
{
|
|
if (_Dictionary == null)
|
|
{
|
|
SetupStyles();
|
|
var target = property.serializedObject.targetObject;
|
|
_Dictionary = fieldInfo.GetValue(target) as Dictionary<TK, TV>;
|
|
if (_Dictionary == null)
|
|
{
|
|
_Dictionary = new Dictionary<TK, TV>();
|
|
fieldInfo.SetValue(target, _Dictionary);
|
|
}
|
|
|
|
_Foldout = EditorPrefs.GetBool(label.text);
|
|
}
|
|
}
|
|
|
|
private void SetupStyles()
|
|
{
|
|
//Setup GUIStyle and GUIContent for the "Add Item" button
|
|
addEntryStyle = new GUIStyle(EditorStyles.miniButton);
|
|
addEntryStyle.padding = new RectOffset(3, 3, 3, 3);
|
|
addEntryContent = new GUIContent(EditorGUIUtility.IconContent("d_CreateAddNew@2x"));
|
|
addEntryContent.tooltip = "Add Item";
|
|
|
|
//Setup GUIStyle and GUIContent for the "Clear Dictionary" button
|
|
clearDictionaryStyle = new GUIStyle(EditorStyles.miniButton);
|
|
clearDictionaryStyle.padding = new RectOffset(2, 2, 2, 2);
|
|
clearDictionaryContent = new GUIContent(EditorGUIUtility.IconContent("d_winbtn_win_close@2x"));
|
|
clearDictionaryContent.tooltip = "Clear dictionary";
|
|
|
|
removeEntryContent = new GUIContent(EditorGUIUtility.IconContent("d_winbtn_win_close@2x"));
|
|
removeEntryContent.tooltip = "Remove Item";
|
|
removeEntryStyle = new GUIStyle(clearDictionaryStyle);
|
|
|
|
HeaderStyle = new GUIStyle("MiniToolbarButton");
|
|
HeaderStyle.fixedHeight = 0;
|
|
HeaderStyle.fixedWidth = 0;
|
|
HeaderStyle.padding = new RectOffset(2,2,2,2);
|
|
}
|
|
|
|
private void ClearDictionary()
|
|
{
|
|
_Dictionary.Clear();
|
|
}
|
|
|
|
private void AddNewItem()
|
|
{
|
|
TK key;
|
|
if (typeof(TK) == typeof(string))
|
|
key = (TK)(object)"";
|
|
else key = default(TK);
|
|
|
|
if (typeof(TV) == typeof(object))
|
|
{
|
|
var value = (TV)(object)1;
|
|
try
|
|
{
|
|
_Dictionary.Add(key, value);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.Log(e.Message);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var value = default(TV);
|
|
try
|
|
{
|
|
_Dictionary.Add(key, value);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.Log(e.Message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
[CustomPropertyDrawer(typeof(Blackboard))]
|
|
public class BlackboardDrawer : DictionaryDrawer<string, object> { }
|
|
|
|
|
|
// [CustomPropertyDrawer(typeof(Inventory))]
|
|
// public class InventoryDrawer : DictionaryDrawer<string, int> { }
|