diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Plugins/iOS/InputSettingsiOSProvider.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Plugins/iOS/InputSettingsiOSProvider.cs index a6c2b7c263..ff9d91c0f1 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/Plugins/iOS/InputSettingsiOSProvider.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Plugins/iOS/InputSettingsiOSProvider.cs @@ -1,6 +1,7 @@ #if UNITY_EDITOR using System; using UnityEditor; +using UnityEngine.UIElements; namespace UnityEngine.InputSystem { @@ -8,20 +9,28 @@ internal class InputSettingsiOSProvider { [NonSerialized] private SerializedProperty m_MotionUsageEnabled; [NonSerialized] private SerializedProperty m_MotionUsageDescription; + [NonSerialized] private VisualElement m_Container; + [NonSerialized] private Toggle m_MotionUsageToggle; + [NonSerialized] private TextField m_MotionUsageDescriptionField; private GUIContent m_MotionUsageContent; private GUIContent m_MotionUsageDescriptionContent; public InputSettingsiOSProvider(SerializedObject parent) { - var prefix = "m_iOSSettings.m_MotionUsage"; - m_MotionUsageEnabled = parent.FindProperty(prefix + ".m_Enabled"); - m_MotionUsageDescription = parent.FindProperty(prefix + ".m_Description"); + Update(parent); m_MotionUsageContent = new GUIContent("Motion Usage", "Enables Motion Usage for the app, required for sensors like Step Counter. This also adds 'Privacy - Motion Usage Description' entry to Info.plist"); m_MotionUsageDescriptionContent = new GUIContent(" Description", "Describe why the app wants to access the device's Motion Usage sensor."); } + public void Update(SerializedObject parent) + { + var prefix = "m_iOSSettings.m_MotionUsage"; + m_MotionUsageEnabled = parent.FindProperty(prefix + ".m_Enabled"); + m_MotionUsageDescription = parent.FindProperty(prefix + ".m_Description"); + } + public void OnGUI() { EditorGUILayout.PropertyField(m_MotionUsageEnabled, m_MotionUsageContent); @@ -29,6 +38,66 @@ public void OnGUI() EditorGUILayout.PropertyField(m_MotionUsageDescription, m_MotionUsageDescriptionContent); EditorGUI.EndDisabledGroup(); } + + public void CreateGUI(VisualElement parent, Action onValueChanged) + { + if (parent == null) + return; + + m_Container = new VisualElement(); + var titleLabel = new Label("iOS"); + titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold; + titleLabel.style.marginTop = 12; + m_Container.Add(titleLabel); + + m_MotionUsageToggle = new Toggle(m_MotionUsageContent.text) + { + tooltip = m_MotionUsageContent.tooltip + }; + m_MotionUsageToggle.RegisterValueChangedCallback(evt => + { + if (m_MotionUsageEnabled == null || m_MotionUsageEnabled.boolValue == evt.newValue) + return; + + m_MotionUsageEnabled.boolValue = evt.newValue; + onValueChanged?.Invoke(); + }); + m_Container.Add(m_MotionUsageToggle); + + m_MotionUsageDescriptionField = new TextField("Description") + { + tooltip = m_MotionUsageDescriptionContent.tooltip + }; + m_MotionUsageDescriptionField.RegisterValueChangedCallback(evt => + { + if (m_MotionUsageDescription == null || m_MotionUsageDescription.stringValue == evt.newValue) + return; + + m_MotionUsageDescription.stringValue = evt.newValue; + onValueChanged?.Invoke(); + }); + m_Container.Add(m_MotionUsageDescriptionField); + + parent.Add(m_Container); + } + + public void RefreshUIToolkitState(bool canEditSettings) + { + if (m_Container == null) + return; + + if (m_MotionUsageToggle != null) + { + m_MotionUsageToggle.SetEnabled(canEditSettings && m_MotionUsageEnabled != null); + m_MotionUsageToggle.SetValueWithoutNotify(m_MotionUsageEnabled?.boolValue ?? false); + } + + if (m_MotionUsageDescriptionField != null) + { + m_MotionUsageDescriptionField.SetEnabled(canEditSettings && m_MotionUsageEnabled != null && m_MotionUsageEnabled.boolValue); + m_MotionUsageDescriptionField.SetValueWithoutNotify(m_MotionUsageDescription?.stringValue ?? string.Empty); + } + } } } #endif diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs index cb21d46051..77a8b79de2 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs @@ -2,12 +2,9 @@ using System; using System.Linq; using UnityEditor; -using UnityEditorInternal; using UnityEngine.InputSystem.Utilities; using UnityEngine.UIElements; -////TODO: detect if new input backends are enabled and put UI in here to enable them if needed - #pragma warning disable CS0414 namespace UnityEngine.InputSystem.Editor { @@ -54,13 +51,16 @@ private InputSettingsProvider(string path, SettingsScope scopes) public override void OnActivate(string searchContext, VisualElement rootElement) { base.OnActivate(searchContext, rootElement); + m_RootElement = rootElement; InputSystem.onSettingsChange += OnSettingsChange; Undo.undoRedoPerformed += OnUndoRedo; + BuildUI(); } public override void OnDeactivate() { base.OnDeactivate(); + m_RootElement = null; InputSystem.onSettingsChange -= OnSettingsChange; Undo.undoRedoPerformed -= OnUndoRedo; } @@ -88,118 +88,527 @@ public override void OnTitleBarGUI() } } - public override void OnGUI(string searchContext) + private void BuildUI() { + if (m_RootElement == null) + return; + InitializeWithCurrentSettingsIfNecessary(); + m_RootElement.Clear(); - if (m_AvailableInputSettingsAssets.Length == 0) - { - EditorGUILayout.HelpBox( - "Settings for the new input system are stored in an asset. Click the button below to create a settings asset you can edit.", - MessageType.Info); - if (GUILayout.Button("Create settings asset", GUILayout.Height(30))) - CreateNewSettingsAsset("Assets/InputSystem.inputsettings.asset"); - GUILayout.Space(20); - } + m_CreateSettingsAssetContainer = new VisualElement(); + m_CreateSettingsAssetContainer.style.marginBottom = 12; + m_RootElement.Add(m_CreateSettingsAssetContainer); - using (new EditorGUI.DisabledScope(m_AvailableInputSettingsAssets.Length == 0)) + m_CreateSettingsAssetHelpBox = new HelpBox( + "Settings for the new input system are stored in an asset. Click the button below to create a settings asset you can edit.", + HelpBoxMessageType.Info); + m_CreateSettingsAssetContainer.Add(m_CreateSettingsAssetHelpBox); + + m_CreateSettingsAssetButton = new Button(() => CreateNewSettingsAsset("Assets/InputSystem.inputsettings.asset")) { - EditorGUILayout.Space(); - EditorGUILayout.Separator(); - EditorGUILayout.Space(); + text = "Create settings asset" + }; + m_CreateSettingsAssetButton.style.marginTop = 6; + m_CreateSettingsAssetButton.style.height = 30; + m_CreateSettingsAssetContainer.Add(m_CreateSettingsAssetButton); + + var titleLabel = new Label("Settings"); + titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold; + titleLabel.style.fontSize = 19; + titleLabel.style.marginBottom = 12; + m_RootElement.Add(titleLabel); + + m_HeaderContainer = new VisualElement(); + m_RootElement.Add(m_HeaderContainer); - Debug.Assert(m_Settings != null); + m_UpdateModeDropdown = CreateEnumDropdown( + () => m_UpdateMode, + m_UpdateModeContent, + RefreshUIToolkitHeaderState); + m_HeaderContainer.Add(m_UpdateModeDropdown); - EditorGUI.BeginChangeCheck(); + m_UpdateModeHelpContainer = new VisualElement(); - EditorGUILayout.PropertyField(m_UpdateMode, m_UpdateModeContent); - if (InputSystem.settings?.updateMode == InputSettings.UpdateMode.ProcessEventsManually) - CustomUpdateModeHelpBox(); + m_UpdateModeHelpBox = new HelpBox( + "This is not recommended, the default update mode is dynamic update and should only be changed for compelling reasons. Please refer to the documentation.", + HelpBoxMessageType.Warning); + m_UpdateModeHelpContainer.Add(m_UpdateModeHelpBox); - var runInBackground = Application.runInBackground; - using (new EditorGUI.DisabledScope(!runInBackground)) - EditorGUILayout.PropertyField(m_BackgroundBehavior, m_BackgroundBehaviorContent); - if (!runInBackground) - EditorGUILayout.HelpBox("Focus change behavior can only be changed if 'Run In Background' is enabled in Player Settings.", MessageType.Info); + m_UpdateModeReadMoreButton = new Button(OpenUpdateModeDocumentation) + { + text = "Read more" + }; + m_UpdateModeHelpContainer.Add(m_UpdateModeReadMoreButton); + m_HeaderContainer.Add(m_UpdateModeHelpContainer); + + m_BackgroundBehaviorDropdown = CreateEnumDropdown( + () => m_BackgroundBehavior, + m_BackgroundBehaviorContent, + RefreshUIToolkitHeaderState); + m_HeaderContainer.Add(m_BackgroundBehaviorDropdown); + + m_BackgroundBehaviorHelpBox = new HelpBox( + "Focus change behavior can only be changed if 'Run In Background' is enabled in Player Settings.", + HelpBoxMessageType.Info); + m_HeaderContainer.Add(m_BackgroundBehaviorHelpBox); #if UNITY_INPUT_SYSTEM_PLATFORM_SCROLL_DELTA - EditorGUILayout.PropertyField(m_ScrollDeltaBehavior, m_ScrollDeltaBehaviorContent); + m_ScrollDeltaBehaviorDropdown = CreateEnumDropdown( + () => m_ScrollDeltaBehavior, + m_ScrollDeltaBehaviorContent, + RefreshUIToolkitHeaderState); + m_HeaderContainer.Add(m_ScrollDeltaBehaviorDropdown); #endif - EditorGUILayout.Space(); - EditorGUILayout.PropertyField(m_CompensateForScreenOrientation, m_CompensateForScreenOrientationContent); - - // NOTE: We do NOT make showing this one conditional on whether runInBackground is actually set in the - // player settings as regardless of whether it's on or not, Unity will force it on in standalone - // development players. - - EditorGUILayout.Space(); - EditorGUILayout.Separator(); - EditorGUILayout.Space(); - - EditorGUILayout.PropertyField(m_DefaultDeadzoneMin, m_DefaultDeadzoneMinContent); - EditorGUILayout.PropertyField(m_DefaultDeadzoneMax, m_DefaultDeadzoneMaxContent); - EditorGUILayout.PropertyField(m_DefaultButtonPressPoint, m_DefaultButtonPressPointContent); - EditorGUILayout.PropertyField(m_ButtonReleaseThreshold, m_ButtonReleaseThresholdContent); - EditorGUILayout.PropertyField(m_DefaultTapTime, m_DefaultTapTimeContent); - EditorGUILayout.PropertyField(m_DefaultSlowTapTime, m_DefaultSlowTapTimeContent); - EditorGUILayout.PropertyField(m_DefaultHoldTime, m_DefaultHoldTimeContent); - EditorGUILayout.PropertyField(m_TapRadius, m_TapRadiusContent); - EditorGUILayout.PropertyField(m_MultiTapDelayTime, m_MultiTapDelayTimeContent); - - EditorGUILayout.Space(); - EditorGUILayout.Separator(); - EditorGUILayout.Space(); - - EditorGUILayout.HelpBox("Leave 'Supported Devices' empty if you want the input system to support all input devices it can recognize. If, however, " - + "you are only interested in a certain set of devices, adding them here will narrow the scope of what's presented in the editor " - + "and avoid picking up input from devices not relevant to the project. When you add devices here, any device that will not be classified " - + "as supported will appear under 'Unsupported Devices' in the input debugger.", MessageType.None); - - m_SupportedDevices.DoLayoutList(); - - EditorGUILayout.LabelField("iOS", EditorStyles.boldLabel); - EditorGUILayout.Space(); - m_iOSProvider.OnGUI(); - - EditorGUILayout.Space(); - EditorGUILayout.LabelField("Editor", EditorStyles.boldLabel); - EditorGUILayout.Space(); - EditorGUILayout.PropertyField(m_EditorInputBehaviorInPlayMode, m_EditorInputBehaviorInPlayModeContent); - - EditorGUILayout.Space(); - EditorGUILayout.LabelField("Improved Shortcut Support", EditorStyles.boldLabel); - EditorGUILayout.Space(); - EditorGUILayout.PropertyField(m_ShortcutKeysConsumeInputs, m_ShortcutKeysConsumeInputsContent); - if (m_ShortcutKeysConsumeInputs.boolValue) - EditorGUILayout.HelpBox("Please note that enabling Improved Shortcut Support will cause actions with composite bindings to consume input and block any other actions which are enabled and sharing the same controls. " - + "Input consumption is performed in priority order, with the action containing the greatest number of bindings checked first. " - + "Therefore actions requiring fewer keypresses will not be triggered if an action using more keypresses is triggered and has overlapping controls. " - + "This works for shortcut keys, however in other cases this might not give the desired result, especially where there are actions with the exact same number of composite controls, in which case it is non-deterministic which action will be triggered. " - + "These conflicts may occur even between actions which belong to different Action Maps e.g. if using an UIInputModule with the Arrow Keys bound to the Navigate Action in the UI Action Map, this would interfere with other Action Maps using those keys. " - + "However conflicts would not occur between actions which belong to different Action Assets. " - + "Since event consumption only occurs for enabled actions, you can resolve unexpected issues by ensuring that only those Actions or Action Maps that are relevant to your game's current context are enabled. Enabling or disabling actions as your game or application moves between different contexts. " - , MessageType.None); - - if (EditorGUI.EndChangeCheck()) + m_CompensateForScreenOrientationToggle = CreateToggle( + () => m_CompensateForScreenOrientation, + m_CompensateForScreenOrientationContent, + RefreshUIToolkitHeaderState); + m_CompensateForScreenOrientationToggle.style.marginTop = 12; + m_CompensateForScreenOrientationToggle.style.marginBottom = 12; + m_HeaderContainer.Add(m_CompensateForScreenOrientationToggle); + + m_DefaultDeadzoneMinField = CreateFloatField( + () => m_DefaultDeadzoneMin, + m_DefaultDeadzoneMinContent, + RefreshUIToolkitHeaderState); + m_HeaderContainer.Add(m_DefaultDeadzoneMinField); + + m_DefaultDeadzoneMaxField = CreateFloatField( + () => m_DefaultDeadzoneMax, + m_DefaultDeadzoneMaxContent, + RefreshUIToolkitHeaderState); + m_HeaderContainer.Add(m_DefaultDeadzoneMaxField); + + m_DefaultButtonPressPointField = CreateFloatField( + () => m_DefaultButtonPressPoint, + m_DefaultButtonPressPointContent, + RefreshUIToolkitHeaderState); + m_HeaderContainer.Add(m_DefaultButtonPressPointField); + + m_ButtonReleaseThresholdField = CreateFloatField( + () => m_ButtonReleaseThreshold, + m_ButtonReleaseThresholdContent, + RefreshUIToolkitHeaderState); + m_HeaderContainer.Add(m_ButtonReleaseThresholdField); + + m_DefaultTapTimeField = CreateFloatField( + () => m_DefaultTapTime, + m_DefaultTapTimeContent, + RefreshUIToolkitHeaderState); + m_HeaderContainer.Add(m_DefaultTapTimeField); + + m_DefaultSlowTapTimeField = CreateFloatField( + () => m_DefaultSlowTapTime, + m_DefaultSlowTapTimeContent, + RefreshUIToolkitHeaderState); + m_HeaderContainer.Add(m_DefaultSlowTapTimeField); + + m_DefaultHoldTimeField = CreateFloatField( + () => m_DefaultHoldTime, + m_DefaultHoldTimeContent, + RefreshUIToolkitHeaderState); + m_HeaderContainer.Add(m_DefaultHoldTimeField); + + m_TapRadiusField = CreateFloatField( + () => m_TapRadius, + m_TapRadiusContent, + RefreshUIToolkitHeaderState); + m_HeaderContainer.Add(m_TapRadiusField); + + m_MultiTapDelayTimeField = CreateFloatField( + () => m_MultiTapDelayTime, + m_MultiTapDelayTimeContent, + RefreshUIToolkitHeaderState); + m_HeaderContainer.Add(m_MultiTapDelayTimeField); + + m_SupportedDevicesHelpBox = new HelpBox( + "Leave 'Supported Devices' empty if you want the input system to support all input devices it can recognize. If, however, " + + "you are only interested in a certain set of devices, adding them here will narrow the scope of what's presented in the editor " + + "and avoid picking up input from devices not relevant to the project. When you add devices here, any device that will not be classified " + + "as supported will appear under 'Unsupported Devices' in the input debugger.", + HelpBoxMessageType.None); + m_SupportedDevicesHelpBox.style.marginTop = 48; + m_HeaderContainer.Add(m_SupportedDevicesHelpBox); + + var supportedDevicesTitleLabel = new Label("Supported Devices"); + supportedDevicesTitleLabel.style.unityFontStyleAndWeight = FontStyle.Bold; + supportedDevicesTitleLabel.style.marginTop = 6; + m_HeaderContainer.Add(supportedDevicesTitleLabel); + + m_SupportedDevicesListView = new ListView + { + selectionType = UIElements.SelectionType.Single, + reorderable = true, + showBorder = true, + fixedItemHeight = 22 + }; + m_SupportedDevicesListView.makeItem = MakeSupportedDevicesItem; + m_SupportedDevicesListView.bindItem = BindSupportedDevicesItem; + m_SupportedDevicesListView.itemIndexChanged += OnSupportedDevicesReordered; + m_SupportedDevicesListView.selectionChanged += _ => RefreshSupportedDevicesButtonsState(); + m_HeaderContainer.Add(m_SupportedDevicesListView); + + var supportedDevicesButtonsContainer = new VisualElement(); + supportedDevicesButtonsContainer.style.flexDirection = FlexDirection.Row; + supportedDevicesButtonsContainer.style.justifyContent = Justify.FlexEnd; + supportedDevicesButtonsContainer.style.marginTop = 4; + m_HeaderContainer.Add(supportedDevicesButtonsContainer); + + m_AddSupportedDeviceButton = new Button(AddSupportedDevice) + { + text = "Add" + }; + supportedDevicesButtonsContainer.Add(m_AddSupportedDeviceButton); + + m_RemoveSupportedDeviceButton = new Button(RemoveSupportedDevice) + { + text = "Remove" + }; + m_RemoveSupportedDeviceButton.style.marginLeft = 4; + supportedDevicesButtonsContainer.Add(m_RemoveSupportedDeviceButton); + + m_iOSProvider.CreateGUI(m_HeaderContainer, () => + { + Apply(); + RefreshUIToolkitHeaderState(); + }); + + var editorTitleLabel = new Label("Editor"); + editorTitleLabel.style.unityFontStyleAndWeight = FontStyle.Bold; + editorTitleLabel.style.marginTop = 12; + m_HeaderContainer.Add(editorTitleLabel); + + m_EditorInputBehaviorInPlayModeDropdown = CreateEnumDropdown( + () => m_EditorInputBehaviorInPlayMode, + m_EditorInputBehaviorInPlayModeContent, + RefreshUIToolkitHeaderState); + m_HeaderContainer.Add(m_EditorInputBehaviorInPlayModeDropdown); + + var shortcutSupportTitleLabel = new Label("Improved Shortcut Support"); + shortcutSupportTitleLabel.style.unityFontStyleAndWeight = FontStyle.Bold; + shortcutSupportTitleLabel.style.marginTop = 12; + m_HeaderContainer.Add(shortcutSupportTitleLabel); + + m_ShortcutKeysConsumeInputsToggle = CreateToggle( + () => m_ShortcutKeysConsumeInputs, + m_ShortcutKeysConsumeInputsContent, + RefreshUIToolkitHeaderState); + m_HeaderContainer.Add(m_ShortcutKeysConsumeInputsToggle); + + m_ShortcutKeysConsumeInputsHelpBox = new HelpBox( + "Please note that enabling Improved Shortcut Support will cause actions with composite bindings to consume input and block any other actions which are enabled and sharing the same controls. " + + "Input consumption is performed in priority order, with the action containing the greatest number of bindings checked first. " + + "Therefore actions requiring fewer keypresses will not be triggered if an action using more keypresses is triggered and has overlapping controls. " + + "This works for shortcut keys, however in other cases this might not give the desired result, especially where there are actions with the exact same number of composite controls, in which case it is non-deterministic which action will be triggered. " + + "These conflicts may occur even between actions which belong to different Action Maps e.g. if using an UIInputModule with the Arrow Keys bound to the Navigate Action in the UI Action Map, this would interfere with other Action Maps using those keys. " + + "However conflicts would not occur between actions which belong to different Action Assets. " + + "Since event consumption only occurs for enabled actions, you can resolve unexpected issues by ensuring that only those Actions or Action Maps that are relevant to your game's current context are enabled. Enabling or disabling actions as your game or application moves between different contexts. ", + HelpBoxMessageType.None); + m_HeaderContainer.Add(m_ShortcutKeysConsumeInputsHelpBox); + + RefreshUIToolkitHeaderState(); + } + + private DropdownField CreateEnumDropdown(Func propertyAccessor, GUIContent content, Action onValueChanged) + { + var dropdown = new DropdownField(content.text) + { + tooltip = content.tooltip + }; + dropdown.RegisterValueChangedCallback(evt => + { + var property = propertyAccessor(); + if (property == null) + return; + + var newIndex = dropdown.choices?.IndexOf(evt.newValue) ?? -1; + if (newIndex == -1 || property.enumValueIndex == newIndex) + return; + + property.enumValueIndex = newIndex; + Apply(); + onValueChanged?.Invoke(); + }); + + return dropdown; + } + + private Toggle CreateToggle(Func propertyAccessor, GUIContent content, Action onValueChanged) + { + var toggle = new Toggle(content.text) + { + tooltip = content.tooltip + }; + toggle.RegisterValueChangedCallback(evt => + { + var property = propertyAccessor(); + if (property == null || property.boolValue == evt.newValue) + return; + + property.boolValue = evt.newValue; + Apply(); + onValueChanged?.Invoke(); + }); + + return toggle; + } + + private FloatField CreateFloatField(Func propertyAccessor, GUIContent content, Action onValueChanged) + { + var field = new FloatField(content.text) + { + tooltip = content.tooltip + }; + field.RegisterValueChangedCallback(evt => + { + var property = propertyAccessor(); + if (property == null || Mathf.Approximately(property.floatValue, evt.newValue)) + return; + + property.floatValue = evt.newValue; + Apply(); + onValueChanged?.Invoke(); + }); + + return field; + } + + private VisualElement MakeSupportedDevicesItem() + { + var row = new VisualElement(); + row.style.flexDirection = FlexDirection.Row; + row.style.alignItems = Align.Center; + + var icon = new Image + { + name = "icon", + scaleMode = ScaleMode.ScaleToFit + }; + icon.style.width = 20; + icon.style.height = 20; + icon.style.marginRight = 4; + row.Add(icon); + + var label = new Label + { + name = "label" + }; + label.style.flexGrow = 1; + row.Add(label); + + return row; + } + + private void BindSupportedDevicesItem(VisualElement element, int index) + { + var icon = element.Q("icon"); + var label = element.Q