#if UNITY_EDITOR || PACKAGE_DOCS_GENERATION
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.UIElements;
////REVIEW: generalize this to something beyond just parameters?
namespace UnityEngine.InputSystem.Editor
{
///
/// A custom UI for editing parameter values on a , ,
/// or .
///
///
/// When implementing a custom parameter editor, use instead.
///
///
///
public abstract class InputParameterEditor
{
///
/// The , , or
/// being edited.
///
public object target { get; internal set; }
///
/// Callback for implementing a custom UI.
///
public abstract void OnGUI();
#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
///
/// Add visual elements for this parameter editor to a root VisualElement.
///
/// The VisualElement that parameter editor elements should be added to.
/// A callback that will be called when any of the parameter editors
/// changes value.
public abstract void OnDrawVisualElements(VisualElement root, Action onChangedCallback);
#endif
internal abstract void SetTarget(object target);
internal static Type LookupEditorForType(Type type)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
if (s_TypeLookupCache == null)
{
s_TypeLookupCache = new Dictionary();
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (var typeInfo in assembly.DefinedTypes)
{
// Only looking for classes.
if (!typeInfo.IsClass)
continue;
var definedType = typeInfo.AsType();
if (definedType == null)
continue;
// Only looking for InputParameterEditors.
if (!typeof(InputParameterEditor).IsAssignableFrom(definedType))
continue;
// Grab parameter from InputParameterEditor<>.
var objectType =
TypeHelpers.GetGenericTypeArgumentFromHierarchy(definedType, typeof(InputParameterEditor<>),
0);
if (objectType == null)
continue;
s_TypeLookupCache[objectType] = definedType;
}
}
}
s_TypeLookupCache.TryGetValue(type, out var editorType);
return editorType;
}
private static Dictionary s_TypeLookupCache;
}
///
/// A custom UI for editing parameter values on a ,
/// , or .
///
///
/// Custom parameter editors do not need to be registered explicitly. Say you have a custom
/// called QuantizeProcessor. To define a custom editor
/// UI for it, simply define a new class based on InputParameterEditor<QuantizeProcessor>.
///
///
///
/// public class QuantizeProcessorEditor : InputParameterEditor<QuantizeProcessor>
/// {
/// // You can put initialization logic in OnEnable, if you need it.
/// public override void OnEnable()
/// {
/// // Use the 'target' property to access the QuantizeProcessor instance.
/// }
///
/// // In OnGUI, you can define custom UI elements. Use EditorGUILayout to lay
/// // out the controls.
/// public override void OnGUI()
/// {
/// // Say that QuantizeProcessor has a "stepping" property that determines
/// // the stepping distance for discrete values returned by the processor.
/// // We can expose it here as a float field. To apply the modification to
/// // processor object, we just assign the value back to the field on it.
/// target.stepping = EditorGUILayout.FloatField(
/// m_SteppingLabel, target.stepping);
/// }
///
/// private GUIContent m_SteppingLabel = new GUIContent("Stepping",
/// "Discrete stepping with which input values will be quantized.");
/// }
///
///
///
/// Note that a parameter editor takes over the entire editing UI for the object and
/// not just the editing of specific parameters.
///
/// The default parameter editor will derive names from the names of the respective
/// fields just like the Unity inspector does. Also, it will respect tooltips applied
/// to these fields with Unity's TooltipAttribute.
///
/// So, let's say that QuantizeProcessor from our example was defined like
/// below. In that case, the result would be equivalent to the custom parameter editor
/// UI defined above.
///
///
///
/// public class QuantizeProcessor : InputProcessor<float>
/// {
/// [Tooltip("Discrete stepping with which input values will be quantized.")]
/// public float stepping;
///
/// public override float Process(float value, InputControl control)
/// {
/// return value - value % stepping;
/// }
/// }
///
///
///
public abstract class InputParameterEditor : InputParameterEditor
where TObject : class
{
///
/// The , , or
/// being edited.
///
public new TObject target { get; private set; }
///
/// Called after the parameter editor has been initialized.
///
protected virtual void OnEnable()
{
}
internal override void SetTarget(object target)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
if (!(target is TObject targetOfType))
throw new ArgumentException(
$"Expecting object of type '{typeof(TObject).Name}' but got object of type '{target.GetType().Name}' instead",
nameof(target));
this.target = targetOfType;
base.target = targetOfType;
OnEnable();
}
#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
///
/// Default stub implementation of .
/// Should be overridden to create the desired UI.
///
public override void OnDrawVisualElements(VisualElement root, Action onChangedCallback)
{
}
#endif
///
/// Helper for parameters that have defaults (usually from ).
///
///
/// Has a bool toggle to switch between default and custom value.
///
internal class CustomOrDefaultSetting
{
public void Initialize(string label, string tooltip, string defaultName, Func getValue,
Action setValue, Func getDefaultValue, bool defaultComesFromInputSettings = true,
float defaultInitializedValue = default)
{
m_GetValue = getValue;
m_SetValue = setValue;
m_GetDefaultValue = getDefaultValue;
m_ToggleLabel = EditorGUIUtility.TrTextContent("Default",
defaultComesFromInputSettings
? $"If enabled, the default {label.ToLower()} configured globally in the input settings is used. See Edit >> Project Settings... >> Input (NEW)."
: "If enabled, the default value is used.");
m_ValueLabel = EditorGUIUtility.TrTextContent(label, tooltip);
if (defaultComesFromInputSettings)
m_OpenInputSettingsLabel = EditorGUIUtility.TrTextContent("Open Input Settings");
m_DefaultInitializedValue = defaultInitializedValue;
m_UseDefaultValue = Mathf.Approximately(getValue(), defaultInitializedValue);
m_DefaultComesFromInputSettings = defaultComesFromInputSettings;
m_HelpBoxText =
EditorGUIUtility.TrTextContent(
$"Uses \"{defaultName}\" set in project-wide input settings.");
}
#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
public void OnDrawVisualElements(VisualElement root, Action onChangedCallback)
{
var value = m_GetValue();
if (m_UseDefaultValue)
value = m_GetDefaultValue();
// If previous value was an epsilon away from default value, it most likely means that value was set by our own code down in this method.
// Revert it back to default to show a nice readable value in UI.
// ReSharper disable once CompareOfFloatsByEqualityOperator
if ((value - float.Epsilon) == m_DefaultInitializedValue)
value = m_DefaultInitializedValue;
var container = new VisualElement();
var settingsContainer = new VisualElement { style = { flexDirection = FlexDirection.Row } };
m_FloatField = new FloatField(m_ValueLabel.text) { value = value };
m_FloatField.Q("unity-text-input").AddToClassList("float-field");
m_FloatField.RegisterValueChangedCallback(ChangeSettingValue);
m_FloatField.RegisterCallback(_ => OnEditEnd(onChangedCallback));
m_FloatField.SetEnabled(!m_UseDefaultValue);
m_HelpBox = new HelpBox(m_HelpBoxText.text, HelpBoxMessageType.None);
m_DefaultToggle = new Toggle("Default") { value = m_UseDefaultValue };
m_DefaultToggle.RegisterValueChangedCallback(evt => ToggleUseDefaultValue(evt, onChangedCallback));
var buttonContainer = new VisualElement
{
style =
{
flexDirection = FlexDirection.RowReverse
}
};
m_OpenInputSettingsButton = new Button(InputSettingsProvider.Open){text = m_OpenInputSettingsLabel.text};
m_OpenInputSettingsButton.AddToClassList("open-settings-button");
settingsContainer.Add(m_FloatField);
settingsContainer.Add(m_DefaultToggle);
container.Add(settingsContainer);
if (m_UseDefaultValue)
{
buttonContainer.Add(m_OpenInputSettingsButton);
container.Add(m_HelpBox);
}
container.Add(buttonContainer);
root.Add(container);
}
private void ChangeSettingValue(ChangeEvent evt)
{
if (m_UseDefaultValue) return;
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (evt.newValue == m_DefaultInitializedValue)
{
// If user sets a value that is equal to default initialized, change value slightly so it doesn't pass potential default checks.
////TODO: refactor all of this to use tri-state values instead, there is no obvious float value that we can use as default (well maybe NaN),
////so instead it would be better to have a separate bool to show if value is present or not.
m_SetValue(evt.newValue + float.Epsilon);
}
else
{
m_SetValue(evt.newValue);
}
}
private void OnEditEnd(Action onChangedCallback)
{
onChangedCallback.Invoke();
}
private void ToggleUseDefaultValue(ChangeEvent evt, Action onChangedCallback)
{
if (evt.newValue != m_UseDefaultValue)
{
m_SetValue(!evt.newValue ? m_GetDefaultValue() : m_DefaultInitializedValue);
onChangedCallback.Invoke();
}
m_UseDefaultValue = evt.newValue;
m_FloatField?.SetEnabled(!m_UseDefaultValue);
}
#endif
public void OnGUI()
{
EditorGUILayout.BeginHorizontal();
EditorGUI.BeginDisabledGroup(m_UseDefaultValue);
var value = m_GetValue();
if (m_UseDefaultValue)
value = m_GetDefaultValue();
// If previous value was an epsilon away from default value, it most likely means that value was set by our own code down in this method.
// Revert it back to default to show a nice readable value in UI.
// ReSharper disable once CompareOfFloatsByEqualityOperator
if ((value - float.Epsilon) == m_DefaultInitializedValue)
value = m_DefaultInitializedValue;
////TODO: use slider rather than float field
var newValue = EditorGUILayout.FloatField(m_ValueLabel, value, GUILayout.ExpandWidth(false));
if (!m_UseDefaultValue)
{
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (newValue == m_DefaultInitializedValue)
// If user sets a value that is equal to default initialized, change value slightly so it doesn't pass potential default checks.
////TODO: refactor all of this to use tri-state values instead, there is no obvious float value that we can use as default (well maybe NaN),
////so instead it would be better to have a separate bool to show if value is present or not.
m_SetValue(newValue + float.Epsilon);
else
m_SetValue(newValue);
}
EditorGUI.EndDisabledGroup();
var newUseDefault = GUILayout.Toggle(m_UseDefaultValue, m_ToggleLabel, GUILayout.ExpandWidth(false));
if (newUseDefault != m_UseDefaultValue)
{
if (!newUseDefault)
m_SetValue(m_GetDefaultValue());
else
m_SetValue(m_DefaultInitializedValue);
}
m_UseDefaultValue = newUseDefault;
EditorGUILayout.EndHorizontal();
// If we're using a default from global InputSettings, show info text for that and provide
// button to open input settings.
if (m_UseDefaultValue && m_DefaultComesFromInputSettings)
{
EditorGUILayout.HelpBox(m_HelpBoxText);
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button(m_OpenInputSettingsLabel, EditorStyles.miniButton))
InputSettingsProvider.Open();
EditorGUILayout.EndHorizontal();
}
}
private Func m_GetValue;
private Action m_SetValue;
private Func m_GetDefaultValue;
private bool m_UseDefaultValue;
private bool m_DefaultComesFromInputSettings;
private float m_DefaultInitializedValue;
private GUIContent m_ToggleLabel;
private GUIContent m_ValueLabel;
private GUIContent m_OpenInputSettingsLabel;
private GUIContent m_HelpBoxText;
private FloatField m_FloatField;
private Button m_OpenInputSettingsButton;
private Toggle m_DefaultToggle;
#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
private HelpBox m_HelpBox;
#endif
}
}
}
#endif // UNITY_EDITOR