#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.UIElements;
////TODO: show description of interaction or processor when selected
namespace UnityEngine.InputSystem.Editor.Lists
{
///
/// Inspector-like functionality for editing parameter lists as used in .
///
///
/// This can be used for parameters on interactions, processors, and composites.
///
/// Call to set up (can be done repeatedly on the same instance). Call
/// to render.
///
/// Custom parameter GUIs can be defined by deriving from .
/// This class will automatically incorporate custom GUIs and fall back to default GUIs where no custom
/// ones are defined.
///
internal class ParameterListView
{
///
/// Invoked whenever a parameter is changed.
///
public Action onChange { get; set; }
public bool hasUIToShow => (m_Parameters != null && m_Parameters.Length > 0) || m_ParameterEditor != null;
public bool visible { get; set; }
public string name { get; set; }
///
/// Get the current parameter values according to the editor state.
///
/// An array of parameter values.
public NamedValue[] GetParameters()
{
if (m_Parameters == null)
return null;
// See if we have parameters that aren't at their default value.
var countOfParametersNotAtDefaultValue = 0;
for (var i = 0; i < m_Parameters.Length; ++i)
{
if (!m_Parameters[i].isAtDefault)
++countOfParametersNotAtDefaultValue;
}
// If not, we return null.
if (countOfParametersNotAtDefaultValue == 0)
return null;
// Collect non-default parameter values.
var result = new NamedValue[countOfParametersNotAtDefaultValue];
var index = 0;
for (var i = 0; i < m_Parameters.Length; ++i)
{
var parameter = m_Parameters[i];
if (parameter.isAtDefault)
continue;
result[index++] = parameter.value;
}
return result;
}
///
/// Initialize the parameter list view based on the given registered type that has parameters to edit. This can be
/// things such as interactions, processors, or composites.
///
/// Type of object that the parameters will be passed to at runtime.
/// We need this to be able to determine the possible set of parameters and their possible values. This
/// can be a class implementing , for example.
/// List of existing parameters. Can be empty.
public void Initialize(Type registeredType, ReadOnlyArray existingParameters)
{
if (registeredType == null)
{
// No registered type. This usually happens when data references a registration that has
// been removed in the meantime (e.g. an interaction that is no longer supported). We want
// to accept this case and simply pretend that the given type has no parameters.
Clear();
return;
}
visible = true;
// Try to instantiate object so that we can determine defaults.
object instance = null;
try
{
instance = Activator.CreateInstance(registeredType);
}
catch (Exception)
{
// Swallow. If we can't create an instance, we simply assume no defaults.
}
var parameters = new List();
////REVIEW: support properties here?
// Go through public instance fields and add every parameter found on the registered
// type.
var fields = registeredType.GetFields(BindingFlags.Public | BindingFlags.Instance);
foreach (var field in fields)
{
// Skip all fields that have an [InputControl] attribute. This is relevant
// only for composites, but we just always do it here.
if (field.GetCustomAttribute(false) != null)
continue;
// Determine parameter name from field.
var parameter = new EditableParameterValue {field = field};
var name = field.Name;
parameter.value.name = name;
// Determine parameter type from field.
var fieldType = field.FieldType;
if (fieldType.IsEnum)
{
// For enums, we want the underlying integer type.
var underlyingType = fieldType.GetEnumUnderlyingType();
var underlyingTypeCode = Type.GetTypeCode(underlyingType);
parameter.value = parameter.value.ConvertTo(underlyingTypeCode);
// Read enum names and values.
parameter.enumNames = Enum.GetNames(fieldType).Select(x => new GUIContent(x)).ToArray();
////REVIEW: this probably falls apart if multiple members have the same value
var list = new List();
foreach (var value in Enum.GetValues(fieldType))
list.Add((int)value);
parameter.enumValues = list.ToArray();
}
else
{
var typeCode = Type.GetTypeCode(fieldType);
parameter.value = parameter.value.ConvertTo(typeCode);
}
// Determine default value.
if (instance != null)
{
try
{
var value = field.GetValue(instance);
parameter.defaultValue = new NamedValue
{
name = name,
value = PrimitiveValue.FromObject(value)
};
}
catch
{
// If the getter throws, ignore. All we lose is the actual default value from
// the field.
}
}
// If the parameter already exists in the given list, maintain its value.
var existingParameterIndex = existingParameters.IndexOf(x => x.name == field.Name);
if (existingParameterIndex >= 0)
{
// Make sure we're preserving the right type.
parameter.value = existingParameters[existingParameterIndex].ConvertTo(parameter.value.type);
}
else
{
// Not assigned. Set to default.
if (parameter.defaultValue != null)
parameter.value = parameter.defaultValue.Value;
}
// Add.
parameters.Add(parameter);
}
m_Parameters = parameters.ToArray();
// See if we have a dedicated parameter editor.
var parameterEditorType = InputParameterEditor.LookupEditorForType(registeredType);
if (parameterEditorType != null)
{
// Create an editor instance and hand it the instance we created. Unlike our default
// editing logic, on this path we will be operating on an object instance that contains
// the parameter values. So on this path, we actually need to update the object to reflect
// the current parameter values.
NamedValue.ApplyAllToObject(instance, m_Parameters.Select(x => x.value));
m_ParameterEditor = (InputParameterEditor)Activator.CreateInstance(parameterEditorType);
// We have to jump through some hoops here to create instances of any CustomOrDefaultSetting fields on the
// parameter editor. This is because those types changed from structs to classes when UIToolkit was
// introduced, and we don't want to force users to have to create those instances manually on any of their
// own editors.
var genericArgumentType = TypeHelpers.GetGenericTypeArgumentFromHierarchy(parameterEditorType,
typeof(InputParameterEditor<>), 0);
if (genericArgumentType != null)
{
var fieldInfos = parameterEditorType
.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
var customOrDefaultGenericType = typeof(InputParameterEditor<>.CustomOrDefaultSetting);
var customOrDefaultType = customOrDefaultGenericType.MakeGenericType(genericArgumentType);
foreach (var customOrDefaultEditorField in fieldInfos.Where(f => f.FieldType == customOrDefaultType))
{
customOrDefaultEditorField.SetValue(m_ParameterEditor, Activator.CreateInstance(customOrDefaultEditorField.FieldType));
}
}
m_ParameterEditor.SetTarget(instance);
}
else
{
m_ParameterEditor = null;
// Create parameter labels.
m_ParameterLabels = new GUIContent[m_Parameters.Length];
for (var i = 0; i < m_Parameters.Length; ++i)
{
// Look up tooltip from field.
var tooltip = string.Empty;
var field = m_Parameters[i].field;
var tooltipAttribute = field.GetCustomAttribute();
if (tooltipAttribute != null)
tooltip = tooltipAttribute.tooltip;
// Create label.
var niceName = ObjectNames.NicifyVariableName(m_Parameters[i].value.name);
m_ParameterLabels[i] = new GUIContent(niceName, tooltip);
}
}
}
public void Clear()
{
m_Parameters = null;
m_ParameterEditor = null;
}
#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
public void OnDrawVisualElements(VisualElement root)
{
if (m_ParameterEditor != null)
{
m_ParameterEditor.OnDrawVisualElements(root, OnValuesChanged);
return;
}
if (m_Parameters == null)
return;
void OnValueChanged(ref EditableParameterValue parameter, object result, int i)
{
parameter.value.value = PrimitiveValue.FromObject(result).ConvertTo(parameter.value.type);
m_Parameters[i] = parameter;
}
void OnEditEnd()
{
onChange?.Invoke();
}
for (var i = 0; i < m_Parameters.Length; i++)
{
var parameter = m_Parameters[i];
var label = m_ParameterLabels[i];
var closedIndex = i;
if (parameter.isEnum)
{
var intValue = parameter.value.value.ToInt32();
var field = new DropdownField(label.text, parameter.enumNames.Select(x => x.text).ToList(), intValue);
field.tooltip = label.tooltip;
field.RegisterValueChangedCallback(evt => OnValueChanged(ref parameter, field.index, closedIndex));
field.RegisterCallback(_ => OnEditEnd());
root.Add(field);
}
else if (parameter.value.type == TypeCode.Int64 || parameter.value.type == TypeCode.UInt64)
{
var longValue = parameter.value.value.ToInt64();
var field = new LongField(label.text) { value = longValue, tooltip = label.tooltip };
field.RegisterValueChangedCallback(evt => OnValueChanged(ref parameter, evt.newValue, closedIndex));
field.RegisterCallback(_ => OnEditEnd());
root.Add(field);
}
else if (parameter.value.type.IsInt())
{
var intValue = parameter.value.value.ToInt32();
var field = new IntegerField(label.text) { value = intValue, tooltip = label.tooltip };
field.RegisterValueChangedCallback(evt => OnValueChanged(ref parameter, evt.newValue, closedIndex));
field.RegisterCallback(_ => OnEditEnd());
root.Add(field);
}
else if (parameter.value.type == TypeCode.Single)
{
var floatValue = parameter.value.value.ToSingle();
var field = new FloatField(label.text) { value = floatValue, tooltip = label.tooltip };
field.RegisterValueChangedCallback(evt => OnValueChanged(ref parameter, evt.newValue, closedIndex));
field.RegisterCallback(_ => OnEditEnd());
root.Add(field);
}
else if (parameter.value.type == TypeCode.Double)
{
var floatValue = parameter.value.value.ToDouble();
var field = new DoubleField(label.text) { value = floatValue, tooltip = label.tooltip };
field.RegisterValueChangedCallback(evt => OnValueChanged(ref parameter, evt.newValue, closedIndex));
field.RegisterCallback(_ => OnEditEnd());
root.Add(field);
}
else if (parameter.value.type == TypeCode.Boolean)
{
var boolValue = parameter.value.value.ToBoolean();
var field = new Toggle(label.text) { value = boolValue, tooltip = label.tooltip };
field.RegisterValueChangedCallback(evt => OnValueChanged(ref parameter, evt.newValue, closedIndex));
field.RegisterValueChangedCallback(_ => OnEditEnd());
root.Add(field);
}
}
}
#endif
private void OnValuesChanged()
{
ReadParameterValuesFrom(m_ParameterEditor.target);
onChange?.Invoke();
}
public void OnGUI()
{
// If we have a dedicated parameter editor, let it do all the work.
if (m_ParameterEditor != null)
{
EditorGUI.BeginChangeCheck();
m_ParameterEditor.OnGUI();
if (EditorGUI.EndChangeCheck())
{
ReadParameterValuesFrom(m_ParameterEditor.target);
onChange?.Invoke();
}
return;
}
#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
// handled by OnDrawVisualElements with UI Toolkit
if (!InputSystem.settings.useIMGUIEditorForAssets) return;
#endif
// Otherwise, fall back to our default logic.
if (m_Parameters == null)
return;
for (var i = 0; i < m_Parameters.Length; i++)
{
var parameter = m_Parameters[i];
var label = m_ParameterLabels[i];
EditorGUI.BeginChangeCheck();
object result = null;
if (parameter.isEnum)
{
var intValue = parameter.value.value.ToInt32();
result = EditorGUILayout.IntPopup(label, intValue, parameter.enumNames, parameter.enumValues);
}
else if (parameter.value.type == TypeCode.Int64 || parameter.value.type == TypeCode.UInt64)
{
var longValue = parameter.value.value.ToInt64();
result = EditorGUILayout.LongField(label, longValue);
}
else if (parameter.value.type.IsInt())
{
var intValue = parameter.value.value.ToInt32();
result = EditorGUILayout.IntField(label, intValue);
}
else if (parameter.value.type == TypeCode.Single)
{
var floatValue = parameter.value.value.ToSingle();
result = EditorGUILayout.FloatField(label, floatValue);
}
else if (parameter.value.type == TypeCode.Double)
{
var floatValue = parameter.value.value.ToDouble();
result = EditorGUILayout.DoubleField(label, floatValue);
}
else if (parameter.value.type == TypeCode.Boolean)
{
var boolValue = parameter.value.value.ToBoolean();
result = EditorGUILayout.Toggle(label, boolValue);
}
if (EditorGUI.EndChangeCheck())
{
parameter.value.value = PrimitiveValue.FromObject(result).ConvertTo(parameter.value.type);
m_Parameters[i] = parameter;
onChange?.Invoke();
}
}
}
////REVIEW: check whether parameters have *actually* changed?
///
/// Refresh from the current parameter values in .
///
/// An instance of the current type we are editing parameters on.
private void ReadParameterValuesFrom(object target)
{
if (m_Parameters == null)
return;
for (var i = 0; i < m_Parameters.Length; ++i)
{
var parameter = m_Parameters[i];
object value = null;
try
{
value = parameter.field.GetValue(target);
}
catch
{
// Ignore exceptions from getters.
}
m_Parameters[i].value.value = PrimitiveValue.FromObject(value).ConvertTo(parameter.value.type);
}
}
private InputParameterEditor m_ParameterEditor;
private EditableParameterValue[] m_Parameters;
private GUIContent[] m_ParameterLabels;
private struct EditableParameterValue
{
public NamedValue value;
public NamedValue? defaultValue;
public int[] enumValues;
public GUIContent[] enumNames;
public FieldInfo field;
public bool isEnum => enumValues != null;
public bool isAtDefault => defaultValue != null && value == defaultValue.Value;
}
}
}
#endif // UNITY_EDITOR