#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
namespace UnityEngine.InputSystem.Editor
{
///
/// Base class for property drawers that display input actions.
///
internal abstract class InputActionDrawerBase : PropertyDrawer
{
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
InitTreeIfNeeded(property);
return GetOrCreateViewData(property).TreeView.totalHeight;
}
public override bool CanCacheInspectorGUI(SerializedProperty property)
{
return false;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
InitTreeIfNeeded(property);
EditorGUI.BeginProperty(position, label, property);
SetNameIfNotSet(property);
GetOrCreateViewData(property).TreeView.OnGUI(position);
EditorGUI.EndProperty();
}
private void InitTreeIfNeeded(SerializedProperty property)
{
// NOTE: Unlike InputActionEditorWindow, we do not need to protect against the SerializedObject
// changing behind our backs by undo/redo here. Being a PropertyDrawer, we will automatically
// get recreated by Unity when it touches our serialized data.
var viewData = GetOrCreateViewData(property);
var propertyIsClone = IsPropertyAClone(property);
if (!propertyIsClone && viewData.TreeView != null && viewData.TreeView.serializedObject == property.serializedObject)
return;
if (propertyIsClone)
ResetProperty(property);
viewData.TreeView = new InputActionTreeView(property.serializedObject)
{
onBuildTree = () => BuildTree(property),
onDoubleClick = item => OnItemDoubleClicked(item, property),
drawActionPropertiesButton = true,
title = (GetPropertyTitle(property), property.GetTooltip())
};
viewData.TreeView.Reload();
}
private void SetNameIfNotSet(SerializedProperty actionProperty)
{
var nameProperty = actionProperty.FindPropertyRelative("m_Name");
if (!string.IsNullOrEmpty(nameProperty.stringValue))
return;
// Special case for InputActionProperty where we want to take the name not from
// the m_Action property embedded in it but rather from the InputActionProperty field
// itself.
var name = actionProperty.displayName;
var parent = actionProperty.GetParentProperty();
if (parent != null && parent.type == "InputActionProperty")
name = parent.displayName;
var suffix = GetSuffixToRemoveFromPropertyDisplayName();
if (name.EndsWith(suffix))
name = name.Substring(0, name.Length - suffix.Length);
// If it's a singleton action, we also need to adjust the InputBinding.action
// property values in its binding list.
var singleActionBindings = actionProperty.FindPropertyRelative("m_SingletonActionBindings");
if (singleActionBindings != null)
{
var bindingCount = singleActionBindings.arraySize;
for (var i = 0; i < bindingCount; ++i)
{
var binding = singleActionBindings.GetArrayElementAtIndex(i);
var actionNameProperty = binding.FindPropertyRelative("m_Action");
actionNameProperty.stringValue = name;
}
}
nameProperty.stringValue = name;
actionProperty.serializedObject.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(actionProperty.serializedObject.targetObject);
}
private static string GetPropertyTitle(SerializedProperty property)
{
var propertyTitleNumeral = string.Empty;
if (property.GetParentProperty() != null && property.GetParentProperty().isArray)
propertyTitleNumeral = $" {property.GetIndexOfArrayElement()}";
if (property.displayName != null &&
property.displayName.Length > 0 &&
(property.type == nameof(InputAction) || property.type == nameof(InputActionMap)))
{
return $"{property.displayName}{propertyTitleNumeral}";
}
return property.type == nameof(InputActionMap) ? $"Input Action Map{propertyTitleNumeral}" : $"Input Action{propertyTitleNumeral}";
}
private void OnItemDoubleClicked(ActionTreeItemBase item, SerializedProperty property)
{
var viewData = GetOrCreateViewData(property);
// Double-clicking on binding or action item opens property popup.
PropertiesViewBase propertyView = null;
if (item is BindingTreeItem)
{
if (viewData.ControlPickerState == null)
viewData.ControlPickerState = new InputControlPickerState();
propertyView = new InputBindingPropertiesView(item.property,
controlPickerState: viewData.ControlPickerState,
expectedControlLayout: item.expectedControlLayout,
onChange:
change => viewData.TreeView.Reload());
}
else if (item is ActionTreeItem)
{
propertyView = new InputActionPropertiesView(item.property,
onChange: change => viewData.TreeView.Reload());
}
if (propertyView != null)
{
var rect = new Rect(GUIUtility.GUIToScreenPoint(Event.current.mousePosition), Vector2.zero);
PropertiesViewPopup.Show(rect, propertyView);
}
}
private InputActionDrawerViewData GetOrCreateViewData(SerializedProperty property)
{
if (m_PerPropertyViewData == null)
m_PerPropertyViewData = new Dictionary();
if (m_PerPropertyViewData.TryGetValue(property.propertyPath, out var data)) return data;
data = new InputActionDrawerViewData();
m_PerPropertyViewData.Add(property.propertyPath, data);
return data;
}
protected abstract TreeViewItem BuildTree(SerializedProperty property);
protected abstract string GetSuffixToRemoveFromPropertyDisplayName();
protected abstract bool IsPropertyAClone(SerializedProperty property);
protected abstract void ResetProperty(SerializedProperty property);
// Unity creates a single instance of a property drawer to draw multiple instances of the property drawer type,
// so we can't store state in the property drawer for each item. We do need that though, because each InputAction
// needs to have it's own instance of the InputActionTreeView to correctly draw it's own bindings. So what we do
// is keep this array around that stores a tree view instance for each unique property path that the property
// drawer encounters. The tree view will be recreated if we detect that the property being drawn has changed.
private Dictionary m_PerPropertyViewData;
internal class PropertiesViewPopup : EditorWindow
{
public static void Show(Rect btnRect, PropertiesViewBase view)
{
var window = CreateInstance();
window.m_PropertyView = view;
window.ShowPopup();
window.ShowAsDropDown(btnRect, new Vector2(300, 350));
}
private void OnGUI()
{
m_PropertyView.OnGUI();
}
private PropertiesViewBase m_PropertyView;
}
private class InputActionDrawerViewData
{
public InputActionTreeView TreeView;
public InputControlPickerState ControlPickerState;
}
}
}
#endif // UNITY_EDITOR