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(); 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 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 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 { /// /// 用于指定 SerializableDictionary 抽屉中 Key 区域的宽度占比。 /// [System.AttributeUsage(System.AttributeTargets.Field)] public class KeyWidthAttribute : PropertyAttribute { public readonly float WidthPercentage; /// /// 设置 Key 区域的宽度占比。 /// /// 一个0.1到0.9之间的浮点数,代表Key区域占总宽度的百分比。 public KeyWidthAttribute(float widthPercentage) { // 将值限制在一个合理的范围内,避免UI错乱 WidthPercentage = Mathf.Clamp(widthPercentage, 0.1f, 0.9f); } } }