MonsterShop/Assets/Editor/DictionaryDrawer.cs

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> { }