Skip to content
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#if UNITY_EDITOR
using System;
using System.IO;
using UnityEngine.InputSystem.Utilities;
using UnityEditor;
using UnityEditor.AssetImporters;
using UnityEngine.UIElements;
using UnityEditor.UIElements;

////TODO: support for multi-editing

Expand All @@ -15,90 +16,212 @@ namespace UnityEngine.InputSystem.Editor
[CustomEditor(typeof(InputActionImporter))]
internal class InputActionImporterEditor : ScriptedImporterEditor
{
public override void OnInspectorGUI()
public override VisualElement CreateInspectorGUI()
{
var root = new VisualElement();
var inputActionAsset = GetAsset();

// ScriptedImporterEditor in 2019.2 now requires explicitly updating the SerializedObject
// like in other types of editors.
serializedObject.Update();
if (inputActionAsset == null)
{
root.Add(new HelpBox(
"The currently selected object is not an editable input action asset.",
HelpBoxMessageType.Info));
}

EditorGUILayout.Space();
var editButton = new Button(() => OpenEditor(inputActionAsset))
{
text = GetOpenEditorButtonText(inputActionAsset)
};
editButton.style.height = 30;
editButton.style.marginTop = 4;
editButton.style.marginBottom = 4;
editButton.SetEnabled(inputActionAsset != null);
root.Add(editButton);

if (inputActionAsset == null)
EditorGUILayout.HelpBox("The currently selected object is not an editable input action asset.",
MessageType.Info);
var projectWideContainer = new VisualElement();
projectWideContainer.style.marginTop = 6;
projectWideContainer.style.marginBottom = 6;
root.Add(projectWideContainer);
BuildProjectWideSection(projectWideContainer, inputActionAsset);

BuildCodeGenerationSection(root, inputActionAsset);

root.Add(new IMGUIContainer(() =>
{
serializedObject.ApplyModifiedProperties();
ApplyRevertGUI();
}));

return root;
}

private void BuildProjectWideSection(VisualElement container, InputActionAsset inputActionAsset)
{
container.Clear();

var currentActions = InputSystem.actions;

// Button to pop up window to edit the asset.
using (new EditorGUI.DisabledScope(inputActionAsset == null))
if (currentActions == inputActionAsset)
{
if (GUILayout.Button(GetOpenEditorButtonText(inputActionAsset), GUILayout.Height(30)))
OpenEditor(inputActionAsset);
container.Add(new HelpBox(
"These actions are assigned as the Project-wide Input Actions.",
HelpBoxMessageType.Info));
return;
}

var message = "These actions are not assigned as the Project-wide Input Actions for the Input System.";
if (currentActions != null)
{
var currentPath = AssetDatabase.GetAssetPath(currentActions);
if (!string.IsNullOrEmpty(currentPath))
message += $" The actions currently assigned as the Project-wide Input Actions are: {currentPath}. ";
}

EditorGUILayout.Space();
container.Add(new HelpBox(message, HelpBoxMessageType.Warning));

var assignButton = new Button(() =>
{
InputSystem.actions = inputActionAsset;
BuildProjectWideSection(container, inputActionAsset);
})
{
text = "Assign as the Project-wide Input Actions"
};
assignButton.SetEnabled(!EditorApplication.isPlayingOrWillChangePlaymode);
container.Add(assignButton);
}

// Project-wide Input Actions Asset UI.
InputAssetEditorUtils.DrawMakeActiveGui(InputSystem.actions, inputActionAsset,
inputActionAsset ? inputActionAsset.name : "Null", "Project-wide Input Actions",
(value) => InputSystem.actions = value, !EditorApplication.isPlayingOrWillChangePlaymode);
private void BuildCodeGenerationSection(VisualElement root, InputActionAsset inputActionAsset)
{
var generateField = new PropertyField(
serializedObject.FindProperty("m_GenerateWrapperCode"), "Generate C# Class");
root.Add(generateField);

EditorGUILayout.Space();
var codeGenContainer = new VisualElement();
root.Add(codeGenContainer);

// Importer settings UI.
var generateWrapperCodeProperty = serializedObject.FindProperty("m_GenerateWrapperCode");
EditorGUILayout.PropertyField(generateWrapperCodeProperty, m_GenerateWrapperCodeLabel);
if (generateWrapperCodeProperty.boolValue)
// File path with browse button
string defaultFileName = "";
if (inputActionAsset != null)
{
var wrapperCodePathProperty = serializedObject.FindProperty("m_WrapperCodePath");
var wrapperClassNameProperty = serializedObject.FindProperty("m_WrapperClassName");
var wrapperCodeNamespaceProperty = serializedObject.FindProperty("m_WrapperCodeNamespace");
var assetPath = AssetDatabase.GetAssetPath(inputActionAsset);
defaultFileName = Path.ChangeExtension(assetPath, ".cs");
}

var pathRow = new VisualElement();
pathRow.style.flexDirection = FlexDirection.Row;
pathRow.style.alignItems = Align.Center;
codeGenContainer.Add(pathRow);

EditorGUILayout.BeginHorizontal();
var pathField = new TextField("C# Class File") { bindingPath = "m_WrapperCodePath" };
pathField.style.flexGrow = 1;
SetupPlaceholder(pathField, defaultFileName);
pathRow.Add(pathField);

string defaultFileName = "";
if (inputActionAsset != null)
var browseButton = new Button(() =>
{
var fileName = EditorUtility.SaveFilePanel("Location for generated C# file",
Path.GetDirectoryName(defaultFileName),
Path.GetFileName(defaultFileName), "cs");
if (!string.IsNullOrEmpty(fileName))
{
var assetPath = AssetDatabase.GetAssetPath(inputActionAsset);
defaultFileName = Path.ChangeExtension(assetPath, ".cs");
if (fileName.StartsWith(Application.dataPath))
fileName = "Assets/" + fileName.Substring(Application.dataPath.Length + 1);

var prop = serializedObject.FindProperty("m_WrapperCodePath");
prop.stringValue = fileName;
serializedObject.ApplyModifiedProperties();
}
})
{
text = "…"
};
browseButton.style.width = 25;
browseButton.style.minWidth = 25;
pathRow.Add(browseButton);

wrapperCodePathProperty.PropertyFieldWithDefaultText(m_WrapperCodePathLabel, defaultFileName);
// Class name
string typeName = inputActionAsset != null
? CSharpCodeHelpers.MakeTypeName(inputActionAsset.name)
: null;

if (GUILayout.Button("…", EditorStyles.miniButton, GUILayout.MaxWidth(20)))
{
var fileName = EditorUtility.SaveFilePanel("Location for generated C# file",
Path.GetDirectoryName(defaultFileName),
Path.GetFileName(defaultFileName), "cs");
if (!string.IsNullOrEmpty(fileName))
{
if (fileName.StartsWith(Application.dataPath))
fileName = "Assets/" + fileName.Substring(Application.dataPath.Length + 1);

wrapperCodePathProperty.stringValue = fileName;
}
}
EditorGUILayout.EndHorizontal();
var classNameField = new TextField("C# Class Name") { bindingPath = "m_WrapperClassName" };
SetupPlaceholder(classNameField, typeName ?? "<Class name>");
codeGenContainer.Add(classNameField);

string typeName = null;
if (inputActionAsset != null)
typeName = CSharpCodeHelpers.MakeTypeName(inputActionAsset?.name);
wrapperClassNameProperty.PropertyFieldWithDefaultText(m_WrapperClassNameLabel, typeName ?? "<Class name>");
var classNameError = new HelpBox("Must be a valid C# identifier", HelpBoxMessageType.Error);
codeGenContainer.Add(classNameError);

if (!CSharpCodeHelpers.IsEmptyOrProperIdentifier(wrapperClassNameProperty.stringValue))
EditorGUILayout.HelpBox("Must be a valid C# identifier", MessageType.Error);
var classNameProp = serializedObject.FindProperty("m_WrapperClassName");
classNameError.style.display = !CSharpCodeHelpers.IsEmptyOrProperIdentifier(classNameProp.stringValue)
? DisplayStyle.Flex : DisplayStyle.None;

wrapperCodeNamespaceProperty.PropertyFieldWithDefaultText(m_WrapperCodeNamespaceLabel, "<Global namespace>");
classNameField.RegisterValueChangedCallback(evt =>
{
classNameError.style.display = !CSharpCodeHelpers.IsEmptyOrProperIdentifier(evt.newValue)
? DisplayStyle.Flex : DisplayStyle.None;
});

if (!CSharpCodeHelpers.IsEmptyOrProperNamespaceName(wrapperCodeNamespaceProperty.stringValue))
EditorGUILayout.HelpBox("Must be a valid C# namespace name", MessageType.Error);
}
// Namespace
var namespaceField = new TextField("C# Class Namespace") { bindingPath = "m_WrapperCodeNamespace" };
SetupPlaceholder(namespaceField, "<Global namespace>");
codeGenContainer.Add(namespaceField);

var namespaceError = new HelpBox("Must be a valid C# namespace name", HelpBoxMessageType.Error);
codeGenContainer.Add(namespaceError);

// Using ApplyRevertGUI requires calling Update and ApplyModifiedProperties around the serializedObject,
// and will print warning messages otherwise (see warning message in ApplyRevertGUI implementation).
serializedObject.ApplyModifiedProperties();
var namespaceProp = serializedObject.FindProperty("m_WrapperCodeNamespace");
namespaceError.style.display = !CSharpCodeHelpers.IsEmptyOrProperNamespaceName(namespaceProp.stringValue)
? DisplayStyle.Flex : DisplayStyle.None;

ApplyRevertGUI();
namespaceField.RegisterValueChangedCallback(evt =>
{
namespaceError.style.display = !CSharpCodeHelpers.IsEmptyOrProperNamespaceName(evt.newValue)
? DisplayStyle.Flex : DisplayStyle.None;
});

// Show/hide code gen fields based on toggle
var generateProp = serializedObject.FindProperty("m_GenerateWrapperCode");
codeGenContainer.style.display = generateProp.boolValue ? DisplayStyle.Flex : DisplayStyle.None;

generateField.RegisterValueChangeCallback(evt =>
{
codeGenContainer.style.display = evt.changedProperty.boolValue
? DisplayStyle.Flex : DisplayStyle.None;
});
}

