modular-avatar/Editor/Inspector/AvatarParametersEditor.cs
kaikoga 3667dc319a
chore: Wrap VRC dependencies and VRCAvatarDescriptors (#504)
* add version defines

* refactor: prefer BuildContext.ctor() overload taking GameObject

* wrap unit tests entirely with MA_VRCSDK3_AVATARS

* wrap unit tests smart with MA_VRCSDK3_AVATARS

* wrap runtime entirely with MA_VRCSDK3_AVATARS

* wrap VRC.SDKBase.IEditorOnly with MA_VRCSDK3_AVATARS

* wrap editor entirely with MA_VRCSDK3_AVATARS

* fix AvatarObjectReference.Get(Component)

* wrap editor smart with MA_VRCSDK3_AVATARS

* wrap BuildContext smart with MA_VRCSDK3_AVATARS

* wrap PluginDefinition smart with MA_VRCSDK3_AVATARS

* style: move conditional compiles one step outside
2023-11-10 15:37:56 +09:00

483 lines
19 KiB
C#

#if MA_VRCSDK3_AVATARS
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using static nadena.dev.modular_avatar.core.editor.Localization;
using Debug = System.Diagnostics.Debug;
namespace nadena.dev.modular_avatar.core.editor
{
[CustomPropertyDrawer(typeof(ParameterSyncType))]
internal class ParameterSyncTypeDrawer : EnumDrawer<ParameterSyncType>
{
protected override string localizationPrefix => "params.syncmode";
protected override Array enumValues => new object[]
{
ParameterSyncType.NotSynced,
ParameterSyncType.Bool,
ParameterSyncType.Float,
ParameterSyncType.Int,
};
}
[CustomEditor(typeof(ModularAvatarParameters))]
internal class AvatarParametersEditor : MAEditorBase
{
/**********************************************************************
* | Field name | Remap to / config |
* |------------|--------------------|
*/
private bool _devMode;
private ReorderableList _reorderableList;
private SerializedProperty _parameters;
private bool _needsRebuild = false;
private float elemHeight;
private readonly List<int> _selectedIndices = new List<int>();
private ImplicitParams _implicitParams = null;
private SerializedObject _implicitParamsObj;
private SerializedProperty _implicitParamsProp;
private class ImplicitParams : ScriptableObject
{
public List<ParameterConfig> ImplicitParameters = new List<ParameterConfig>();
}
private void OnEnable()
{
SetupList();
}
private SerializedProperty GetParamByIndex(int idx)
{
if (idx >= 0)
{
return _parameters.GetArrayElementAtIndex(idx);
}
else
{
idx = -(idx + 1);
return _implicitParamsProp.GetArrayElementAtIndex(idx);
}
}
private void DetectParameters()
{
if (_implicitParams == null || _implicitParamsProp == null)
{
_implicitParams = CreateInstance<ImplicitParams>();
_implicitParamsObj = new SerializedObject(_implicitParams);
_implicitParamsProp = _implicitParamsObj.FindProperty(nameof(ImplicitParams.ImplicitParameters));
}
var target = (ModularAvatarParameters) this.target;
var registered = target.parameters.Select(p => p.nameOrPrefix + (p.isPrefix ? "*" : ""))
.ToImmutableHashSet();
_implicitParams.ImplicitParameters.Clear();
var detected = ParameterPolicy.ProbeParameters(((ModularAvatarParameters) target).gameObject);
foreach (var param in detected.Values)
{
if (registered.Contains(param.MapKey)) continue;
var config = new ParameterConfig()
{
internalParameter = false,
isPrefix = param.IsPrefix,
nameOrPrefix = param.OriginalName,
syncType = param.syncType,
defaultValue = param.defaultValue,
saved = param.saved,
remapTo = "",
};
_implicitParams.ImplicitParameters.Add(config);
_selectedIndices.Add(-_implicitParams.ImplicitParameters.Count);
}
_implicitParamsObj.Update();
}
private void SetupList()
{
_parameters = serializedObject.FindProperty(nameof(ModularAvatarParameters.parameters));
if (_devMode)
{
var target = (ModularAvatarParameters) this.target;
_selectedIndices.Clear();
_selectedIndices.AddRange(Enumerable.Range(0, target.parameters.Count));
}
else
{
_selectedIndices.Clear();
for (int i = 0; i < _parameters.arraySize; i++)
{
var isInternal = _parameters.GetArrayElementAtIndex(i)
.FindPropertyRelative(nameof(ParameterConfig.internalParameter));
if (isInternal.boolValue) continue;
_selectedIndices.Add(i);
}
}
DetectParameters();
ElementHeightCache = BuildHeightCache();
if (_reorderableList == null)
{
_reorderableList = new ReorderableList(
_selectedIndices,
typeof(int),
false, true, _devMode, _devMode
);
_reorderableList.drawHeaderCallback = DrawHeader;
_reorderableList.drawElementCallback = DrawElement;
_reorderableList.onAddCallback = AddElement;
_reorderableList.onRemoveCallback = RemoveElement;
_reorderableList.elementHeightCallback = ElementHeight;
_reorderableList.onReorderCallbackWithDetails = ReorderElements;
elemHeight = _reorderableList.elementHeight;
}
_reorderableList.displayAdd = _devMode;
_reorderableList.displayRemove = _devMode;
_reorderableList.draggable = _devMode;
_needsRebuild = false;
}
private void ReorderElements(ReorderableList list, int oldindex, int newindex)
{
if (_selectedIndices[oldindex] >= 0 && _selectedIndices[newindex] >= 0)
{
// We're in dev mode, so the "real" entries are in the same order in _selectedIndices
// as the underlying. So just reorder them in the underlying object.
serializedObject.ApplyModifiedProperties();
var target = (ModularAvatarParameters) this.target;
var tmp = target.parameters[oldindex];
target.parameters.RemoveAt(oldindex);
target.parameters.Insert(newindex, tmp);
serializedObject.Update();
}
// The reorderable list trashed our internal indices, so force a rebuild on next repaint.
_needsRebuild = true;
}
private void AddElement(ReorderableList list)
{
_parameters.arraySize += 1;
_selectedIndices.Insert(_parameters.arraySize - 1, _parameters.arraySize - 1);
list.index = _parameters.arraySize - 1;
}
private void RemoveElement(ReorderableList list)
{
if (list.index < 0 || _selectedIndices[list.index] < 0) return;
_parameters.DeleteArrayElementAtIndex(list.index);
_needsRebuild = true;
}
// https://github.com/bdunderscore/modular-avatar/issues/217
private float[] ElementHeightCache = null;
private float[] BuildHeightCache()
{
float[] cache = new float[_selectedIndices.Count];
for (int i = 0; i < _selectedIndices.Count; i++)
{
float baseHeight = _devMode ? elemHeight * 2 : elemHeight;
var param = GetParamByIndex(_selectedIndices[i]);
var syncMode = param.FindPropertyRelative(nameof(ParameterConfig.syncType));
if (syncMode.enumValueIndex != (int) ParameterSyncType.NotSynced)
{
baseHeight += elemHeight;
}
if (_selectedIndices[i] == -1)
{
cache[i] = elemHeight + baseHeight;
}
else
{
cache[i] = baseHeight;
}
}
return cache;
}
private float ElementHeight(int reqIndex)
{
return ElementHeightCache[reqIndex];
}
private void DrawAutodetectHeader(ref Rect rect)
{
Rect top = rect;
top.height = elemHeight;
Rect bottom = rect;
bottom.y += top.height;
bottom.height -= top.height;
rect = bottom;
var style = new GUIStyle(EditorStyles.label);
style.fontStyle = FontStyle.Italic;
var content = new GUIContent(G("params.autodetect_header"));
var size = style.CalcSize(content);
var centeredRect = new Rect(
top.x + (top.width - size.x) / 2,
top.y + (top.height - size.y) / 2,
size.x,
size.y
);
EditorGUI.LabelField(centeredRect, content, style);
}
private void DrawElement(Rect rect, int index, bool isactive, bool isfocused)
{
var originalIndex = index;
index = _selectedIndices[index];
var elem = GetParamByIndex(index);
if (index == -1)
{
DrawAutodetectHeader(ref rect);
}
var nameOrPrefix = elem.FindPropertyRelative(nameof(ParameterConfig.nameOrPrefix));
var remapTo = elem.FindPropertyRelative(nameof(ParameterConfig.remapTo));
var internalParameter = elem.FindPropertyRelative(nameof(ParameterConfig.internalParameter));
var isPrefix = elem.FindPropertyRelative(nameof(ParameterConfig.isPrefix));
var syncType = elem.FindPropertyRelative(nameof(ParameterConfig.syncType));
var isSynced = syncType.enumValueIndex != (int) ParameterSyncType.NotSynced;
var margin = 20;
var halfMargin = margin / 2;
var leftHalf = new Rect(rect.x, rect.y, rect.width * 0.3f - halfMargin, elemHeight);
var rightHalf = new Rect(rect.x + leftHalf.width + halfMargin, rect.y, leftHalf.width, elemHeight);
rightHalf.xMax = rect.xMax;
var rightHalfTop = new Rect(rect.x + leftHalf.width + halfMargin, rect.y, leftHalf.width, elemHeight);
rightHalfTop.xMax = rect.xMax;
var rightHalfSyncControlField = rightHalfTop;
rightHalfSyncControlField.y += _devMode ? elemHeight : 0;
var rightHalfDefaultValue = rightHalfSyncControlField;
rightHalfDefaultValue.y += elemHeight;
var indentLevel = EditorGUI.indentLevel;
try
{
EditorGUI.BeginChangeCheck();
indentLevel = 0;
if (_devMode)
{
EditorGUI.PropertyField(leftHalf, nameOrPrefix, GUIContent.none);
var internalGuiContent = G("params.internal");
var toggleInternalWidth = EditorStyles.toggle.CalcSize(internalGuiContent).x;
var toggleInternalRect = new Rect(rightHalfTop.x, rightHalfTop.y, toggleInternalWidth,
rightHalfTop.height);
internalParameter.boolValue =
EditorGUI.ToggleLeft(toggleInternalRect, internalGuiContent, internalParameter.boolValue);
var isPrefixRect = new Rect(rightHalfTop.x + toggleInternalWidth + halfMargin, rightHalfTop.y,
rightHalfTop.width - toggleInternalWidth - halfMargin, rightHalfTop.height);
isPrefix.boolValue = EditorGUI.ToggleLeft(isPrefixRect, G("params.pb_prefix"), isPrefix.boolValue);
var syncedContent = new GUIContent(G("params.syncmode"));
var labelSize = EditorStyles.label.CalcSize(syncedContent);
var syncedWidth = labelSize.x + EditorStyles.label.margin.right;
var syncedRect = new Rect(rightHalfSyncControlField.x, rightHalfSyncControlField.y, syncedWidth,
rightHalfSyncControlField.height);
EditorGUI.LabelField(syncedRect, syncedContent);
rightHalfSyncControlField.x += syncedWidth;
rightHalfSyncControlField.width -= syncedWidth;
EditorGUI.PropertyField(rightHalfSyncControlField, syncType, GUIContent.none);
}
else
{
EditorGUI.LabelField(leftHalf,
isPrefix.boolValue ? nameOrPrefix.stringValue + "*" : nameOrPrefix.stringValue);
EditorGUI.PropertyField(rightHalf, remapTo, GUIContent.none);
if (string.IsNullOrWhiteSpace(remapTo.stringValue))
{
var style = new GUIStyle(EditorStyles.label);
style.fontStyle = FontStyle.Italic;
var oldColor = GUI.color;
var color = GUI.color;
color.a = 0.5f;
GUI.color = color;
EditorGUI.LabelField(rightHalf, nameOrPrefix.stringValue, style);
GUI.color = oldColor;
}
}
if (isSynced)
{
var localOnly = elem.FindPropertyRelative(nameof(ParameterConfig.localOnly));
var saved = elem.FindPropertyRelative(nameof(ParameterConfig.saved));
var savedContents = new GUIContent(G("params.saved"));
var savedStyle = EditorStyles.toggle;
var savedSize = savedStyle.CalcSize(savedContents);
var savedLabelWidth = EditorStyles.label.CalcSize(savedContents).x;
var checkboxPad = EditorStyles.toggle.margin.right;
var savedPos = rightHalfDefaultValue;
savedPos.width = savedSize.x + checkboxPad * 2;
rightHalfDefaultValue.x += savedPos.width;
rightHalfDefaultValue.width -= savedPos.width;
//savedPos.x -= savedSize.x + checkboxPad * 2;
EditorGUI.LabelField(savedPos, savedContents);
savedPos.x += savedLabelWidth + checkboxPad;
savedPos.width -= savedLabelWidth - checkboxPad * 2;
saved.boolValue = EditorGUI.Toggle(savedPos, saved.boolValue);
var syncedContents = new GUIContent(G("params.synced"));
var syncedStyle = EditorStyles.toggle;
var syncedSize = syncedStyle.CalcSize(syncedContents);
var syncedLabelWidth = EditorStyles.label.CalcSize(syncedContents).x;
var syncedPos = rightHalfDefaultValue;
syncedPos.width = syncedSize.x + checkboxPad * 2;
rightHalfDefaultValue.x += syncedPos.width;
rightHalfDefaultValue.width -= syncedPos.width;
EditorGUI.LabelField(syncedPos, syncedContents);
syncedPos.x += syncedLabelWidth + checkboxPad;
syncedPos.width -= syncedLabelWidth - checkboxPad * 2;
localOnly.boolValue = !EditorGUI.Toggle(syncedPos, !localOnly.boolValue);
var defaultValueProp = elem.FindPropertyRelative(nameof(ParameterConfig.defaultValue));
var label = new GUIContent(G("params.default"));
var labelSize = EditorStyles.label.CalcSize(label);
var labelWidth = labelSize.x + EditorStyles.label.margin.right;
var labelRect = new Rect(rightHalfDefaultValue.x, rightHalfDefaultValue.y, labelWidth,
rightHalfDefaultValue.height);
EditorGUI.LabelField(labelRect, label);
rightHalfDefaultValue.x += labelWidth;
rightHalfDefaultValue.width -= labelWidth;
switch ((ParameterSyncType) syncType.enumValueIndex)
{
case ParameterSyncType.Int:
{
int val = Mathf.RoundToInt(defaultValueProp.floatValue);
val = EditorGUI.IntField(rightHalfDefaultValue, val);
defaultValueProp.floatValue = val;
break;
}
case ParameterSyncType.Float:
{
float val = defaultValueProp.floatValue;
val = EditorGUI.FloatField(rightHalfDefaultValue, val);
defaultValueProp.floatValue = val;
break;
}
case ParameterSyncType.Bool:
{
bool val = defaultValueProp.floatValue > 0.1f;
val = EditorGUI.Toggle(rightHalfDefaultValue, val);
defaultValueProp.floatValue = val ? 1.0f : 0.0f;
break;
}
default:
// Maybe we just changed sync mode?
break;
}
}
if (EditorGUI.EndChangeCheck() && index < 0)
{
var target = (ModularAvatarParameters) this.target;
// Create this implicit parameter in the backing object
var implicitIndex = -(index + 1);
serializedObject.ApplyModifiedProperties();
Undo.RecordObject(target, "Updating parameters");
_implicitParamsObj.ApplyModifiedPropertiesWithoutUndo();
var config = _implicitParams.ImplicitParameters[implicitIndex];
target.parameters.Add(config);
_selectedIndices.RemoveAt(originalIndex);
_selectedIndices.Insert(target.parameters.Count - 1, target.parameters.Count - 1);
serializedObject.Update();
_reorderableList.index = target.parameters.Count - 1;
_needsRebuild = true;
}
}
finally
{
EditorGUI.indentLevel = indentLevel;
}
}
private void DrawHeader(Rect rect)
{
var leftHalf = new Rect(rect.x, rect.y, rect.width * 0.3f, rect.height);
var rightHalf = new Rect(rect.x + leftHalf.width, rect.y, rect.width / 2, rect.height);
EditorGUI.LabelField(leftHalf, G("params.fieldname"));
if (!_devMode) EditorGUI.LabelField(rightHalf, G("params.remapto"));
}
protected override void OnInnerInspectorGUI()
{
EditorGUI.BeginChangeCheck();
_devMode = EditorGUILayout.Toggle(G("params.devmode"), _devMode);
if (EditorGUI.EndChangeCheck() || _reorderableList == null || _needsRebuild) SetupList();
Debug.Assert(_reorderableList != null, nameof(_reorderableList) + " != null");
ElementHeightCache = BuildHeightCache();
if (_devMode || _selectedIndices.Count > 0)
{
_reorderableList.DoLayoutList();
}
serializedObject.ApplyModifiedProperties();
Localization.ShowLanguageUI();
}
}
}
#endif