433 lines
17 KiB
C#
433 lines
17 KiB
C#
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);
|
||
}
|
||
}
|
||
} |