A game about forced loneliness, made by TACStudios
1#if UNITY_EDITOR || PACKAGE_DOCS_GENERATION
2using System;
3using System.Collections.Generic;
4using UnityEditor;
5using UnityEditor.UIElements;
6using UnityEngine.InputSystem.Utilities;
7using UnityEngine.UIElements;
8
9////REVIEW: generalize this to something beyond just parameters?
10
11namespace UnityEngine.InputSystem.Editor
12{
13 /// <summary>
14 /// A custom UI for editing parameter values on a <see cref="InputProcessor"/>, <see cref="InputBindingComposite"/>,
15 /// or <see cref="IInputInteraction"/>.
16 /// </summary>
17 /// <remarks>
18 /// When implementing a custom parameter editor, use <see cref="InputParameterEditor{TObject}"/> instead.
19 /// </remarks>
20 /// <seealso cref="InputActionRebindingExtensions.GetParameterValue(InputAction,string,InputBinding)"/>
21 /// <seealso cref="InputActionRebindingExtensions.ApplyParameterOverride(InputActionMap,string,PrimitiveValue,InputBinding)"/>
22 public abstract class InputParameterEditor
23 {
24 /// <summary>
25 /// The <see cref="InputProcessor"/>, <see cref="InputBindingComposite"/>, or <see cref="IInputInteraction"/>
26 /// being edited.
27 /// </summary>
28 public object target { get; internal set; }
29
30 /// <summary>
31 /// Callback for implementing a custom UI.
32 /// </summary>
33 public abstract void OnGUI();
34
35#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
36 /// <summary>
37 /// Add visual elements for this parameter editor to a root VisualElement.
38 /// </summary>
39 /// <param name="root">The VisualElement that parameter editor elements should be added to.</param>
40 /// <param name="onChangedCallback">A callback that will be called when any of the parameter editors
41 /// changes value.</param>
42 public abstract void OnDrawVisualElements(VisualElement root, Action onChangedCallback);
43#endif
44
45 internal abstract void SetTarget(object target);
46
47 internal static Type LookupEditorForType(Type type)
48 {
49 if (type == null)
50 throw new ArgumentNullException(nameof(type));
51
52 if (s_TypeLookupCache == null)
53 {
54 s_TypeLookupCache = new Dictionary<Type, Type>();
55 foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
56 {
57 foreach (var typeInfo in assembly.DefinedTypes)
58 {
59 // Only looking for classes.
60 if (!typeInfo.IsClass)
61 continue;
62
63 var definedType = typeInfo.AsType();
64 if (definedType == null)
65 continue;
66
67 // Only looking for InputParameterEditors.
68 if (!typeof(InputParameterEditor).IsAssignableFrom(definedType))
69 continue;
70
71 // Grab <TValue> parameter from InputParameterEditor<>.
72 var objectType =
73 TypeHelpers.GetGenericTypeArgumentFromHierarchy(definedType, typeof(InputParameterEditor<>),
74 0);
75 if (objectType == null)
76 continue;
77
78 s_TypeLookupCache[objectType] = definedType;
79 }
80 }
81 }
82
83 s_TypeLookupCache.TryGetValue(type, out var editorType);
84 return editorType;
85 }
86
87 private static Dictionary<Type, Type> s_TypeLookupCache;
88 }
89
90 /// <summary>
91 /// A custom UI for editing parameter values on a <see cref="InputProcessor"/>,
92 /// <see cref="InputBindingComposite"/>, or <see cref="IInputInteraction"/>.
93 /// </summary>
94 /// <remarks>
95 /// Custom parameter editors do not need to be registered explicitly. Say you have a custom
96 /// <see cref="InputProcessor"/> called <c>QuantizeProcessor</c>. To define a custom editor
97 /// UI for it, simply define a new class based on <c>InputParameterEditor<QuantizeProcessor></c>.
98 ///
99 /// <example>
100 /// <code>
101 /// public class QuantizeProcessorEditor : InputParameterEditor<QuantizeProcessor>
102 /// {
103 /// // You can put initialization logic in OnEnable, if you need it.
104 /// public override void OnEnable()
105 /// {
106 /// // Use the 'target' property to access the QuantizeProcessor instance.
107 /// }
108 ///
109 /// // In OnGUI, you can define custom UI elements. Use EditorGUILayout to lay
110 /// // out the controls.
111 /// public override void OnGUI()
112 /// {
113 /// // Say that QuantizeProcessor has a "stepping" property that determines
114 /// // the stepping distance for discrete values returned by the processor.
115 /// // We can expose it here as a float field. To apply the modification to
116 /// // processor object, we just assign the value back to the field on it.
117 /// target.stepping = EditorGUILayout.FloatField(
118 /// m_SteppingLabel, target.stepping);
119 /// }
120 ///
121 /// private GUIContent m_SteppingLabel = new GUIContent("Stepping",
122 /// "Discrete stepping with which input values will be quantized.");
123 /// }
124 /// </code>
125 /// </example>
126 ///
127 /// Note that a parameter editor takes over the entire editing UI for the object and
128 /// not just the editing of specific parameters.
129 ///
130 /// The default parameter editor will derive names from the names of the respective
131 /// fields just like the Unity inspector does. Also, it will respect tooltips applied
132 /// to these fields with Unity's <c>TooltipAttribute</c>.
133 ///
134 /// So, let's say that <c>QuantizeProcessor</c> from our example was defined like
135 /// below. In that case, the result would be equivalent to the custom parameter editor
136 /// UI defined above.
137 ///
138 /// <example>
139 /// <code>
140 /// public class QuantizeProcessor : InputProcessor<float>
141 /// {
142 /// [Tooltip("Discrete stepping with which input values will be quantized.")]
143 /// public float stepping;
144 ///
145 /// public override float Process(float value, InputControl control)
146 /// {
147 /// return value - value % stepping;
148 /// }
149 /// }
150 /// </code>
151 /// </example>
152 /// </remarks>
153 public abstract class InputParameterEditor<TObject> : InputParameterEditor
154 where TObject : class
155 {
156 /// <summary>
157 /// The <see cref="InputProcessor"/>, <see cref="InputBindingComposite"/>, or <see cref="IInputInteraction"/>
158 /// being edited.
159 /// </summary>
160 public new TObject target { get; private set; }
161
162 /// <summary>
163 /// Called after the parameter editor has been initialized.
164 /// </summary>
165 protected virtual void OnEnable()
166 {
167 }
168
169 internal override void SetTarget(object target)
170 {
171 if (target == null)
172 throw new ArgumentNullException(nameof(target));
173
174 if (!(target is TObject targetOfType))
175 throw new ArgumentException(
176 $"Expecting object of type '{typeof(TObject).Name}' but got object of type '{target.GetType().Name}' instead",
177 nameof(target));
178
179 this.target = targetOfType;
180 base.target = targetOfType;
181
182 OnEnable();
183 }
184
185#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
186 /// <summary>
187 /// Default stub implementation of <see cref="InputParameterEditor.OnDrawVisualElements"/>.
188 /// Should be overridden to create the desired UI.
189 /// </summary>
190 public override void OnDrawVisualElements(VisualElement root, Action onChangedCallback)
191 {
192 }
193
194#endif
195
196 /// <summary>
197 /// Helper for parameters that have defaults (usually from <see cref="InputSettings"/>).
198 /// </summary>
199 /// <remarks>
200 /// Has a bool toggle to switch between default and custom value.
201 /// </remarks>
202 internal class CustomOrDefaultSetting
203 {
204 public void Initialize(string label, string tooltip, string defaultName, Func<float> getValue,
205 Action<float> setValue, Func<float> getDefaultValue, bool defaultComesFromInputSettings = true,
206 float defaultInitializedValue = default)
207 {
208 m_GetValue = getValue;
209 m_SetValue = setValue;
210 m_GetDefaultValue = getDefaultValue;
211 m_ToggleLabel = EditorGUIUtility.TrTextContent("Default",
212 defaultComesFromInputSettings
213 ? $"If enabled, the default {label.ToLower()} configured globally in the input settings is used. See Edit >> Project Settings... >> Input (NEW)."
214 : "If enabled, the default value is used.");
215 m_ValueLabel = EditorGUIUtility.TrTextContent(label, tooltip);
216 if (defaultComesFromInputSettings)
217 m_OpenInputSettingsLabel = EditorGUIUtility.TrTextContent("Open Input Settings");
218 m_DefaultInitializedValue = defaultInitializedValue;
219 m_UseDefaultValue = Mathf.Approximately(getValue(), defaultInitializedValue);
220 m_DefaultComesFromInputSettings = defaultComesFromInputSettings;
221 m_HelpBoxText =
222 EditorGUIUtility.TrTextContent(
223 $"Uses \"{defaultName}\" set in project-wide input settings.");
224 }
225
226#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
227 public void OnDrawVisualElements(VisualElement root, Action onChangedCallback)
228 {
229 var value = m_GetValue();
230
231 if (m_UseDefaultValue)
232 value = m_GetDefaultValue();
233
234 // 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.
235 // Revert it back to default to show a nice readable value in UI.
236 // ReSharper disable once CompareOfFloatsByEqualityOperator
237 if ((value - float.Epsilon) == m_DefaultInitializedValue)
238 value = m_DefaultInitializedValue;
239
240 var container = new VisualElement();
241 var settingsContainer = new VisualElement { style = { flexDirection = FlexDirection.Row } };
242
243
244 m_FloatField = new FloatField(m_ValueLabel.text) { value = value };
245 m_FloatField.Q("unity-text-input").AddToClassList("float-field");
246 m_FloatField.RegisterValueChangedCallback(ChangeSettingValue);
247 m_FloatField.RegisterCallback<BlurEvent>(_ => OnEditEnd(onChangedCallback));
248 m_FloatField.SetEnabled(!m_UseDefaultValue);
249
250 m_HelpBox = new HelpBox(m_HelpBoxText.text, HelpBoxMessageType.None);
251
252 m_DefaultToggle = new Toggle("Default") { value = m_UseDefaultValue };
253 m_DefaultToggle.RegisterValueChangedCallback(evt => ToggleUseDefaultValue(evt, onChangedCallback));
254
255
256 var buttonContainer = new VisualElement
257 {
258 style =
259 {
260 flexDirection = FlexDirection.RowReverse
261 }
262 };
263 m_OpenInputSettingsButton = new Button(InputSettingsProvider.Open){text = m_OpenInputSettingsLabel.text};
264 m_OpenInputSettingsButton.AddToClassList("open-settings-button");
265
266 settingsContainer.Add(m_FloatField);
267 settingsContainer.Add(m_DefaultToggle);
268 container.Add(settingsContainer);
269
270 if (m_UseDefaultValue)
271 {
272 buttonContainer.Add(m_OpenInputSettingsButton);
273 container.Add(m_HelpBox);
274 }
275
276 container.Add(buttonContainer);
277
278 root.Add(container);
279 }
280
281 private void ChangeSettingValue(ChangeEvent<float> evt)
282 {
283 if (m_UseDefaultValue) return;
284
285 // ReSharper disable once CompareOfFloatsByEqualityOperator
286 if (evt.newValue == m_DefaultInitializedValue)
287 {
288 // If user sets a value that is equal to default initialized, change value slightly so it doesn't pass potential default checks.
289 ////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),
290 ////so instead it would be better to have a separate bool to show if value is present or not.
291 m_SetValue(evt.newValue + float.Epsilon);
292 }
293 else
294 {
295 m_SetValue(evt.newValue);
296 }
297 }
298
299 private void OnEditEnd(Action onChangedCallback)
300 {
301 onChangedCallback.Invoke();
302 }
303
304 private void ToggleUseDefaultValue(ChangeEvent<bool> evt, Action onChangedCallback)
305 {
306 if (evt.newValue != m_UseDefaultValue)
307 {
308 m_SetValue(!evt.newValue ? m_GetDefaultValue() : m_DefaultInitializedValue);
309 onChangedCallback.Invoke();
310 }
311
312 m_UseDefaultValue = evt.newValue;
313 m_FloatField?.SetEnabled(!m_UseDefaultValue);
314 }
315
316#endif
317
318 public void OnGUI()
319 {
320 EditorGUILayout.BeginHorizontal();
321 EditorGUI.BeginDisabledGroup(m_UseDefaultValue);
322
323 var value = m_GetValue();
324
325 if (m_UseDefaultValue)
326 value = m_GetDefaultValue();
327
328 // 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.
329 // Revert it back to default to show a nice readable value in UI.
330 // ReSharper disable once CompareOfFloatsByEqualityOperator
331 if ((value - float.Epsilon) == m_DefaultInitializedValue)
332 value = m_DefaultInitializedValue;
333
334 ////TODO: use slider rather than float field
335 var newValue = EditorGUILayout.FloatField(m_ValueLabel, value, GUILayout.ExpandWidth(false));
336 if (!m_UseDefaultValue)
337 {
338 // ReSharper disable once CompareOfFloatsByEqualityOperator
339 if (newValue == m_DefaultInitializedValue)
340 // If user sets a value that is equal to default initialized, change value slightly so it doesn't pass potential default checks.
341 ////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),
342 ////so instead it would be better to have a separate bool to show if value is present or not.
343 m_SetValue(newValue + float.Epsilon);
344 else
345 m_SetValue(newValue);
346 }
347
348 EditorGUI.EndDisabledGroup();
349
350 var newUseDefault = GUILayout.Toggle(m_UseDefaultValue, m_ToggleLabel, GUILayout.ExpandWidth(false));
351 if (newUseDefault != m_UseDefaultValue)
352 {
353 if (!newUseDefault)
354 m_SetValue(m_GetDefaultValue());
355 else
356 m_SetValue(m_DefaultInitializedValue);
357 }
358
359 m_UseDefaultValue = newUseDefault;
360 EditorGUILayout.EndHorizontal();
361
362 // If we're using a default from global InputSettings, show info text for that and provide
363 // button to open input settings.
364 if (m_UseDefaultValue && m_DefaultComesFromInputSettings)
365 {
366 EditorGUILayout.HelpBox(m_HelpBoxText);
367 EditorGUILayout.BeginHorizontal();
368 GUILayout.FlexibleSpace();
369 if (GUILayout.Button(m_OpenInputSettingsLabel, EditorStyles.miniButton))
370 InputSettingsProvider.Open();
371 EditorGUILayout.EndHorizontal();
372 }
373 }
374
375 private Func<float> m_GetValue;
376 private Action<float> m_SetValue;
377 private Func<float> m_GetDefaultValue;
378 private bool m_UseDefaultValue;
379 private bool m_DefaultComesFromInputSettings;
380 private float m_DefaultInitializedValue;
381 private GUIContent m_ToggleLabel;
382 private GUIContent m_ValueLabel;
383 private GUIContent m_OpenInputSettingsLabel;
384 private GUIContent m_HelpBoxText;
385 private FloatField m_FloatField;
386 private Button m_OpenInputSettingsButton;
387 private Toggle m_DefaultToggle;
388#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
389 private HelpBox m_HelpBox;
390#endif
391 }
392 }
393}
394#endif // UNITY_EDITOR