From 781652a1c0b51a79894fa8bf941e3d4c3fabb7fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20N=C3=BCsse?= Date: Wed, 10 Jul 2024 11:07:29 +0200 Subject: [PATCH 1/7] [WIP] implement button-quickassignment --- .../model/view/InputBindingSetting.kt | 18 ++ .../ui/ControllerAutomappingDialog.kt | 241 ++++++++++++++++++ .../features/settings/ui/SettingsAdapter.kt | 28 ++ .../settings/ui/SettingsFragmentPresenter.kt | 43 ++-- .../layout/dialog_controllerautomapping.xml | 73 ++++++ .../app/src/main/res/values/strings.xml | 3 + 6 files changed, 382 insertions(+), 24 deletions(-) create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerAutomappingDialog.kt create mode 100644 src/android/app/src/main/res/layout/dialog_controllerautomapping.xml diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt index 64827d89d..5e604f65a 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt @@ -16,6 +16,7 @@ import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.R import org.citra.citra_emu.features.hotkeys.Hotkey import org.citra.citra_emu.features.settings.model.AbstractSetting +import org.citra.citra_emu.features.settings.model.AbstractStringSetting import org.citra.citra_emu.features.settings.model.Settings class InputBindingSetting( @@ -330,5 +331,22 @@ class InputBindingSetting( event.keyCode } } + + fun getInputObject(key: String, preferences: SharedPreferences): AbstractStringSetting { + return object : AbstractStringSetting { + override var string: String + get() = preferences.getString(key, "")!! + set(value) { + preferences.edit() + .putString(key, value) + .apply() + } + override val key = key + override val section = Settings.SECTION_CONTROLS + override val isRuntimeEditable = true + override val valueAsString = preferences.getString(key, "")!! + override val defaultValue = "" + } + } } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerAutomappingDialog.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerAutomappingDialog.kt new file mode 100644 index 000000000..570d5e59c --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerAutomappingDialog.kt @@ -0,0 +1,241 @@ +package org.citra.citra_emu.features.settings.ui + +import android.app.AlertDialog +import android.content.Context +import android.content.SharedPreferences +import android.graphics.drawable.Drawable +import android.view.InputDevice +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import androidx.core.content.ContextCompat +import org.citra.citra_emu.R +import org.citra.citra_emu.databinding.DialogControllerautomappingBinding +import org.citra.citra_emu.features.settings.model.view.InputBindingSetting +import kotlin.math.abs + + +class ControllerAutomappingDialog( + private var context: Context, + buttons: ArrayList>, + titles: ArrayList>, + private var preferences: SharedPreferences +) { + + private var index = 0 + val inflater = LayoutInflater.from(context) + val automappingBinding = DialogControllerautomappingBinding.inflate(inflater) + var dialog: AlertDialog? = null + + var allButtons = arrayListOf() + var allTitles = arrayListOf() + + init { + buttons.forEach {group -> + group.forEach {button -> + allButtons.add(button) + } + } + titles.forEach {group -> + group.forEach {title -> + allTitles.add(title) + } + } + + } + + fun show() { + val builder: AlertDialog.Builder = AlertDialog.Builder(context) + builder + .setView(automappingBinding.root) + .setTitle("Automapper") + .setPositiveButton("Next") {_,_ -> } + .setNegativeButton("Close") { dialog, which -> + dialog.dismiss() + } + + dialog = builder.create() + dialog?.show() + + dialog?.setOnKeyListener { _, _, event -> onKeyEvent(event) } + automappingBinding.root.setOnGenericMotionListener { _, event -> onMotionEvent(event) } + + // Prepare the first element + prepareUIforIndex(index) + + val nextButton = dialog?.getButton(AlertDialog.BUTTON_POSITIVE) + nextButton?.setOnClickListener { + // Skip to next: + prepareUIforIndex(index++) + } + + } + + private fun prepareUIforIndex(i: Int) { + if (allButtons.size-1 < i) { + dialog?.dismiss() + return + } + + if(index>0) { + automappingBinding.lastMappingIcon.visibility = View.VISIBLE + automappingBinding.lastMappingDescription.visibility = View.VISIBLE + } + + val currentButton = allButtons[i] + val currentTitleInt = allTitles[i] + + val button = InputBindingSetting.getInputObject(currentButton, preferences) + + var lastTitle = setting?.value ?: "" + if(lastTitle.isBlank()) { + lastTitle = context.getString(R.string.unassigned) + } + automappingBinding.lastMappingDescription.text = lastTitle + automappingBinding.lastMappingIcon.setImageDrawable(automappingBinding.currentMappingIcon.drawable) + setting = InputBindingSetting(button, currentTitleInt) + + automappingBinding.currentMappingTitle.text = calculateTitle() + automappingBinding.currentMappingDescription.text = setting?.value + automappingBinding.currentMappingIcon.setImageDrawable(getIcon()) + + + if (allButtons.size-1 < index) { + dialog?.getButton(AlertDialog.BUTTON_POSITIVE)?.text = + context.getString(R.string.finish) + dialog?.getButton(AlertDialog.BUTTON_NEGATIVE)?.visibility = View.GONE + } + + } + + private fun calculateTitle(): String { + + val inputTypeId = when { + setting!!.isCirclePad() -> R.string.controller_circlepad + setting!!.isCStick() -> R.string.controller_c + setting!!.isDPad() -> R.string.controller_dpad + setting!!.isTrigger() -> R.string.controller_trigger + else -> R.string.button + } + + val nameId = setting?.nameId?.let { context.getString(it) } + + return String.format( + context.getString(R.string.input_dialog_title), + context.getString(inputTypeId), + nameId + ) + } + + private fun getIcon(): Drawable? { + val id = when { + setting!!.isCirclePad() -> R.drawable.stick_main + setting!!.isCStick() -> R.drawable.stick_c + setting!!.isDPad() -> R.drawable.dpad + else -> { + val resourceTitle = context.resources.getResourceEntryName(setting!!.nameId) + if(resourceTitle.startsWith("direction")) { + R.drawable.dpad + } else { + context.resources.getIdentifier(resourceTitle, "drawable", context.packageName) + } + } + } + return ContextCompat.getDrawable(context, id) + } + + private val previousValues = ArrayList() + private var prevDeviceId = 0 + private var waitingForEvent = true + private var setting: InputBindingSetting? = null + + + private var debounceTimestamp = System.currentTimeMillis() + + + private fun onKeyEvent(event: KeyEvent): Boolean { + return when (event.action) { + KeyEvent.ACTION_UP -> { + if(System.currentTimeMillis()-debounceTimestamp < 500) { + return true + } + + debounceTimestamp = System.currentTimeMillis() + + index++ + setting?.onKeyInput(event) + 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) 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) + } + } + return true + } + +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt index bc55bd5d6..2ae742f3d 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt @@ -43,6 +43,7 @@ import org.citra.citra_emu.features.settings.model.AbstractStringSetting import org.citra.citra_emu.features.settings.model.FloatSetting import org.citra.citra_emu.features.settings.model.ScaledFloatSetting import org.citra.citra_emu.features.settings.model.AbstractShortSetting +import org.citra.citra_emu.features.settings.model.Settings import org.citra.citra_emu.features.settings.model.view.DateTimeSetting import org.citra.citra_emu.features.settings.model.view.InputBindingSetting import org.citra.citra_emu.features.settings.model.view.SettingsItem @@ -64,6 +65,7 @@ import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.SwitchSettingViewHolder import org.citra.citra_emu.fragments.MessageDialogFragment import org.citra.citra_emu.fragments.MotionBottomSheetDialogFragment +import org.citra.citra_emu.utils.PermissionsHandler.preferences import org.citra.citra_emu.utils.SystemSaveGame import java.lang.NumberFormatException import java.text.SimpleDateFormat @@ -595,6 +597,32 @@ class SettingsAdapter( .show() } + fun onClickAutoconfigureControls() { + + val buttons = arrayListOf( + Settings.buttonKeys, + Settings.circlePadKeys, + Settings.cStickKeys, + Settings.dPadAxisKeys, + Settings.dPadButtonKeys, + Settings.triggerKeys + ) + + val titles = arrayListOf( + Settings.buttonTitles, + Settings.axisTitles, + Settings.axisTitles, + Settings.axisTitles, + Settings.dPadTitles, + Settings.triggerTitles + ) + + Settings.buttonTitles + ControllerAutomappingDialog(context, buttons, titles, preferences).show() + + + } + fun closeDialog() { if (dialog != null) { if (clickedPosition != -1) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index d4baf6166..bafa09d3d 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -761,44 +761,56 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) private fun addControlsSettings(sl: ArrayList) { settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_controls)) sl.apply { + add(HeaderSetting(R.string.auto_configure)) + + add( + RunnableSetting( + R.string.auto_configure, + 0, + false, + 0, + { settingsAdapter.onClickAutoconfigureControls() } + ) + ) + add(HeaderSetting(R.string.generic_buttons)) Settings.buttonKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) + val button = InputBindingSetting.getInputObject(key, preferences) add(InputBindingSetting(button, Settings.buttonTitles[i])) } add(HeaderSetting(R.string.controller_circlepad)) Settings.circlePadKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) + val button = InputBindingSetting.getInputObject(key, preferences) add(InputBindingSetting(button, Settings.axisTitles[i])) } add(HeaderSetting(R.string.controller_c)) Settings.cStickKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) + val button = InputBindingSetting.getInputObject(key, preferences) add(InputBindingSetting(button, Settings.axisTitles[i])) } add(HeaderSetting(R.string.controller_dpad_axis,R.string.controller_dpad_axis_description)) Settings.dPadAxisKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) + val button = InputBindingSetting.getInputObject(key, preferences) add(InputBindingSetting(button, Settings.axisTitles[i])) } add(HeaderSetting(R.string.controller_dpad_button,R.string.controller_dpad_button_description)) Settings.dPadButtonKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) + val button = InputBindingSetting.getInputObject(key, preferences) add(InputBindingSetting(button, Settings.dPadTitles[i])) } add(HeaderSetting(R.string.controller_triggers)) Settings.triggerKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) + val button = InputBindingSetting.getInputObject(key, preferences) add(InputBindingSetting(button, Settings.triggerTitles[i])) } add(HeaderSetting(R.string.controller_hotkeys)) Settings.hotKeys.forEachIndexed { i: Int, key: String -> - val button = getInputObject(key) + val button = InputBindingSetting.getInputObject(key, preferences) add(InputBindingSetting(button, Settings.hotkeyTitles[i])) } add(HeaderSetting(R.string.miscellaneous)) @@ -814,23 +826,6 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) } } - private fun getInputObject(key: String): AbstractStringSetting { - return object : AbstractStringSetting { - override var string: String - get() = preferences.getString(key, "")!! - set(value) { - preferences.edit() - .putString(key, value) - .apply() - } - override val key = key - override val section = Settings.SECTION_CONTROLS - override val isRuntimeEditable = true - override val valueAsString = preferences.getString(key, "")!! - override val defaultValue = "" - } - } - private fun addGraphicsSettings(sl: ArrayList) { settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_graphics)) sl.apply { diff --git a/src/android/app/src/main/res/layout/dialog_controllerautomapping.xml b/src/android/app/src/main/res/layout/dialog_controllerautomapping.xml new file mode 100644 index 000000000..9dd0160ed --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_controllerautomapping.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index f60fdd0e3..305e25bcb 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -175,6 +175,7 @@ HOME Menu + Auto Configuration Buttons Button @@ -385,6 +386,8 @@ Don\'t show again Visibility Information + Finish + Unassigned Select Game Folder From 14377cde15840aa26ee36617c5111fb69c0b25a2 Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Wed, 10 Jul 2024 23:39:55 +0100 Subject: [PATCH 2/7] Code cleanup + Rebrand feature to "Quick Configure" --- ...mappingDialog.kt => ControllerQuickConfigDialog.kt} | 10 +++------- .../citra_emu/features/settings/ui/SettingsAdapter.kt | 9 +++------ .../features/settings/ui/SettingsFragmentPresenter.kt | 6 ++---- ...omapping.xml => dialog_controller_quick_config.xml} | 0 src/android/app/src/main/res/values/strings.xml | 2 +- 5 files changed, 9 insertions(+), 18 deletions(-) rename src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/{ControllerAutomappingDialog.kt => ControllerQuickConfigDialog.kt} (97%) rename src/android/app/src/main/res/layout/{dialog_controllerautomapping.xml => dialog_controller_quick_config.xml} (100%) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerAutomappingDialog.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt similarity index 97% rename from src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerAutomappingDialog.kt rename to src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt index 570d5e59c..f0d405b1f 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerAutomappingDialog.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt @@ -11,12 +11,12 @@ import android.view.MotionEvent import android.view.View import androidx.core.content.ContextCompat import org.citra.citra_emu.R -import org.citra.citra_emu.databinding.DialogControllerautomappingBinding +import org.citra.citra_emu.databinding.DialogControllerQuickConfigBinding import org.citra.citra_emu.features.settings.model.view.InputBindingSetting import kotlin.math.abs -class ControllerAutomappingDialog( +class ControllerQuickConfigDialog( private var context: Context, buttons: ArrayList>, titles: ArrayList>, @@ -25,7 +25,7 @@ class ControllerAutomappingDialog( private var index = 0 val inflater = LayoutInflater.from(context) - val automappingBinding = DialogControllerautomappingBinding.inflate(inflater) + val automappingBinding = DialogControllerQuickConfigBinding.inflate(inflater) var dialog: AlertDialog? = null var allButtons = arrayListOf() @@ -149,11 +149,8 @@ class ControllerAutomappingDialog( private var prevDeviceId = 0 private var waitingForEvent = true private var setting: InputBindingSetting? = null - - private var debounceTimestamp = System.currentTimeMillis() - private fun onKeyEvent(event: KeyEvent): Boolean { return when (event.action) { KeyEvent.ACTION_UP -> { @@ -179,7 +176,6 @@ class ControllerAutomappingDialog( if (event.action != MotionEvent.ACTION_MOVE) return false val input = event.device - val motionRanges = input.motionRanges if (input.id != prevDeviceId) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt index 2ae742f3d..b4ac3c3dc 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt @@ -65,7 +65,7 @@ import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.SwitchSettingViewHolder import org.citra.citra_emu.fragments.MessageDialogFragment import org.citra.citra_emu.fragments.MotionBottomSheetDialogFragment -import org.citra.citra_emu.utils.PermissionsHandler.preferences +import org.citra.citra_emu.utils.PermissionsHandler import org.citra.citra_emu.utils.SystemSaveGame import java.lang.NumberFormatException import java.text.SimpleDateFormat @@ -597,7 +597,7 @@ class SettingsAdapter( .show() } - fun onClickAutoconfigureControls() { + fun onClickControllerQuickConfig() { val buttons = arrayListOf( Settings.buttonKeys, @@ -617,10 +617,7 @@ class SettingsAdapter( Settings.triggerTitles ) - Settings.buttonTitles - ControllerAutomappingDialog(context, buttons, titles, preferences).show() - - + ControllerQuickConfigDialog(context, buttons, titles, PermissionsHandler.preferences).show() } fun closeDialog() { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index bafa09d3d..d90fff4c9 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -761,15 +761,13 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) private fun addControlsSettings(sl: ArrayList) { settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_controls)) sl.apply { - add(HeaderSetting(R.string.auto_configure)) - add( RunnableSetting( - R.string.auto_configure, + R.string.controller_quick_config, 0, false, 0, - { settingsAdapter.onClickAutoconfigureControls() } + { settingsAdapter.onClickControllerQuickConfig() } ) ) diff --git a/src/android/app/src/main/res/layout/dialog_controllerautomapping.xml b/src/android/app/src/main/res/layout/dialog_controller_quick_config.xml similarity index 100% rename from src/android/app/src/main/res/layout/dialog_controllerautomapping.xml rename to src/android/app/src/main/res/layout/dialog_controller_quick_config.xml diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 305e25bcb..9a921c5b7 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -125,6 +125,7 @@ Only map the D-pad to these if you\'re facing issues with the D-Pad (Axis) button mappings. Up/Down Axis Left/Right Axis + Quick Configure Up Down Left @@ -175,7 +176,6 @@ HOME Menu - Auto Configuration Buttons Button From db09c2c9e9646e0e003de9db51ba76bcc88a1261 Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Thu, 11 Jul 2024 19:25:41 +0100 Subject: [PATCH 3/7] More code cleanup --- .../ui/ControllerQuickConfigDialog.kt | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt index f0d405b1f..ef975f89a 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt @@ -22,10 +22,9 @@ class ControllerQuickConfigDialog( titles: ArrayList>, private var preferences: SharedPreferences ) { - private var index = 0 val inflater = LayoutInflater.from(context) - val automappingBinding = DialogControllerQuickConfigBinding.inflate(inflater) + val quickConfigBinding = DialogControllerQuickConfigBinding.inflate(inflater) var dialog: AlertDialog? = null var allButtons = arrayListOf() @@ -42,14 +41,13 @@ class ControllerQuickConfigDialog( allTitles.add(title) } } - } fun show() { val builder: AlertDialog.Builder = AlertDialog.Builder(context) builder - .setView(automappingBinding.root) - .setTitle("Automapper") + .setView(quickConfigBinding.root) + .setTitle("Quick Configure") .setPositiveButton("Next") {_,_ -> } .setNegativeButton("Close") { dialog, which -> dialog.dismiss() @@ -59,7 +57,7 @@ class ControllerQuickConfigDialog( dialog?.show() dialog?.setOnKeyListener { _, _, event -> onKeyEvent(event) } - automappingBinding.root.setOnGenericMotionListener { _, event -> onMotionEvent(event) } + quickConfigBinding.root.setOnGenericMotionListener { _, event -> onMotionEvent(event) } // Prepare the first element prepareUIforIndex(index) @@ -69,7 +67,6 @@ class ControllerQuickConfigDialog( // Skip to next: prepareUIforIndex(index++) } - } private fun prepareUIforIndex(i: Int) { @@ -78,9 +75,9 @@ class ControllerQuickConfigDialog( return } - if(index>0) { - automappingBinding.lastMappingIcon.visibility = View.VISIBLE - automappingBinding.lastMappingDescription.visibility = View.VISIBLE + if (index>0) { + quickConfigBinding.lastMappingIcon.visibility = View.VISIBLE + quickConfigBinding.lastMappingDescription.visibility = View.VISIBLE } val currentButton = allButtons[i] @@ -89,28 +86,25 @@ class ControllerQuickConfigDialog( val button = InputBindingSetting.getInputObject(currentButton, preferences) var lastTitle = setting?.value ?: "" - if(lastTitle.isBlank()) { + if (lastTitle.isBlank()) { lastTitle = context.getString(R.string.unassigned) } - automappingBinding.lastMappingDescription.text = lastTitle - automappingBinding.lastMappingIcon.setImageDrawable(automappingBinding.currentMappingIcon.drawable) + quickConfigBinding.lastMappingDescription.text = lastTitle + quickConfigBinding.lastMappingIcon.setImageDrawable(quickConfigBinding.currentMappingIcon.drawable) + setting = InputBindingSetting(button, currentTitleInt) - - automappingBinding.currentMappingTitle.text = calculateTitle() - automappingBinding.currentMappingDescription.text = setting?.value - automappingBinding.currentMappingIcon.setImageDrawable(getIcon()) - + quickConfigBinding.currentMappingTitle.text = calculateTitle() + quickConfigBinding.currentMappingDescription.text = setting?.value + quickConfigBinding.currentMappingIcon.setImageDrawable(getIcon()) if (allButtons.size-1 < index) { dialog?.getButton(AlertDialog.BUTTON_POSITIVE)?.text = context.getString(R.string.finish) dialog?.getButton(AlertDialog.BUTTON_NEGATIVE)?.visibility = View.GONE } - } private fun calculateTitle(): String { - val inputTypeId = when { setting!!.isCirclePad() -> R.string.controller_circlepad setting!!.isCStick() -> R.string.controller_c @@ -135,7 +129,7 @@ class ControllerQuickConfigDialog( setting!!.isDPad() -> R.drawable.dpad else -> { val resourceTitle = context.resources.getResourceEntryName(setting!!.nameId) - if(resourceTitle.startsWith("direction")) { + if (resourceTitle.startsWith("direction")) { R.drawable.dpad } else { context.resources.getIdentifier(resourceTitle, "drawable", context.packageName) @@ -154,26 +148,24 @@ class ControllerQuickConfigDialog( private fun onKeyEvent(event: KeyEvent): Boolean { return when (event.action) { KeyEvent.ACTION_UP -> { - if(System.currentTimeMillis()-debounceTimestamp < 500) { + if (System.currentTimeMillis()-debounceTimestamp < 500) { return true } debounceTimestamp = System.currentTimeMillis() - index++ setting?.onKeyInput(event) 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) return false - if (event.action != MotionEvent.ACTION_MOVE) return false + if ((event.source and InputDevice.SOURCE_CLASS_JOYSTICK == 0) || + event.action != MotionEvent.ACTION_MOVE) return false val input = event.device val motionRanges = input.motionRanges @@ -224,7 +216,6 @@ class ControllerQuickConfigDialog( } previousValues[i] = origValue } - // If only one axis moved, that's the winner. if (numMovedAxis == 1) { waitingForEvent = false @@ -233,5 +224,4 @@ class ControllerQuickConfigDialog( } return true } - } \ No newline at end of file From 11b99da1e65fc0d42027ddfd0d987f53aeb37b1d Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Thu, 11 Jul 2024 19:42:20 +0100 Subject: [PATCH 4/7] Reduce debounce timer + Remove magic number --- .../features/settings/ui/ControllerQuickConfigDialog.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt index ef975f89a..413b0d79e 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt @@ -148,7 +148,7 @@ class ControllerQuickConfigDialog( private fun onKeyEvent(event: KeyEvent): Boolean { return when (event.action) { KeyEvent.ACTION_UP -> { - if (System.currentTimeMillis()-debounceTimestamp < 500) { + if (System.currentTimeMillis()-debounceTimestamp < DEBOUNCE_TIMER) { return true } @@ -224,4 +224,8 @@ class ControllerQuickConfigDialog( } return true } + + companion object { + private const val DEBOUNCE_TIMER = 100 + } } \ No newline at end of file From 813cb2fe007a34d37f44bd14d1184594dde252f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20N=C3=BCsse?= Date: Thu, 11 Jul 2024 22:22:21 +0200 Subject: [PATCH 5/7] extract strings --- .../features/settings/ui/ControllerQuickConfigDialog.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt index 413b0d79e..ee3254afc 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt @@ -47,9 +47,9 @@ class ControllerQuickConfigDialog( val builder: AlertDialog.Builder = AlertDialog.Builder(context) builder .setView(quickConfigBinding.root) - .setTitle("Quick Configure") - .setPositiveButton("Next") {_,_ -> } - .setNegativeButton("Close") { dialog, which -> + .setTitle(context.getString(R.string.controller_quick_config)) + .setPositiveButton(context.getString(R.string.next)) {_,_ -> } + .setNegativeButton(context.getString(R.string.close)) { dialog, which -> dialog.dismiss() } From 371d747df729a56d7135f5f49be6981b3eaafea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20N=C3=BCsse?= Date: Fri, 12 Jul 2024 21:13:19 +0200 Subject: [PATCH 6/7] apply settings only on finish --- .../model/view/InputBindingSetting.kt | 25 +++++++++++++++++++ .../ui/ControllerQuickConfigDialog.kt | 8 +++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt index 5e604f65a..f242e77aa 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt @@ -35,6 +35,8 @@ class InputBindingSetting( .apply() } + private var key: String = "" + /** * Returns true if this key is for the 3DS Circle Pad */ @@ -231,6 +233,29 @@ class InputBindingSetting( 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. * diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt index ee3254afc..0200ad8bc 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt @@ -71,6 +71,7 @@ class ControllerQuickConfigDialog( private fun prepareUIforIndex(i: Int) { if (allButtons.size-1 < i) { + settingsList.forEach { it.applyMapping() } dialog?.dismiss() return } @@ -145,6 +146,8 @@ class ControllerQuickConfigDialog( private var setting: InputBindingSetting? = null private var debounceTimestamp = System.currentTimeMillis() + private var settingsList = arrayListOf() + private fun onKeyEvent(event: KeyEvent): Boolean { return when (event.action) { KeyEvent.ACTION_UP -> { @@ -154,7 +157,10 @@ class ControllerQuickConfigDialog( debounceTimestamp = System.currentTimeMillis() index++ - setting?.onKeyInput(event) + 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 From a438094d4466ba307159cd0e34ada6db136b6bc4 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Sat, 15 Nov 2025 19:45:37 +0300 Subject: [PATCH 7/7] complete refactor --- .../model/view/InputBindingSetting.kt | 55 +++-- .../ui/ControllerQuickConfigDialog.kt | 231 +++++++++--------- .../features/settings/ui/SettingsAdapter.kt | 3 +- .../settings/ui/SettingsFragmentPresenter.kt | 4 +- .../settings/utils/InputBindingBase.kt | 87 +++++++ .../MotionBottomSheetDialogFragment.kt | 104 ++------ .../layout/dialog_controller_quick_config.xml | 4 +- .../app/src/main/res/values/strings.xml | 2 + 8 files changed, 251 insertions(+), 239 deletions(-) create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/InputBindingBase.kt diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt index f242e77aa..b760287e6 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt @@ -35,8 +35,6 @@ class InputBindingSetting( .apply() } - private var key: String = "" - /** * Returns true if this key is for the 3DS Circle Pad */ @@ -81,6 +79,36 @@ class InputBindingSetting( 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 * triggers on the 3DS, but we support them as such on a physical gamepad. @@ -233,29 +261,6 @@ class InputBindingSetting( 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. * diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt index 0200ad8bc..cad28ea3a 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/ControllerQuickConfigDialog.kt @@ -4,40 +4,85 @@ import android.app.AlertDialog import android.content.Context import android.content.SharedPreferences import android.graphics.drawable.Drawable -import android.view.InputDevice -import android.view.KeyEvent import android.view.LayoutInflater -import android.view.MotionEvent import android.view.View import androidx.core.content.ContextCompat import org.citra.citra_emu.R import org.citra.citra_emu.databinding.DialogControllerQuickConfigBinding 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( private var context: Context, buttons: ArrayList>, titles: ArrayList>, - private var preferences: SharedPreferences + private var preferences: SharedPreferences, + adapter: SettingsAdapter ) { + private var isWaiting = false; private var index = 0 val inflater = LayoutInflater.from(context) - val quickConfigBinding = DialogControllerQuickConfigBinding.inflate(inflater) + private lateinit var quickConfigBinding: DialogControllerQuickConfigBinding var dialog: AlertDialog? = null + private var boundVerticalDpadAxis = false + private var boundHorizontalDpadAxis = false var allButtons = arrayListOf() var allTitles = arrayListOf() + 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 { - buttons.forEach {group -> - group.forEach {button -> + buttons.forEach { group -> + group.forEach { button -> allButtons.add(button) } } - titles.forEach {group -> - group.forEach {title -> + titles.forEach { group -> + group.forEach { title -> allTitles.add(title) } } @@ -45,10 +90,11 @@ class ControllerQuickConfigDialog( fun show() { val builder: AlertDialog.Builder = AlertDialog.Builder(context) + quickConfigBinding = DialogControllerQuickConfigBinding.inflate(inflater) builder .setView(quickConfigBinding.root) .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 -> dialog.dismiss() } @@ -56,36 +102,63 @@ class ControllerQuickConfigDialog( dialog = builder.create() dialog?.show() - dialog?.setOnKeyListener { _, _, event -> onKeyEvent(event) } - quickConfigBinding.root.setOnGenericMotionListener { _, event -> onMotionEvent(event) } + quickConfigBinding.root.requestFocus() + quickConfigBinding.root.setOnFocusChangeListener { v, hasFocus -> + if (!hasFocus) v.requestFocus() + } // Prepare the first element - prepareUIforIndex(index) + prepareUIforIndex() val nextButton = dialog?.getButton(AlertDialog.BUTTON_POSITIVE) nextButton?.setOnClickListener { - // Skip to next: - prepareUIforIndex(index++) + if (setting != null) setting!!.removeOldMapping() + index++ + prepareUIforIndex() } } - private fun prepareUIforIndex(i: Int) { - if (allButtons.size-1 < i) { - settingsList.forEach { it.applyMapping() } + private fun prepareUIforIndex() { + if (index >= allButtons.size) { dialog?.dismiss() 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.lastMappingDescription.visibility = View.VISIBLE } - val currentButton = allButtons[i] - val currentTitleInt = allTitles[i] - - val button = InputBindingSetting.getInputObject(currentButton, preferences) + // change the button layout for the last button + if (index == allButtons.size - 1) { + dialog?.getButton(AlertDialog.BUTTON_POSITIVE)?.text = + context.getString(R.string.finish) + dialog?.getButton(AlertDialog.BUTTON_NEGATIVE)?.visibility = View.GONE + } + // set all the icons and text var lastTitle = setting?.value ?: "" if (lastTitle.isBlank()) { lastTitle = context.getString(R.string.unassigned) @@ -93,16 +166,23 @@ class ControllerQuickConfigDialog( quickConfigBinding.lastMappingDescription.text = lastTitle quickConfigBinding.lastMappingIcon.setImageDrawable(quickConfigBinding.currentMappingIcon.drawable) - setting = InputBindingSetting(button, currentTitleInt) quickConfigBinding.currentMappingTitle.text = calculateTitle() quickConfigBinding.currentMappingDescription.text = setting?.value quickConfigBinding.currentMappingIcon.setImageDrawable(getIcon()) - if (allButtons.size-1 < index) { - dialog?.getButton(AlertDialog.BUTTON_POSITIVE)?.text = - context.getString(R.string.finish) - dialog?.getButton(AlertDialog.BUTTON_NEGATIVE)?.visibility = View.GONE + // reset all the handlers + if (setting!!.isButtonMappingSupported()) { + dialog?.setOnKeyListener { _, _, event -> inputHandler.onKeyEvent(event) } } + if (setting!!.isAxisMappingSupported()) { + quickConfigBinding.root.setOnGenericMotionListener { _, event -> + inputHandler.onMotionEvent( + event + ) + } + } + inputHandler.reset() + } private fun calculateTitle(): String { @@ -140,98 +220,7 @@ class ControllerQuickConfigDialog( return ContextCompat.getDrawable(context, id) } - private val previousValues = ArrayList() - private var prevDeviceId = 0 - private var waitingForEvent = true private var setting: InputBindingSetting? = null - private var debounceTimestamp = System.currentTimeMillis() private var settingsList = arrayListOf() - - 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 - } } \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt index b4ac3c3dc..cc53307b3 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt @@ -617,7 +617,8 @@ class SettingsAdapter( Settings.triggerTitles ) - ControllerQuickConfigDialog(context, buttons, titles, PermissionsHandler.preferences).show() + ControllerQuickConfigDialog(context, buttons, titles, PermissionsHandler.preferences, this).show() + } fun closeDialog() { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index d90fff4c9..b162c3c3f 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -764,8 +764,8 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add( RunnableSetting( R.string.controller_quick_config, - 0, - false, + R.string.controller_quick_config_description, + true, 0, { settingsAdapter.onClickControllerQuickConfig() } ) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/InputBindingBase.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/InputBindingBase.kt new file mode 100644 index 000000000..5a1cfefb5 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/InputBindingBase.kt @@ -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() + 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 + } + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt index cf42bed12..c23c05a81 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt @@ -6,10 +6,7 @@ package org.citra.citra_emu.fragments import android.content.DialogInterface 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 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.databinding.DialogInputBinding import org.citra.citra_emu.features.settings.model.view.InputBindingSetting -import org.citra.citra_emu.utils.Log -import kotlin.math.abs +import org.citra.citra_emu.features.settings.utils.InputBindingBase 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 val binding get() = _binding!! @@ -28,10 +36,6 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() { private var onCancel: (() -> Unit)? = null private var onDismiss: (() -> Unit)? = null - private val previousValues = ArrayList() - private var prevDeviceId = 0 - private var waitingForEvent = true - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (setting == null) { @@ -57,10 +61,10 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() { view.requestFocus() view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() } if (setting!!.isButtonMappingSupported()) { - dialog?.setOnKeyListener { _, _, event -> onKeyEvent(event) } + dialog?.setOnKeyListener { _, _, event -> inputHandler.onKeyEvent(event) } } if (setting!!.isAxisMappingSupported()) { - binding.root.setOnGenericMotionListener { _, event -> onMotionEvent(event) } + binding.root.setOnGenericMotionListener { _, event -> inputHandler.onMotionEvent(event) } } val inputTypeId = when { @@ -108,85 +112,7 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() { 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 { const val TAG = "MotionBottomSheetDialogFragment" diff --git a/src/android/app/src/main/res/layout/dialog_controller_quick_config.xml b/src/android/app/src/main/res/layout/dialog_controller_quick_config.xml index 9dd0160ed..f97ed7cd0 100644 --- a/src/android/app/src/main/res/layout/dialog_controller_quick_config.xml +++ b/src/android/app/src/main/res/layout/dialog_controller_quick_config.xml @@ -3,7 +3,9 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/linearLayout" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:focusable="true" + android:focusableInTouchMode="true"> Up/Down Axis Left/Right Axis Quick Configure + Quickly configure all input buttons (not including hotkeys) + Please Wait Up Down Left