private static void SetupPlaceholder(TextField textField, string placeholder)
{
if (string.IsNullOrEmpty(placeholder))
return;

var placeholderLabel = new Label(placeholder);
placeholderLabel.pickingMode = PickingMode.Ignore;
placeholderLabel.style.position = Position.Absolute;
placeholderLabel.style.unityTextAlign = TextAnchor.MiddleLeft;
placeholderLabel.style.opacity = 0.5f;
placeholderLabel.style.paddingLeft = 2;

textField.RegisterCallback<GeometryChangedEvent>(_ =>
{
var textInput = textField.Q("unity-text-input");
if (textInput != null && placeholderLabel.parent != textInput)
{
textInput.Add(placeholderLabel);
UpdatePlaceholder(textField, placeholderLabel);
}
});

textField.RegisterValueChangedCallback(_ => UpdatePlaceholder(textField, placeholderLabel));
textField.RegisterCallback<FocusInEvent>(_ => placeholderLabel.style.display = DisplayStyle.None);
textField.RegisterCallback<FocusOutEvent>(_ => UpdatePlaceholder(textField, placeholderLabel));
}

private static void UpdatePlaceholder(TextField textField, Label placeholder)
{
placeholder.style.display = string.IsNullOrEmpty(textField.value)
? DisplayStyle.Flex : DisplayStyle.None;
}

private InputActionAsset GetAsset()
Expand Down Expand Up @@ -131,7 +254,6 @@ private string GetOpenEditorButtonText(InputActionAsset asset)

private static void OpenEditor(InputActionAsset asset)
{
// Redirect to Project-settings Input Actions editor if this is the project-wide actions asset
if (IsProjectWideActionsAsset(asset))
{
SettingsService.OpenProjectSettings(InputSettingsPath.kSettingsRootPath);
Expand All @@ -140,11 +262,6 @@ private static void OpenEditor(InputActionAsset asset)

InputActionsEditorWindow.OpenEditor(asset);
}

private readonly GUIContent m_GenerateWrapperCodeLabel = EditorGUIUtility.TrTextContent("Generate C# Class");
private readonly GUIContent m_WrapperCodePathLabel = EditorGUIUtility.TrTextContent("C# Class File");
private readonly GUIContent m_WrapperClassNameLabel = EditorGUIUtility.TrTextContent("C# Class Name");
private readonly GUIContent m_WrapperCodeNamespaceLabel = EditorGUIUtility.TrTextContent("C# Class Namespace");
}
}
#endif // UNITY_EDITOR
Loading