mirror of
https://github.com/Lime3DS/Lime3DS.git
synced 2025-12-16 12:08:49 +00:00
complete refactor
This commit is contained in:
parent
371d747df7
commit
a438094d44
@ -35,8 +35,6 @@ class InputBindingSetting(
|
|||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var key: String = ""
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if this key is for the 3DS Circle Pad
|
* Returns true if this key is for the 3DS Circle Pad
|
||||||
*/
|
*/
|
||||||
@ -81,6 +79,36 @@ class InputBindingSetting(
|
|||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this is an up or down dpad button
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun isVerticalButton(): Boolean =
|
||||||
|
when (abstractSetting.key) {
|
||||||
|
Settings.KEY_BUTTON_DOWN,
|
||||||
|
Settings.KEY_BUTTON_UP -> true
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isVerticalAxis(): Boolean {
|
||||||
|
return abstractSetting.key ==
|
||||||
|
Settings.KEY_DPAD_AXIS_VERTICAL
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isHorizontalAxis(): Boolean {
|
||||||
|
return abstractSetting.key ==
|
||||||
|
Settings.KEY_DPAD_AXIS_HORIZONTAL
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isHorizontalButton(): Boolean =
|
||||||
|
when (abstractSetting.key) {
|
||||||
|
Settings.KEY_BUTTON_LEFT,
|
||||||
|
Settings.KEY_BUTTON_RIGHT -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if this key is for the 3DS L/R or ZL/ZR buttons. Note, these are not real
|
* Returns true if this key is for the 3DS L/R or ZL/ZR buttons. Note, these are not real
|
||||||
* triggers on the 3DS, but we support them as such on a physical gamepad.
|
* triggers on the 3DS, but we support them as such on a physical gamepad.
|
||||||
@ -233,29 +261,6 @@ class InputBindingSetting(
|
|||||||
value = uiString
|
value = uiString
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores the provided key input setting as an Android preference.
|
|
||||||
* Only gets applied when apply(); is called.
|
|
||||||
*
|
|
||||||
* @param keyEvent KeyEvent of this key press.
|
|
||||||
*/
|
|
||||||
fun onKeyInputDeferred(keyEvent: KeyEvent) {
|
|
||||||
if (!isButtonMappingSupported()) {
|
|
||||||
Toast.makeText(context, R.string.input_message_analog_only, Toast.LENGTH_LONG).show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
key = getInputButtonKey(keyEvent.keyCode)
|
|
||||||
val uiString = "${keyEvent.device.name}: Button ${keyEvent.keyCode}"
|
|
||||||
value = uiString
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores the provided key input setting as an Android preference.
|
|
||||||
*/
|
|
||||||
fun applyMapping() {
|
|
||||||
writeButtonMapping(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the provided motion input setting as an Android preference.
|
* Saves the provided motion input setting as an Android preference.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -4,40 +4,85 @@ import android.app.AlertDialog
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.view.InputDevice
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import org.citra.citra_emu.R
|
import org.citra.citra_emu.R
|
||||||
import org.citra.citra_emu.databinding.DialogControllerQuickConfigBinding
|
import org.citra.citra_emu.databinding.DialogControllerQuickConfigBinding
|
||||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
|
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
|
||||||
import kotlin.math.abs
|
import org.citra.citra_emu.features.settings.utils.InputBindingBase
|
||||||
|
|
||||||
|
|
||||||
class ControllerQuickConfigDialog(
|
class ControllerQuickConfigDialog(
|
||||||
private var context: Context,
|
private var context: Context,
|
||||||
buttons: ArrayList<List<String>>,
|
buttons: ArrayList<List<String>>,
|
||||||
titles: ArrayList<List<Int>>,
|
titles: ArrayList<List<Int>>,
|
||||||
private var preferences: SharedPreferences
|
private var preferences: SharedPreferences,
|
||||||
|
adapter: SettingsAdapter
|
||||||
) {
|
) {
|
||||||
|
private var isWaiting = false;
|
||||||
private var index = 0
|
private var index = 0
|
||||||
val inflater = LayoutInflater.from(context)
|
val inflater = LayoutInflater.from(context)
|
||||||
val quickConfigBinding = DialogControllerQuickConfigBinding.inflate(inflater)
|
private lateinit var quickConfigBinding: DialogControllerQuickConfigBinding
|
||||||
var dialog: AlertDialog? = null
|
var dialog: AlertDialog? = null
|
||||||
|
private var boundVerticalDpadAxis = false
|
||||||
|
private var boundHorizontalDpadAxis = false
|
||||||
|
|
||||||
var allButtons = arrayListOf<String>()
|
var allButtons = arrayListOf<String>()
|
||||||
var allTitles = arrayListOf<Int>()
|
var allTitles = arrayListOf<Int>()
|
||||||
|
|
||||||
|
private val inputHandler = object : InputBindingBase() {
|
||||||
|
private val AXIS_WAIT_TIME = 400L
|
||||||
|
private val BUTTON_WAIT_TIME = 100L
|
||||||
|
|
||||||
|
private val handler = android.os.Handler(android.os.Looper.getMainLooper())
|
||||||
|
|
||||||
|
override fun onAxisCaptured() {
|
||||||
|
boundHorizontalDpadAxis =
|
||||||
|
boundHorizontalDpadAxis || setting != null && setting!!.isVerticalAxis()
|
||||||
|
boundVerticalDpadAxis =
|
||||||
|
boundVerticalDpadAxis || setting != null && setting!!.isHorizontalAxis()
|
||||||
|
onInputCaptured(AXIS_WAIT_TIME)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onButtonCaptured() {
|
||||||
|
onInputCaptured(BUTTON_WAIT_TIME)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onInputCaptured(waitTime: Long) {
|
||||||
|
if (isWaiting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
quickConfigBinding.root.cancelPendingInputEvents()
|
||||||
|
index++
|
||||||
|
setting?.let { settingsList.add(it) }
|
||||||
|
isWaiting = true
|
||||||
|
quickConfigBinding.currentMappingTitle.text = context.getString(R.string.controller_quick_config_wait)
|
||||||
|
|
||||||
|
// the changed item after each setting is one more than the index, since the Quick Configure button is position 1
|
||||||
|
adapter.notifyItemChanged(index + 1)
|
||||||
|
|
||||||
|
// clear listeners during the waiting period, they will get reset once ready for input
|
||||||
|
dialog?.setOnKeyListener(null)
|
||||||
|
quickConfigBinding.root.setOnGenericMotionListener(null)
|
||||||
|
|
||||||
|
// Wait before preparing the next input
|
||||||
|
handler.postDelayed({
|
||||||
|
isWaiting = false
|
||||||
|
prepareUIforIndex()
|
||||||
|
}, waitTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCurrentSetting() = setting
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
buttons.forEach {group ->
|
buttons.forEach { group ->
|
||||||
group.forEach {button ->
|
group.forEach { button ->
|
||||||
allButtons.add(button)
|
allButtons.add(button)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
titles.forEach {group ->
|
titles.forEach { group ->
|
||||||
group.forEach {title ->
|
group.forEach { title ->
|
||||||
allTitles.add(title)
|
allTitles.add(title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -45,10 +90,11 @@ class ControllerQuickConfigDialog(
|
|||||||
|
|
||||||
fun show() {
|
fun show() {
|
||||||
val builder: AlertDialog.Builder = AlertDialog.Builder(context)
|
val builder: AlertDialog.Builder = AlertDialog.Builder(context)
|
||||||
|
quickConfigBinding = DialogControllerQuickConfigBinding.inflate(inflater)
|
||||||
builder
|
builder
|
||||||
.setView(quickConfigBinding.root)
|
.setView(quickConfigBinding.root)
|
||||||
.setTitle(context.getString(R.string.controller_quick_config))
|
.setTitle(context.getString(R.string.controller_quick_config))
|
||||||
.setPositiveButton(context.getString(R.string.next)) {_,_ -> }
|
.setPositiveButton(context.getString(R.string.next)) { _, _ -> }
|
||||||
.setNegativeButton(context.getString(R.string.close)) { dialog, which ->
|
.setNegativeButton(context.getString(R.string.close)) { dialog, which ->
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
@ -56,36 +102,63 @@ class ControllerQuickConfigDialog(
|
|||||||
dialog = builder.create()
|
dialog = builder.create()
|
||||||
dialog?.show()
|
dialog?.show()
|
||||||
|
|
||||||
dialog?.setOnKeyListener { _, _, event -> onKeyEvent(event) }
|
quickConfigBinding.root.requestFocus()
|
||||||
quickConfigBinding.root.setOnGenericMotionListener { _, event -> onMotionEvent(event) }
|
quickConfigBinding.root.setOnFocusChangeListener { v, hasFocus ->
|
||||||
|
if (!hasFocus) v.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare the first element
|
// Prepare the first element
|
||||||
prepareUIforIndex(index)
|
prepareUIforIndex()
|
||||||
|
|
||||||
val nextButton = dialog?.getButton(AlertDialog.BUTTON_POSITIVE)
|
val nextButton = dialog?.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||||
nextButton?.setOnClickListener {
|
nextButton?.setOnClickListener {
|
||||||
// Skip to next:
|
if (setting != null) setting!!.removeOldMapping()
|
||||||
prepareUIforIndex(index++)
|
index++
|
||||||
|
prepareUIforIndex()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun prepareUIforIndex(i: Int) {
|
private fun prepareUIforIndex() {
|
||||||
if (allButtons.size-1 < i) {
|
if (index >= allButtons.size) {
|
||||||
settingsList.forEach { it.applyMapping() }
|
|
||||||
dialog?.dismiss()
|
dialog?.dismiss()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
setting = InputBindingSetting(
|
||||||
|
InputBindingSetting.getInputObject(allButtons[index], preferences),
|
||||||
|
allTitles[index]
|
||||||
|
)
|
||||||
|
// skip the dpad buttons if the corresponding axis is mapped
|
||||||
|
while (setting != null && (
|
||||||
|
setting!!.isVerticalButton() && boundVerticalDpadAxis ||
|
||||||
|
setting!!.isHorizontalButton() && boundHorizontalDpadAxis)
|
||||||
|
) {
|
||||||
|
index++
|
||||||
|
if (index >= allButtons.size) {
|
||||||
|
dialog?.dismiss()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setting = InputBindingSetting(
|
||||||
|
InputBindingSetting.getInputObject(
|
||||||
|
allButtons[index],
|
||||||
|
preferences
|
||||||
|
), allTitles[index]
|
||||||
|
)
|
||||||
|
|
||||||
if (index>0) {
|
}
|
||||||
|
// show the previous key, if this isn't the first key
|
||||||
|
if (index > 0) {
|
||||||
quickConfigBinding.lastMappingIcon.visibility = View.VISIBLE
|
quickConfigBinding.lastMappingIcon.visibility = View.VISIBLE
|
||||||
quickConfigBinding.lastMappingDescription.visibility = View.VISIBLE
|
quickConfigBinding.lastMappingDescription.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentButton = allButtons[i]
|
// change the button layout for the last button
|
||||||
val currentTitleInt = allTitles[i]
|
if (index == allButtons.size - 1) {
|
||||||
|
dialog?.getButton(AlertDialog.BUTTON_POSITIVE)?.text =
|
||||||
val button = InputBindingSetting.getInputObject(currentButton, preferences)
|
context.getString(R.string.finish)
|
||||||
|
dialog?.getButton(AlertDialog.BUTTON_NEGATIVE)?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// set all the icons and text
|
||||||
var lastTitle = setting?.value ?: ""
|
var lastTitle = setting?.value ?: ""
|
||||||
if (lastTitle.isBlank()) {
|
if (lastTitle.isBlank()) {
|
||||||
lastTitle = context.getString(R.string.unassigned)
|
lastTitle = context.getString(R.string.unassigned)
|
||||||
@ -93,16 +166,23 @@ class ControllerQuickConfigDialog(
|
|||||||
quickConfigBinding.lastMappingDescription.text = lastTitle
|
quickConfigBinding.lastMappingDescription.text = lastTitle
|
||||||
quickConfigBinding.lastMappingIcon.setImageDrawable(quickConfigBinding.currentMappingIcon.drawable)
|
quickConfigBinding.lastMappingIcon.setImageDrawable(quickConfigBinding.currentMappingIcon.drawable)
|
||||||
|
|
||||||
setting = InputBindingSetting(button, currentTitleInt)
|
|
||||||
quickConfigBinding.currentMappingTitle.text = calculateTitle()
|
quickConfigBinding.currentMappingTitle.text = calculateTitle()
|
||||||
quickConfigBinding.currentMappingDescription.text = setting?.value
|
quickConfigBinding.currentMappingDescription.text = setting?.value
|
||||||
quickConfigBinding.currentMappingIcon.setImageDrawable(getIcon())
|
quickConfigBinding.currentMappingIcon.setImageDrawable(getIcon())
|
||||||
|
|
||||||
if (allButtons.size-1 < index) {
|
// reset all the handlers
|
||||||
dialog?.getButton(AlertDialog.BUTTON_POSITIVE)?.text =
|
if (setting!!.isButtonMappingSupported()) {
|
||||||
context.getString(R.string.finish)
|
dialog?.setOnKeyListener { _, _, event -> inputHandler.onKeyEvent(event) }
|
||||||
dialog?.getButton(AlertDialog.BUTTON_NEGATIVE)?.visibility = View.GONE
|
|
||||||
}
|
}
|
||||||
|
if (setting!!.isAxisMappingSupported()) {
|
||||||
|
quickConfigBinding.root.setOnGenericMotionListener { _, event ->
|
||||||
|
inputHandler.onMotionEvent(
|
||||||
|
event
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inputHandler.reset()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculateTitle(): String {
|
private fun calculateTitle(): String {
|
||||||
@ -140,98 +220,7 @@ class ControllerQuickConfigDialog(
|
|||||||
return ContextCompat.getDrawable(context, id)
|
return ContextCompat.getDrawable(context, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val previousValues = ArrayList<Float>()
|
|
||||||
private var prevDeviceId = 0
|
|
||||||
private var waitingForEvent = true
|
|
||||||
private var setting: InputBindingSetting? = null
|
private var setting: InputBindingSetting? = null
|
||||||
private var debounceTimestamp = System.currentTimeMillis()
|
|
||||||
|
|
||||||
private var settingsList = arrayListOf<InputBindingSetting>()
|
private var settingsList = arrayListOf<InputBindingSetting>()
|
||||||
|
|
||||||
private fun onKeyEvent(event: KeyEvent): Boolean {
|
|
||||||
return when (event.action) {
|
|
||||||
KeyEvent.ACTION_UP -> {
|
|
||||||
if (System.currentTimeMillis()-debounceTimestamp < DEBOUNCE_TIMER) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
debounceTimestamp = System.currentTimeMillis()
|
|
||||||
index++
|
|
||||||
setting?.let {
|
|
||||||
it.onKeyInputDeferred(event)
|
|
||||||
settingsList.add(it)
|
|
||||||
}
|
|
||||||
prepareUIforIndex(index)
|
|
||||||
// Even if we ignore the key, we still consume it. Thus return true regardless.
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onMotionEvent(event: MotionEvent): Boolean {
|
|
||||||
if ((event.source and InputDevice.SOURCE_CLASS_JOYSTICK == 0) ||
|
|
||||||
event.action != MotionEvent.ACTION_MOVE) return false
|
|
||||||
|
|
||||||
val input = event.device
|
|
||||||
val motionRanges = input.motionRanges
|
|
||||||
|
|
||||||
if (input.id != prevDeviceId) {
|
|
||||||
previousValues.clear()
|
|
||||||
}
|
|
||||||
prevDeviceId = input.id
|
|
||||||
val firstEvent = previousValues.isEmpty()
|
|
||||||
|
|
||||||
var numMovedAxis = 0
|
|
||||||
var axisMoveValue = 0.0f
|
|
||||||
var lastMovedRange: InputDevice.MotionRange? = null
|
|
||||||
var lastMovedDir = '?'
|
|
||||||
if (waitingForEvent) {
|
|
||||||
for (i in motionRanges.indices) {
|
|
||||||
val range = motionRanges[i]
|
|
||||||
val axis = range.axis
|
|
||||||
val origValue = event.getAxisValue(axis)
|
|
||||||
if (firstEvent) {
|
|
||||||
previousValues.add(origValue)
|
|
||||||
} else {
|
|
||||||
val previousValue = previousValues[i]
|
|
||||||
|
|
||||||
// Only handle the axes that are not neutral (more than 0.5)
|
|
||||||
// but ignore any axis that has a constant value (e.g. always 1)
|
|
||||||
if (abs(origValue) > 0.5f && origValue != previousValue) {
|
|
||||||
// It is common to have multiple axes with the same physical input. For example,
|
|
||||||
// shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE.
|
|
||||||
// To handle this, we ignore an axis motion that's the exact same as a motion
|
|
||||||
// we already saw. This way, we ignore axes with two names, but catch the case
|
|
||||||
// where a joystick is moved in two directions.
|
|
||||||
// ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html
|
|
||||||
if (origValue != axisMoveValue) {
|
|
||||||
axisMoveValue = origValue
|
|
||||||
numMovedAxis++
|
|
||||||
lastMovedRange = range
|
|
||||||
lastMovedDir = if (origValue < 0.0f) '-' else '+'
|
|
||||||
}
|
|
||||||
} else if (abs(origValue) < 0.25f && abs(previousValue) > 0.75f) {
|
|
||||||
// Special case for d-pads (axis value jumps between 0 and 1 without any values
|
|
||||||
// in between). Without this, the user would need to press the d-pad twice
|
|
||||||
// due to the first press being caught by the "if (firstEvent)" case further up.
|
|
||||||
numMovedAxis++
|
|
||||||
lastMovedRange = range
|
|
||||||
lastMovedDir = if (previousValue < 0.0f) '-' else '+'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
previousValues[i] = origValue
|
|
||||||
}
|
|
||||||
// If only one axis moved, that's the winner.
|
|
||||||
if (numMovedAxis == 1) {
|
|
||||||
waitingForEvent = false
|
|
||||||
setting?.onMotionInput(input, lastMovedRange!!, lastMovedDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val DEBOUNCE_TIMER = 100
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -617,7 +617,8 @@ class SettingsAdapter(
|
|||||||
Settings.triggerTitles
|
Settings.triggerTitles
|
||||||
)
|
)
|
||||||
|
|
||||||
ControllerQuickConfigDialog(context, buttons, titles, PermissionsHandler.preferences).show()
|
ControllerQuickConfigDialog(context, buttons, titles, PermissionsHandler.preferences, this).show()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun closeDialog() {
|
fun closeDialog() {
|
||||||
|
|||||||
@ -764,8 +764,8 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||||||
add(
|
add(
|
||||||
RunnableSetting(
|
RunnableSetting(
|
||||||
R.string.controller_quick_config,
|
R.string.controller_quick_config,
|
||||||
0,
|
R.string.controller_quick_config_description,
|
||||||
false,
|
true,
|
||||||
0,
|
0,
|
||||||
{ settingsAdapter.onClickControllerQuickConfig() }
|
{ settingsAdapter.onClickControllerQuickConfig() }
|
||||||
)
|
)
|
||||||
|
|||||||
@ -0,0 +1,87 @@
|
|||||||
|
package org.citra.citra_emu.features.settings.utils
|
||||||
|
|
||||||
|
import android.view.InputDevice
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
abstract class InputBindingBase {
|
||||||
|
private val previousValues = ArrayList<Float>()
|
||||||
|
private var prevDeviceId = 0
|
||||||
|
private var waitingForEvent = true
|
||||||
|
|
||||||
|
abstract fun onButtonCaptured()
|
||||||
|
abstract fun onAxisCaptured()
|
||||||
|
abstract fun getCurrentSetting(): InputBindingSetting?
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
previousValues.clear()
|
||||||
|
prevDeviceId = 0
|
||||||
|
waitingForEvent = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onMotionEvent(event: MotionEvent): Boolean {
|
||||||
|
if ((event.source and InputDevice.SOURCE_CLASS_JOYSTICK == 0) ||
|
||||||
|
event.action != MotionEvent.ACTION_MOVE) return false
|
||||||
|
|
||||||
|
val input = event.device
|
||||||
|
val motionRanges = input.motionRanges
|
||||||
|
|
||||||
|
if (input.id != prevDeviceId) {
|
||||||
|
previousValues.clear()
|
||||||
|
}
|
||||||
|
prevDeviceId = input.id
|
||||||
|
val firstEvent = previousValues.isEmpty()
|
||||||
|
|
||||||
|
var numMovedAxis = 0
|
||||||
|
var axisMoveValue = 0.0f
|
||||||
|
var lastMovedRange: InputDevice.MotionRange? = null
|
||||||
|
var lastMovedDir = '?'
|
||||||
|
|
||||||
|
if (waitingForEvent) {
|
||||||
|
for (i in motionRanges.indices) {
|
||||||
|
val range = motionRanges[i]
|
||||||
|
val axis = range.axis
|
||||||
|
val origValue = event.getAxisValue(axis)
|
||||||
|
if (firstEvent) {
|
||||||
|
previousValues.add(origValue)
|
||||||
|
} else {
|
||||||
|
val previousValue = previousValues[i]
|
||||||
|
|
||||||
|
if (abs(origValue) > 0.5f && origValue != previousValue) {
|
||||||
|
if (origValue != axisMoveValue) {
|
||||||
|
axisMoveValue = origValue
|
||||||
|
numMovedAxis++
|
||||||
|
lastMovedRange = range
|
||||||
|
lastMovedDir = if (origValue < 0.0f) '-' else '+'
|
||||||
|
}
|
||||||
|
} else if (abs(origValue) < 0.25f && abs(previousValue) > 0.75f) {
|
||||||
|
numMovedAxis++
|
||||||
|
lastMovedRange = range
|
||||||
|
lastMovedDir = if (previousValue < 0.0f) '-' else '+'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previousValues[i] = origValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numMovedAxis == 1) {
|
||||||
|
waitingForEvent = false
|
||||||
|
getCurrentSetting()?.onMotionInput(input, lastMovedRange!!, lastMovedDir)
|
||||||
|
onAxisCaptured()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onKeyEvent(event: KeyEvent): Boolean {
|
||||||
|
return when (event.action) {
|
||||||
|
KeyEvent.ACTION_UP -> {
|
||||||
|
getCurrentSetting()?.onKeyInput(event)
|
||||||
|
onButtonCaptured()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,10 +6,7 @@ package org.citra.citra_emu.fragments
|
|||||||
|
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.InputDevice
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
@ -17,10 +14,21 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
|||||||
import org.citra.citra_emu.R
|
import org.citra.citra_emu.R
|
||||||
import org.citra.citra_emu.databinding.DialogInputBinding
|
import org.citra.citra_emu.databinding.DialogInputBinding
|
||||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
|
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
|
||||||
import org.citra.citra_emu.utils.Log
|
import org.citra.citra_emu.features.settings.utils.InputBindingBase
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
private val inputHandler = object : InputBindingBase() {
|
||||||
|
override fun onButtonCaptured() {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAxisCaptured() {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCurrentSetting() = setting
|
||||||
|
}
|
||||||
private var _binding: DialogInputBinding? = null
|
private var _binding: DialogInputBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
@ -28,10 +36,6 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
|||||||
private var onCancel: (() -> Unit)? = null
|
private var onCancel: (() -> Unit)? = null
|
||||||
private var onDismiss: (() -> Unit)? = null
|
private var onDismiss: (() -> Unit)? = null
|
||||||
|
|
||||||
private val previousValues = ArrayList<Float>()
|
|
||||||
private var prevDeviceId = 0
|
|
||||||
private var waitingForEvent = true
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
if (setting == null) {
|
if (setting == null) {
|
||||||
@ -57,10 +61,10 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
|||||||
view.requestFocus()
|
view.requestFocus()
|
||||||
view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
|
view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
|
||||||
if (setting!!.isButtonMappingSupported()) {
|
if (setting!!.isButtonMappingSupported()) {
|
||||||
dialog?.setOnKeyListener { _, _, event -> onKeyEvent(event) }
|
dialog?.setOnKeyListener { _, _, event -> inputHandler.onKeyEvent(event) }
|
||||||
}
|
}
|
||||||
if (setting!!.isAxisMappingSupported()) {
|
if (setting!!.isAxisMappingSupported()) {
|
||||||
binding.root.setOnGenericMotionListener { _, event -> onMotionEvent(event) }
|
binding.root.setOnGenericMotionListener { _, event -> inputHandler.onMotionEvent(event) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val inputTypeId = when {
|
val inputTypeId = when {
|
||||||
@ -108,85 +112,7 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
|||||||
onDismiss?.invoke()
|
onDismiss?.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onKeyEvent(event: KeyEvent): Boolean {
|
|
||||||
Log.debug("[MotionBottomSheetDialogFragment] Received key event: " + event.action)
|
|
||||||
return when (event.action) {
|
|
||||||
KeyEvent.ACTION_UP -> {
|
|
||||||
setting?.onKeyInput(event)
|
|
||||||
dismiss()
|
|
||||||
// Even if we ignore the key, we still consume it. Thus return true regardless.
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onMotionEvent(event: MotionEvent): Boolean {
|
|
||||||
Log.debug("[MotionBottomSheetDialogFragment] Received motion event: " + event.action)
|
|
||||||
if (event.source and InputDevice.SOURCE_CLASS_JOYSTICK == 0) return false
|
|
||||||
if (event.action != MotionEvent.ACTION_MOVE) return false
|
|
||||||
|
|
||||||
val input = event.device
|
|
||||||
|
|
||||||
val motionRanges = input.motionRanges
|
|
||||||
|
|
||||||
if (input.id != prevDeviceId) {
|
|
||||||
previousValues.clear()
|
|
||||||
}
|
|
||||||
prevDeviceId = input.id
|
|
||||||
val firstEvent = previousValues.isEmpty()
|
|
||||||
|
|
||||||
var numMovedAxis = 0
|
|
||||||
var axisMoveValue = 0.0f
|
|
||||||
var lastMovedRange: InputDevice.MotionRange? = null
|
|
||||||
var lastMovedDir = '?'
|
|
||||||
if (waitingForEvent) {
|
|
||||||
for (i in motionRanges.indices) {
|
|
||||||
val range = motionRanges[i]
|
|
||||||
val axis = range.axis
|
|
||||||
val origValue = event.getAxisValue(axis)
|
|
||||||
if (firstEvent) {
|
|
||||||
previousValues.add(origValue)
|
|
||||||
} else {
|
|
||||||
val previousValue = previousValues[i]
|
|
||||||
|
|
||||||
// Only handle the axes that are not neutral (more than 0.5)
|
|
||||||
// but ignore any axis that has a constant value (e.g. always 1)
|
|
||||||
if (abs(origValue) > 0.5f && origValue != previousValue) {
|
|
||||||
// It is common to have multiple axes with the same physical input. For example,
|
|
||||||
// shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE.
|
|
||||||
// To handle this, we ignore an axis motion that's the exact same as a motion
|
|
||||||
// we already saw. This way, we ignore axes with two names, but catch the case
|
|
||||||
// where a joystick is moved in two directions.
|
|
||||||
// ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html
|
|
||||||
if (origValue != axisMoveValue) {
|
|
||||||
axisMoveValue = origValue
|
|
||||||
numMovedAxis++
|
|
||||||
lastMovedRange = range
|
|
||||||
lastMovedDir = if (origValue < 0.0f) '-' else '+'
|
|
||||||
}
|
|
||||||
} else if (abs(origValue) < 0.25f && abs(previousValue) > 0.75f) {
|
|
||||||
// Special case for d-pads (axis value jumps between 0 and 1 without any values
|
|
||||||
// in between). Without this, the user would need to press the d-pad twice
|
|
||||||
// due to the first press being caught by the "if (firstEvent)" case further up.
|
|
||||||
numMovedAxis++
|
|
||||||
lastMovedRange = range
|
|
||||||
lastMovedDir = if (previousValue < 0.0f) '-' else '+'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
previousValues[i] = origValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// If only one axis moved, that's the winner.
|
|
||||||
if (numMovedAxis == 1) {
|
|
||||||
waitingForEvent = false
|
|
||||||
setting?.onMotionInput(input, lastMovedRange!!, lastMovedDir)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "MotionBottomSheetDialogFragment"
|
const val TAG = "MotionBottomSheetDialogFragment"
|
||||||
|
|||||||
@ -3,7 +3,9 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/linearLayout"
|
android:id="@+id/linearLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:focusable="true"
|
||||||
|
android:focusableInTouchMode="true">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/lastMappingIcon"
|
android:id="@+id/lastMappingIcon"
|
||||||
|
|||||||
@ -126,6 +126,8 @@
|
|||||||
<string name="controller_axis_vertical">Up/Down Axis</string>
|
<string name="controller_axis_vertical">Up/Down Axis</string>
|
||||||
<string name="controller_axis_horizontal">Left/Right Axis</string>
|
<string name="controller_axis_horizontal">Left/Right Axis</string>
|
||||||
<string name="controller_quick_config">Quick Configure</string>
|
<string name="controller_quick_config">Quick Configure</string>
|
||||||
|
<string name="controller_quick_config_description">Quickly configure all input buttons (not including hotkeys)</string>
|
||||||
|
<string name="controller_quick_config_wait">Please Wait</string>
|
||||||
<string name="direction_up">Up</string>
|
<string name="direction_up">Up</string>
|
||||||
<string name="direction_down">Down</string>
|
<string name="direction_down">Down</string>
|
||||||
<string name="direction_left">Left</string>
|
<string name="direction_left">Left</string>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user