diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporterEditor.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporterEditor.cs index f117089189..dd1fffe228 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporterEditor.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporterEditor.cs @@ -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 @@ -15,90 +16,219 @@ 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(); - EditorGUILayout.Space(); - if (inputActionAsset == null) - EditorGUILayout.HelpBox("The currently selected object is not an editable input action asset.", - MessageType.Info); + { + root.Add(new HelpBox( + "The currently selected object is not an editable input action asset.", + HelpBoxMessageType.Info)); + } + + 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); + + 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; + pathField.AddToClassList(BaseField.alignedFieldUssClassName); + 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" }; + classNameField.AddToClassList(BaseField.alignedFieldUssClassName); + SetupPlaceholder(classNameField, typeName ?? ""); + codeGenContainer.Add(classNameField); - string typeName = null; - if (inputActionAsset != null) - typeName = CSharpCodeHelpers.MakeTypeName(inputActionAsset?.name); - wrapperClassNameProperty.PropertyFieldWithDefaultText(m_WrapperClassNameLabel, typeName ?? ""); + 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, ""); + 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" }; + namespaceField.AddToClassList(BaseField.alignedFieldUssClassName); + SetupPlaceholder(namespaceField, ""); + 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(_ => + { + 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(_ => placeholderLabel.style.display = DisplayStyle.None); + textField.RegisterCallback(_ => 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() @@ -131,7 +261,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); @@ -140,11 +269,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 diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/InputAssetEditorUtils.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/InputAssetEditorUtils.cs index e9aede7015..faf9800be8 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/InputAssetEditorUtils.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/InputAssetEditorUtils.cs @@ -3,6 +3,7 @@ using System; using System.IO; using UnityEditor; +using UnityEngine.UIElements; namespace UnityEngine.InputSystem.Editor { @@ -19,7 +20,7 @@ internal enum DialogResult InvalidPath, /// - /// The dialog was cancelled by the user and the path is invalid. + /// The dialog was canceled by the user and the path is invalid. /// Cancelled, @@ -82,13 +83,15 @@ internal static T CreateAsset(T asset, string relativePath) where T : Scripta return asset; } - public static void DrawMakeActiveGui(T current, T target, string targetName, string entity, Action apply, bool allowAssignActive = true) + public static VisualElement CreateMakeActiveGui(T current, T target, string targetName, string entity, Action apply, bool allowAssignActive = true) where T : ScriptableObject { + var container = new VisualElement(); + if (current == target) { - EditorGUILayout.HelpBox($"These actions are assigned as the {entity}.", MessageType.Info); - return; + container.Add(new HelpBox($"These actions are assigned as the {entity}.", HelpBoxMessageType.Info)); + return container; } string currentlyActiveAssetsPath = null; @@ -96,11 +99,19 @@ public static void DrawMakeActiveGui(T current, T target, string targetName, currentlyActiveAssetsPath = AssetDatabase.GetAssetPath(current); if (!string.IsNullOrEmpty(currentlyActiveAssetsPath)) currentlyActiveAssetsPath = $" The actions currently assigned as the {entity} are: {currentlyActiveAssetsPath}. "; - EditorGUILayout.HelpBox($"These actions are not assigned as the {entity} for the Input System. {currentlyActiveAssetsPath??""}", MessageType.Warning); - GUI.enabled = allowAssignActive; - if (GUILayout.Button($"Assign as the {entity}", EditorStyles.miniButton)) - apply(target); - GUI.enabled = true; + + container.Add(new HelpBox( + $"These actions are not assigned as the {entity} for the Input System. {currentlyActiveAssetsPath ?? ""}", + HelpBoxMessageType.Warning)); + + var assignButton = new Button(() => apply(target)) + { + text = $"Assign as the {entity}" + }; + assignButton.SetEnabled(allowAssignActive); + container.Add(assignButton); + + return container; } public static bool IsValidFileExtension(string path) diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs index 1ac676a022..a120311d33 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs @@ -489,17 +489,21 @@ internal static void ForceReload() [CustomEditor(typeof(InputSettings))] internal class InputSettingsEditor : UnityEditor.Editor { - public override void OnInspectorGUI() + public override VisualElement CreateInspectorGUI() { - EditorGUILayout.Space(); + var root = new VisualElement(); - if (GUILayout.Button("Open Input Settings Window", GUILayout.Height(30))) - InputSettingsProvider.Open(); + var openButton = new Button(() => InputSettingsProvider.Open()) + { + text = "Open Input Settings Window", + style = { height = 30 } + }; + root.Add(openButton); - EditorGUILayout.Space(); + root.Add(InputAssetEditorUtils.CreateMakeActiveGui(InputSystem.settings, target as InputSettings, + target.name, "settings", (value) => InputSystem.settings = value)); - InputAssetEditorUtils.DrawMakeActiveGui(InputSystem.settings, target as InputSettings, - target.name, "settings", (value) => InputSystem.settings = value); + return root; } protected override bool ShouldHideOpenButton()