summaryrefslogtreecommitdiffstats
path: root/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt
diff options
context:
space:
mode:
Diffstat (limited to 'src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt300
1 files changed, 300 insertions, 0 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt
new file mode 100644
index 000000000..16a1d0504
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt
@@ -0,0 +1,300 @@
+// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.ui
+
+import android.app.Dialog
+import android.graphics.drawable.Animatable2
+import android.graphics.drawable.AnimatedVectorDrawable
+import android.graphics.drawable.Drawable
+import android.os.Bundle
+import android.view.InputDevice
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.DialogMappingBinding
+import org.yuzu.yuzu_emu.features.input.NativeInput
+import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
+import org.yuzu.yuzu_emu.features.input.model.NativeButton
+import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting
+import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting
+import org.yuzu.yuzu_emu.features.settings.model.view.InputSetting
+import org.yuzu.yuzu_emu.features.settings.model.view.ModifierInputSetting
+import org.yuzu.yuzu_emu.utils.InputHandler
+import org.yuzu.yuzu_emu.utils.ParamPackage
+
+class InputDialogFragment : DialogFragment() {
+ private var inputAccepted = false
+
+ private var position: Int = 0
+
+ private lateinit var inputSetting: InputSetting
+
+ private lateinit var binding: DialogMappingBinding
+
+ private val settingsViewModel: SettingsViewModel by activityViewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (settingsViewModel.clickedItem == null) dismiss()
+
+ position = requireArguments().getInt(POSITION)
+
+ InputHandler.updateControllerData()
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ inputSetting = settingsViewModel.clickedItem as InputSetting
+ binding = DialogMappingBinding.inflate(layoutInflater)
+
+ val builder = MaterialAlertDialogBuilder(requireContext())
+ .setPositiveButton(android.R.string.cancel) { _, _ ->
+ NativeInput.stopMapping()
+ dismiss()
+ }
+ .setView(binding.root)
+
+ val playButtonMapAnimation = { twoDirections: Boolean ->
+ val stickAnimation: AnimatedVectorDrawable
+ val buttonAnimation: AnimatedVectorDrawable
+ binding.imageStickAnimation.apply {
+ val anim = if (twoDirections) {
+ R.drawable.stick_two_direction_anim
+ } else {
+ R.drawable.stick_one_direction_anim
+ }
+ setBackgroundResource(anim)
+ stickAnimation = background as AnimatedVectorDrawable
+ }
+ binding.imageButtonAnimation.apply {
+ setBackgroundResource(R.drawable.button_anim)
+ buttonAnimation = background as AnimatedVectorDrawable
+ }
+ stickAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() {
+ override fun onAnimationEnd(drawable: Drawable?) {
+ buttonAnimation.start()
+ }
+ })
+ buttonAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() {
+ override fun onAnimationEnd(drawable: Drawable?) {
+ stickAnimation.start()
+ }
+ })
+ stickAnimation.start()
+ }
+
+ when (val setting = inputSetting) {
+ is AnalogInputSetting -> {
+ when (setting.nativeAnalog) {
+ NativeAnalog.LStick -> builder.setTitle(
+ getString(R.string.map_control, getString(R.string.left_stick))
+ )
+
+ NativeAnalog.RStick -> builder.setTitle(
+ getString(R.string.map_control, getString(R.string.right_stick))
+ )
+ }
+
+ builder.setMessage(R.string.stick_map_description)
+
+ playButtonMapAnimation.invoke(true)
+ }
+
+ is ModifierInputSetting -> {
+ builder.setTitle(getString(R.string.map_control, setting.title))
+ .setMessage(R.string.button_map_description)
+ playButtonMapAnimation.invoke(false)
+ }
+
+ is ButtonInputSetting -> {
+ if (setting.nativeButton == NativeButton.DUp ||
+ setting.nativeButton == NativeButton.DDown ||
+ setting.nativeButton == NativeButton.DLeft ||
+ setting.nativeButton == NativeButton.DRight
+ ) {
+ builder.setTitle(getString(R.string.map_dpad_direction, setting.title))
+ } else {
+ builder.setTitle(getString(R.string.map_control, setting.title))
+ }
+ builder.setMessage(R.string.button_map_description)
+ playButtonMapAnimation.invoke(false)
+ }
+ }
+
+ return builder.create()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ view.requestFocus()
+ view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
+ dialog?.setOnKeyListener { _, _, keyEvent -> onKeyEvent(keyEvent) }
+ binding.root.setOnGenericMotionListener { _, motionEvent -> onMotionEvent(motionEvent) }
+ NativeInput.beginMapping(inputSetting.inputType.int)
+ }
+
+ private fun onKeyEvent(event: KeyEvent): Boolean {
+ if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
+ event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
+ ) {
+ return false
+ }
+
+ val action = when (event.action) {
+ KeyEvent.ACTION_DOWN -> NativeInput.ButtonState.PRESSED
+ KeyEvent.ACTION_UP -> NativeInput.ButtonState.RELEASED
+ else -> return false
+ }
+ val controllerData =
+ InputHandler.androidControllers[event.device.controllerNumber] ?: return false
+ NativeInput.onGamePadButtonEvent(
+ controllerData.getGUID(),
+ controllerData.getPort(),
+ event.keyCode,
+ action
+ )
+ onInputReceived(event.device)
+ return true
+ }
+
+ private fun onMotionEvent(event: MotionEvent): Boolean {
+ if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
+ event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
+ ) {
+ return false
+ }
+
+ // Temp workaround for DPads that give both axis and button input. The input system can't
+ // take in a specific axis direction for a binding so you lose half of the directions for a DPad.
+
+ val controllerData =
+ InputHandler.androidControllers[event.device.controllerNumber] ?: return false
+ event.device.motionRanges.forEach {
+ NativeInput.onGamePadAxisEvent(
+ controllerData.getGUID(),
+ controllerData.getPort(),
+ it.axis,
+ event.getAxisValue(it.axis)
+ )
+ onInputReceived(event.device)
+ }
+ return true
+ }
+
+ private fun onInputReceived(device: InputDevice) {
+ val params = ParamPackage(NativeInput.getNextInput())
+ if (params.has("engine") && isInputAcceptable(params) && !inputAccepted) {
+ inputAccepted = true
+ setResult(params, device)
+ }
+ }
+
+ private fun setResult(params: ParamPackage, device: InputDevice) {
+ NativeInput.stopMapping()
+ params.set("display", "${device.name} ${params.get("port", 0)}")
+ when (val item = settingsViewModel.clickedItem as InputSetting) {
+ is ModifierInputSetting,
+ is ButtonInputSetting -> {
+ // Invert DPad up and left bindings by default
+ val tempSetting = inputSetting as? ButtonInputSetting
+ if (tempSetting != null) {
+ if (tempSetting.nativeButton == NativeButton.DUp ||
+ tempSetting.nativeButton == NativeButton.DLeft &&
+ params.has("axis")
+ ) {
+ params.set("invert", "-")
+ }
+ }
+
+ item.setSelectedValue(params)
+ settingsViewModel.setAdapterItemChanged(position)
+ }
+
+ is AnalogInputSetting -> {
+ var analogParam = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
+ analogParam = adjustAnalogParam(params, analogParam, item.analogDirection.param)
+
+ // Invert Y-Axis by default
+ analogParam.set("invert_y", "-")
+
+ item.setSelectedValue(analogParam)
+ settingsViewModel.setReloadListAndNotifyDataset(true)
+ }
+ }
+ dismiss()
+ }
+
+ private fun adjustAnalogParam(
+ inputParam: ParamPackage,
+ analogParam: ParamPackage,
+ buttonName: String
+ ): ParamPackage {
+ // The poller returned a complete axis, so set all the buttons
+ if (inputParam.has("axis_x") && inputParam.has("axis_y")) {
+ return inputParam
+ }
+
+ // Check if the current configuration has either no engine or an axis binding.
+ // Clears out the old binding and adds one with analog_from_button.
+ if (!analogParam.has("engine") || analogParam.has("axis_x") || analogParam.has("axis_y")) {
+ analogParam.clear()
+ analogParam.set("engine", "analog_from_button")
+ }
+ analogParam.set(buttonName, inputParam.serialize())
+ return analogParam
+ }
+
+ private fun isInputAcceptable(params: ParamPackage): Boolean {
+ if (InputHandler.registeredControllers.size == 1) {
+ return true
+ }
+
+ if (params.has("motion")) {
+ return true
+ }
+
+ val currentDevice = settingsViewModel.getCurrentDeviceParams(params)
+ if (currentDevice.get("engine", "any") == "any") {
+ return true
+ }
+
+ val guidMatch = params.get("guid", "") == currentDevice.get("guid", "") ||
+ params.get("guid", "") == currentDevice.get("guid2", "")
+ return params.get("engine", "") == currentDevice.get("engine", "") &&
+ guidMatch &&
+ params.get("port", 0) == currentDevice.get("port", 0)
+ }
+
+ companion object {
+ const val TAG = "InputDialogFragment"
+
+ const val POSITION = "Position"
+
+ fun newInstance(
+ inputMappingViewModel: SettingsViewModel,
+ setting: InputSetting,
+ position: Int
+ ): InputDialogFragment {
+ inputMappingViewModel.clickedItem = setting
+ val args = Bundle()
+ args.putInt(POSITION, position)
+ val fragment = InputDialogFragment()
+ fragment.arguments = args
+ return fragment
+ }
+ }
+}