#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 { /// /// 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(); 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(); } /// /// Helper for parameters that have defaults (usually from ). /// /// /// Has a bool toggle to switch between default and custom value. /// internal struct 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."); } 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; } } } #endif // UNITY_EDITOR