diff options
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.kt | 300 |
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 + } + } +} |