[WIP] implement button-quickassignment

This commit is contained in:
Felix Nüsse 2024-07-10 11:07:29 +02:00 committed by OpenSauce
parent e0b8e8440a
commit 781652a1c0
6 changed files with 382 additions and 24 deletions

View File

@ -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 = ""
}
}
}
}

View File

@ -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<List<String>>,
titles: ArrayList<List<Int>>,
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<String>()
var allTitles = arrayListOf<Int>()
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<Float>()
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
}
}

View File

@ -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) {

View File

@ -761,44 +761,56 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
private fun addControlsSettings(sl: ArrayList<SettingsItem>) {
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<SettingsItem>) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_graphics))
sl.apply {

View File

@ -0,0 +1,73 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/lastMappingIcon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="32dp"
android:scaleType="centerInside"
android:src="@drawable/button_a"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@+id/currentMappingIcon"
app:layout_constraintStart_toStartOf="@+id/currentMappingIcon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/lastMappingDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="32dp"
android:enabled="false"
android:text="TextView"
android:textAlignment="center"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/lastMappingIcon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/lastMappingIcon"
app:layout_constraintTop_toTopOf="@+id/lastMappingIcon" />
<ImageView
android:id="@+id/currentMappingIcon"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/lastMappingIcon"
app:srcCompat="@drawable/button_b" />
<TextView
android:id="@+id/currentMappingTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="32dp"
android:text="TextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/currentMappingIcon"
app:layout_constraintTop_toTopOf="@+id/currentMappingIcon" />
<TextView
android:id="@+id/currentMappingDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="8dp"
android:enabled="false"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/currentMappingIcon"
app:layout_constraintTop_toBottomOf="@+id/currentMappingTitle" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -175,6 +175,7 @@
<string name="home_menu">HOME Menu</string>
<!-- Generic buttons (Shared with lots of stuff) -->
<string name="auto_configure">Auto Configuration</string>
<string name="generic_buttons">Buttons</string>
<string name="button">Button</string>
@ -385,6 +386,8 @@
<string name="dont_show_again">Don\'t show again</string>
<string name="visibility">Visibility</string>
<string name="information">Information</string>
<string name="finish">Finish</string>
<string name="unassigned">Unassigned</string>
<!-- Add Directory Screen-->
<string name="select_game_folder">Select Game Folder</string>