Files
Continentis/Assets/Scripts/SLSUtilities/SerializableDictionary/SerializableDictionaryDrawer.cs
SoulliesOfficial ac98ec3aef 更新
2026-04-17 12:01:50 -04:00

433 lines
17 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditorInternal;
namespace SLSUtilities.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 SLSUtilities.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);
}
}
}