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 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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|