File size: 16,509 Bytes
18a519f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
#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
{
    /// <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();

#if UNITY_INPUT_SYSTEM_UI_TK_ASSET_EDITOR
        /// <summary>
        /// Add visual elements for this parameter editor to a root VisualElement.
        /// </summary>
        /// <param name="root">The VisualElement that parameter editor elements should be added to.</param>
        /// <param name="onChangedCallback">A callback that will be called when any of the parameter editors
        /// changes value.</param>
        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<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&lt;QuantizeProcessor&gt;</c>.
    ///
    /// <example>
    /// <code>
    /// public class QuantizeProcessorEditor : InputParameterEditor&lt;QuantizeProcessor&gt;
    /// {
    ///     // 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&lt;float&gt;
    /// {
    ///     [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 class 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.");
            }

#if UNITY_INPUT_SYSTEM_UI_TK_ASSET_EDITOR
            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.RegisterValueChangedCallback(ChangeSettingValue);
                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(ToggleUseDefaultValue);


                var buttonContainer = new VisualElement
                {
                    style =
                    {
                        flexDirection = FlexDirection.RowReverse
                    }
                };
                m_OpenInputSettingsButton = new Button(InputSettingsProvider.Open){text = m_OpenInputSettingsLabel.text};
                m_OpenInputSettingsButton.AddToClassList("open-settings-button");
                buttonContainer.Add(m_OpenInputSettingsButton);

                settingsContainer.Add(m_FloatField);
                settingsContainer.Add(m_DefaultToggle);
                container.Add(settingsContainer);
                container.Add(m_HelpBox);
                container.Add(buttonContainer);

                root.Add(container);
            }

            private void ChangeSettingValue(ChangeEvent<float> 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 ToggleUseDefaultValue(ChangeEvent<bool> evt)
            {
                if (evt.newValue != m_UseDefaultValue)
                {
                    m_SetValue(!evt.newValue ? m_GetDefaultValue() : m_DefaultInitializedValue);
                }

                m_UseDefaultValue = evt.newValue;

                m_FloatField?.SetEnabled(!m_UseDefaultValue);
                m_HelpBox.visible = m_UseDefaultValue;
                m_OpenInputSettingsButton.visible = 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<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;
            private FloatField m_FloatField;
            private Button m_OpenInputSettingsButton;
            private Toggle m_DefaultToggle;
#if UNITY_INPUT_SYSTEM_UI_TK_ASSET_EDITOR
            private HelpBox m_HelpBox;
#endif
        }
    }
}
#endif // UNITY_EDITOR