#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