A game about forced loneliness, made by TACStudios
at master 17 kB view raw
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&lt;QuantizeProcessor&gt;</c>. 98 /// 99 /// <example> 100 /// <code> 101 /// public class QuantizeProcessorEditor : InputParameterEditor&lt;QuantizeProcessor&gt; 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&lt;float&gt; 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