Files
Continentis/Assets/Scripts/ScriptExtensions/SerializableDictionary/SerializableDictionaryDrawer.cs

433 lines
17 KiB
C#
Raw Normal View History

2025-10-23 00:49:44 -04:00
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditorInternal;
namespace SLSFramework.General
{
[CustomPropertyDrawer(typeof(SerializableDictionary<,>), true)]
public class SerializableDictionaryDrawer : PropertyDrawer
{
public override void OnGUI(Rect rect, SerializedProperty prop, GUIContent label)
{
var indentedRect = EditorGUI.IndentedRect(rect);
void Head()
{
var headerRect = indentedRect;
headerRect.height = EditorGUIUtility.singleLineHeight;
void ExpandablePanel()
{
var fullHeaderRect = new Rect(headerRect);
fullHeaderRect.x -= 17;
fullHeaderRect.width += 34;
if (Event.current != null && fullHeaderRect.Contains(Event.current.mousePosition))
{
Color transparentGrey = new Color(0.4f, 0.4f, 0.4f, 0.4f);
EditorGUI.DrawRect(fullHeaderRect, transparentGrey);
}
GUI.color = Color.clear;
if (GUI.Button(new Rect(fullHeaderRect.x, fullHeaderRect.y, fullHeaderRect.width - 40,
fullHeaderRect.height), ""))
{
prop.isExpanded = !prop.isExpanded;
}
GUI.color = Color.white;
var triangleRect = rect;
triangleRect.height = EditorGUIUtility.singleLineHeight;
EditorGUI.Foldout(triangleRect, prop.isExpanded, "");
}
void DisplayName()
{
GUI.color = Color.white;
#if UNITY_2022_1_OR_NEWER
var labelRect = headerRect;
GUI.Label(labelRect, prop.displayName);
#else
GUI.Label(headerRect, prop.displayName);
#endif
GUI.color = Color.white;
GUI.skin.label.fontSize = 12;
GUI.skin.label.fontStyle = FontStyle.Normal;
GUI.skin.label.alignment = TextAnchor.MiddleLeft;
}
void DuplicatedKeysWarning()
{
if (Event.current != null && Event.current.type != EventType.Repaint)
{
return;
}
var hasRepeated = false;
var repeatedKeys = new List<string>();
for (int i = 0; i < dictionaryList.arraySize; i++)
{
SerializedProperty isKeyRepeatedProperty = dictionaryList.GetArrayElementAtIndex(i)
.FindPropertyRelative("isKeyDuplicated");
if (isKeyRepeatedProperty.boolValue)
{
hasRepeated = true;
SerializedProperty keyProperty = dictionaryList.GetArrayElementAtIndex(i).FindPropertyRelative("Key");
string keyString = GetSerializedPropertyValueAsString(keyProperty);
repeatedKeys.Add(keyString);
}
}
if (!hasRepeated)
{
return;
}
float with = GUI.skin.label.CalcSize(new GUIContent(prop.displayName)).x;
headerRect.x += with + 35f;
var warningRect = headerRect;
Rect warningRectIcon = new Rect(headerRect.x - 18, headerRect.y, headerRect.width, headerRect.height);
GUI.color = Color.white;
GUI.Label(warningRectIcon, EditorGUIUtility.IconContent("console.erroricon"));
GUI.color = new Color(1.0f, 0.443f, 0.443f);
GUI.skin.label.fontStyle = FontStyle.Bold;
GUI.Label(warningRect, "Duplicated keys: " + string.Join(", ", repeatedKeys));
GUI.color = Color.white;
GUI.skin.label.fontStyle = FontStyle.Normal;
}
string GetSerializedPropertyValueAsString(SerializedProperty property)
{
switch (property.propertyType)
{
case SerializedPropertyType.Integer:
return property.intValue.ToString();
case SerializedPropertyType.Boolean:
return property.boolValue.ToString();
case SerializedPropertyType.Float:
return property.floatValue.ToString();
case SerializedPropertyType.String:
return property.stringValue;
default:
return "(Unsupported Type)";
}
}
ExpandablePanel();
DisplayName();
DuplicatedKeysWarning();
}
void List()
{
if (!prop.isExpanded)
{
return;
}
SetupList(prop);
float newHeight = indentedRect.height - EditorGUIUtility.singleLineHeight - 3;
indentedRect.y += indentedRect.height - newHeight;
indentedRect.height = newHeight;
reorderableList.DoList(indentedRect);
}
SetupProps(prop);
Head();
List();
}
public override float GetPropertyHeight(SerializedProperty prop, GUIContent label)
{
SetupProps(prop);
var height = EditorGUIUtility.singleLineHeight;
if (prop.isExpanded)
{
SetupList(prop);
height += reorderableList.GetHeight() + 5;
}
return height;
}
private float GetListElementHeight(int index)
{
if (index >= dictionaryList.arraySize) return 0;
var kvpProp = dictionaryList.GetArrayElementAtIndex(index);
var keyProp = kvpProp.FindPropertyRelative("Key");
var valueProp = kvpProp.FindPropertyRelative("Value");
float keyHeight = EditorGUI.GetPropertyHeight(keyProp, true);
float valueHeight;
if (IsSingleLine(valueProp))
{
// 如果Value是单行高度就是它自身的高度
valueHeight = EditorGUI.GetPropertyHeight(valueProp, true);
}
else
{
// 如果Value是复杂类型基础高度是标题行的高度
valueHeight = EditorGUIUtility.singleLineHeight;
// 如果它被展开了,需要加上所有子属性的高度
if (valueProp.isExpanded)
{
foreach (var child in GetChildren(valueProp))
{
valueHeight += EditorGUI.GetPropertyHeight(child, true) + EditorGUIUtility.standardVerticalSpacing;
}
}
}
// 返回Key和Value中较高者的高度并增加一点垂直间距
return Mathf.Max(keyHeight, valueHeight) + EditorGUIUtility.standardVerticalSpacing;
}
void DrawListElement(Rect rect, int index, bool isActive, bool isFocused)
{
if (index >= dictionaryList.arraySize) return;
var kvpProp = dictionaryList.GetArrayElementAtIndex(index);
var keyProp = kvpProp.FindPropertyRelative("Key");
var valueProp = kvpProp.FindPropertyRelative("Value");
// 为整个元素添加一点垂直内边距
rect.y += 2;
// --- 区域计算 ---
var dividerWidh = 6f;
var dividerPosition = (dividerPosProp != null) ? dividerPosProp.floatValue : 0.3f;
var fullRect = rect;
fullRect.width -= 1;
var keyRect = fullRect;
keyRect.width *= dividerPosition;
keyRect.width -= dividerWidh / 2;
var valueRect = fullRect;
valueRect.x += fullRect.width * dividerPosition;
valueRect.width *= (1 - dividerPosition);
valueRect.width -= dividerWidh / 2;
// --- 绘制Key (保持不变) ---
// 确保Key的高度是单行避免它被拉伸
keyRect.height = EditorGUIUtility.singleLineHeight;
EditorGUI.PropertyField(keyRect, keyProp, GUIContent.none, true);
// --- 核心修改自定义绘制Value列 ---
if (IsSingleLine(valueProp))
{
// 如果Value是单行则正常绘制
valueRect.height = EditorGUIUtility.singleLineHeight;
EditorGUI.PropertyField(valueRect, valueProp, GUIContent.none, true);
}
else
{
// 如果Value是复杂类型则自定义绘制
var headerRect = new Rect(valueRect.x + 12, valueRect.y, valueRect.width, EditorGUIUtility.singleLineHeight);
// --- 问题1修复分离绘制三角箭头和标题增加间距 ---
// 1. 先只绘制三角箭头
valueProp.isExpanded = EditorGUI.Foldout(headerRect, valueProp.isExpanded, GUIContent.none, true);
// 2. 在箭头右侧留出空间后,再绘制标题
var titleRect = new Rect(headerRect.x + 3, headerRect.y, headerRect.width - 15, headerRect.height);
EditorGUI.LabelField(titleRect, valueProp.type, EditorStyles.boldLabel);
// 如果展开了,则在下方绘制所有子属性
if (valueProp.isExpanded)
{
// --- 问题2修复正确的循环绘制逻辑 ---
var contentRect = new Rect(valueRect.x,
valueRect.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing, valueRect.width,
valueRect.height);
EditorGUI.indentLevel++;
foreach (var child in GetChildren(valueProp))
{
// 1. 获取子属性自身的高度
var childHeight = EditorGUI.GetPropertyHeight(child, true);
// 2. 为子属性创建一个精确的矩形区域
var childRect = new Rect(contentRect.x, contentRect.y, contentRect.width, childHeight);
// 3. 在这个精确的区域内绘制子属性
//EditorGUI.PropertyField(childRect, child, true);
// 4. 保留属性名称,拉长子属性的输入框
if (child.hasVisibleChildren == false)
{
EditorGUI.LabelField(new Rect(childRect.x, childRect.y, EditorGUIUtility.labelWidth, childHeight), child.displayName);
Rect valueFieldRect = new Rect(childRect.x + EditorGUIUtility.labelWidth / 2,
childRect.y, childRect.width - EditorGUIUtility.labelWidth / 2, childHeight);
EditorGUI.PropertyField(valueFieldRect, child, GUIContent.none);
}
else
{
EditorGUI.PropertyField(childRect, child, true);
}
// 5. 将下一个绘制的Y坐标向下移动为下一个属性留出空间
contentRect.y += childHeight + EditorGUIUtility.standardVerticalSpacing;
}
EditorGUI.indentLevel--;
}
}
// --- 绘制和处理分割线 (保持不变) ---
Divider(rect, fullRect, dividerPosition, dividerWidh);
}
void Divider(Rect originalRect, Rect fullRect, float dividerPosition, float dividerWidth)
{
Rect dividerRect = fullRect;
dividerRect.x += fullRect.width * dividerPosition - dividerWidth / 2;
dividerRect.width = dividerWidth;
EditorGUIUtility.AddCursorRect(dividerRect, MouseCursor.ResizeHorizontal);
if (Event.current != null && dividerRect.Contains(Event.current.mousePosition))
{
if (Event.current.type == EventType.MouseDown)
{
//isDividerDragged = true;
}
else if (Event.current.type == EventType.MouseUp
|| Event.current.type == EventType.MouseMove
|| Event.current.type == EventType.MouseLeaveWindow)
{
isDividerDragged = false;
}
}
if (isDividerDragged && Event.current != null && Event.current.type == EventType.MouseDrag)
{
dividerPosProp.floatValue = Mathf.Clamp(dividerPosProp.floatValue + Event.current.delta.x / originalRect.width, .2f, .8f);
}
}
private void ShowDictIsEmptyMessage(Rect rect)
{
GUI.Label(rect, "Empty");
}
private IEnumerable<SerializedProperty> GetChildren(SerializedProperty prop)
{
prop = prop.Copy();
var endProp = prop.GetEndProperty();
prop.NextVisible(true);
while (!SerializedProperty.EqualContents(prop, endProp))
{
yield return prop;
if (!prop.NextVisible(false))
break;
}
}
private IEnumerable<SerializedProperty> GetChildren(SerializedProperty prop, bool enterVisibleGrandchildren)
{
prop = prop.Copy();
var startPath = prop.propertyPath;
var enterVisibleChildren = true;
while (prop.NextVisible(enterVisibleChildren) && prop.propertyPath.StartsWith(startPath))
{
yield return prop;
enterVisibleChildren = enterVisibleGrandchildren;
}
}
private bool IsSingleLine(SerializedProperty prop)
{
return prop.propertyType != SerializedPropertyType.Generic || prop.hasVisibleChildren == false;
}
private void SetupList(SerializedProperty prop)
{
if (reorderableList != null)
{
return;
}
SetupProps(prop);
this.reorderableList = new ReorderableList(dictionaryList.serializedObject, dictionaryList, true, false, true, true);
this.reorderableList.drawElementCallback = DrawListElement;
this.reorderableList.elementHeightCallback = GetListElementHeight;
this.reorderableList.drawNoneElementCallback = ShowDictIsEmptyMessage;
}
private ReorderableList reorderableList;
private bool isDividerDragged;
public void SetupProps(SerializedProperty prop)
{
if (this.property != null)
{
return;
}
this.property = prop;
this.dictionaryList = prop.FindPropertyRelative("dictionaryList");
this.dividerPosProp = prop.FindPropertyRelative("dividerPosProp");
// 尝试获取字段上的 KeyWidthAttribute
// 如果找到了该属性,则设置 dividerPosProp 的值
if (fieldInfo.GetCustomAttributes(typeof(KeyWidthAttribute), true).FirstOrDefault() is KeyWidthAttribute keyWidthAttribute)
{
this.dividerPosProp.floatValue = keyWidthAttribute.WidthPercentage;
}
else
{
this.dividerPosProp.floatValue = 0.5f; // 默认值
}
}
private SerializedProperty property;
private SerializedProperty dictionaryList;
private SerializedProperty dividerPosProp;
}
}
#endif
namespace SLSFramework.General
{
/// <summary>
/// 用于指定 SerializableDictionary 抽屉中 Key 区域的宽度占比。
/// </summary>
[System.AttributeUsage(System.AttributeTargets.Field)]
public class KeyWidthAttribute : PropertyAttribute
{
public readonly float WidthPercentage;
/// <summary>
/// 设置 Key 区域的宽度占比。
/// </summary>
/// <param name="widthPercentage">一个0.1到0.9之间的浮点数代表Key区域占总宽度的百分比。</param>
public KeyWidthAttribute(float widthPercentage)
{
// 将值限制在一个合理的范围内避免UI错乱
WidthPercentage = Mathf.Clamp(widthPercentage, 0.1f, 0.9f);
}
}
}