#if UNITY_EDITOR || PACKAGE_DOCS_GENERATION using System; using System.Collections.Generic; using UnityEditor; using UnityEngine.InputSystem.Utilities; ////REVIEW: generalize this to something beyond just parameters? namespace UnityEngine.InputSystem.Editor { /// <summary> /// A custom UI for editing parameter values on a <see cref="InputProcessor"/>, <see cref="InputBindingComposite"/>, /// or <see cref="IInputInteraction"/>. /// </summary> /// <remarks> /// When implementing a custom parameter editor, use <see cref="InputParameterEditor{TObject}"/> instead. /// </remarks> /// <seealso cref="InputActionRebindingExtensions.GetParameterValue(InputAction,string,InputBinding)"/> /// <seealso cref="InputActionRebindingExtensions.ApplyParameterOverride(InputActionMap,string,PrimitiveValue,InputBinding)"/> public abstract class InputParameterEditor { /// <summary> /// The <see cref="InputProcessor"/>, <see cref="InputBindingComposite"/>, or <see cref="IInputInteraction"/> /// being edited. /// </summary> public object target { get; internal set; } /// <summary> /// Callback for implementing a custom UI. /// </summary> public abstract void OnGUI(); 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<Type, Type>(); 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 <TValue> 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<Type, Type> s_TypeLookupCache; } /// <summary> /// A custom UI for editing parameter values on a <see cref="InputProcessor"/>, /// <see cref="InputBindingComposite"/>, or <see cref="IInputInteraction"/>. /// </summary> /// <remarks> /// Custom parameter editors do not need to be registered explicitly. Say you have a custom /// <see cref="InputProcessor"/> called <c>QuantizeProcessor</c>. To define a custom editor /// UI for it, simply define a new class based on <c>InputParameterEditor<QuantizeProcessor></c>. /// /// <example> /// <code> /// 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."); /// } /// </code> /// </example> /// /// 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 <c>TooltipAttribute</c>. /// /// So, let's say that <c>QuantizeProcessor</c> from our example was defined like /// below. In that case, the result would be equivalent to the custom parameter editor /// UI defined above. /// /// <example> /// <code> /// 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; /// } /// } /// </code> /// </example> /// </remarks> public abstract class InputParameterEditor<TObject> : InputParameterEditor where TObject : class { /// <summary> /// The <see cref="InputProcessor"/>, <see cref="InputBindingComposite"/>, or <see cref="IInputInteraction"/> /// being edited. /// </summary> public new TObject target { get; private set; } /// <summary> /// Called after the parameter editor has been initialized. /// </summary> 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(); } /// <summary> /// Helper for parameters that have defaults (usually from <see cref="InputSettings"/>). /// </summary> /// <remarks> /// Has a bool toggle to switch between default and custom value. /// </remarks> internal struct CustomOrDefaultSetting { public void Initialize(string label, string tooltip, string defaultName, Func<float> getValue, Action<float> setValue, Func<float> 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."); } 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<float> m_GetValue; private Action<float> m_SetValue; private Func<float> 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; } } } #endif // UNITY_EDITOR