[WIP] implement button-quickassignment

This commit is contained in:
Felix Nüsse 2024-07-10 11:07:29 +02:00 committed by OpenSauce
parent 43dbe42b29
commit c9cf901d1c
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.R
import org.citra.citra_emu.features.hotkeys.Hotkey 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.AbstractSetting
import org.citra.citra_emu.features.settings.model.AbstractStringSetting
import org.citra.citra_emu.features.settings.model.Settings import org.citra.citra_emu.features.settings.model.Settings
class InputBindingSetting( class InputBindingSetting(
@ -329,5 +330,22 @@ class InputBindingSetting(
event.keyCode 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.automapping_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.automapping_dialog_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

@ -44,6 +44,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.FloatSetting
import org.citra.citra_emu.features.settings.model.ScaledFloatSetting 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.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.DateTimeSetting
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.features.settings.model.view.SettingsItem import org.citra.citra_emu.features.settings.model.view.SettingsItem
@ -65,6 +66,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.features.settings.ui.viewholder.SwitchSettingViewHolder
import org.citra.citra_emu.fragments.MessageDialogFragment import org.citra.citra_emu.fragments.MessageDialogFragment
import org.citra.citra_emu.fragments.MotionBottomSheetDialogFragment import org.citra.citra_emu.fragments.MotionBottomSheetDialogFragment
import org.citra.citra_emu.utils.PermissionsHandler.preferences
import org.citra.citra_emu.utils.SystemSaveGame import org.citra.citra_emu.utils.SystemSaveGame
import java.lang.IllegalStateException import java.lang.IllegalStateException
import java.lang.NumberFormatException import java.lang.NumberFormatException
@ -514,6 +516,32 @@ class SettingsAdapter(
.show() .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() { fun closeDialog() {
if (dialog != null) { if (dialog != null) {
if (clickedPosition != -1) { if (clickedPosition != -1) {

View File

@ -646,44 +646,56 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
private fun addControlsSettings(sl: ArrayList<SettingsItem>) { private fun addControlsSettings(sl: ArrayList<SettingsItem>) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_controls)) settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_controls))
sl.apply { 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)) add(HeaderSetting(R.string.generic_buttons))
Settings.buttonKeys.forEachIndexed { i: Int, key: String -> Settings.buttonKeys.forEachIndexed { i: Int, key: String ->
val button = getInputObject(key) val button = InputBindingSetting.getInputObject(key, preferences)
add(InputBindingSetting(button, Settings.buttonTitles[i])) add(InputBindingSetting(button, Settings.buttonTitles[i]))
} }
add(HeaderSetting(R.string.controller_circlepad)) add(HeaderSetting(R.string.controller_circlepad))
Settings.circlePadKeys.forEachIndexed { i: Int, key: String -> Settings.circlePadKeys.forEachIndexed { i: Int, key: String ->
val button = getInputObject(key) val button = InputBindingSetting.getInputObject(key, preferences)
add(InputBindingSetting(button, Settings.axisTitles[i])) add(InputBindingSetting(button, Settings.axisTitles[i]))
} }
add(HeaderSetting(R.string.controller_c)) add(HeaderSetting(R.string.controller_c))
Settings.cStickKeys.forEachIndexed { i: Int, key: String -> Settings.cStickKeys.forEachIndexed { i: Int, key: String ->
val button = getInputObject(key) val button = InputBindingSetting.getInputObject(key, preferences)
add(InputBindingSetting(button, Settings.axisTitles[i])) add(InputBindingSetting(button, Settings.axisTitles[i]))
} }
add(HeaderSetting(R.string.controller_dpad_axis,R.string.controller_dpad_axis_description)) add(HeaderSetting(R.string.controller_dpad_axis,R.string.controller_dpad_axis_description))
Settings.dPadAxisKeys.forEachIndexed { i: Int, key: String -> Settings.dPadAxisKeys.forEachIndexed { i: Int, key: String ->
val button = getInputObject(key) val button = InputBindingSetting.getInputObject(key, preferences)
add(InputBindingSetting(button, Settings.axisTitles[i])) add(InputBindingSetting(button, Settings.axisTitles[i]))
} }
add(HeaderSetting(R.string.controller_dpad_button,R.string.controller_dpad_button_description)) add(HeaderSetting(R.string.controller_dpad_button,R.string.controller_dpad_button_description))
Settings.dPadButtonKeys.forEachIndexed { i: Int, key: String -> Settings.dPadButtonKeys.forEachIndexed { i: Int, key: String ->
val button = getInputObject(key) val button = InputBindingSetting.getInputObject(key, preferences)
add(InputBindingSetting(button, Settings.dPadTitles[i])) add(InputBindingSetting(button, Settings.dPadTitles[i]))
} }
add(HeaderSetting(R.string.controller_triggers)) add(HeaderSetting(R.string.controller_triggers))
Settings.triggerKeys.forEachIndexed { i: Int, key: String -> Settings.triggerKeys.forEachIndexed { i: Int, key: String ->
val button = getInputObject(key) val button = InputBindingSetting.getInputObject(key, preferences)
add(InputBindingSetting(button, Settings.triggerTitles[i])) add(InputBindingSetting(button, Settings.triggerTitles[i]))
} }
add(HeaderSetting(R.string.controller_hotkeys)) add(HeaderSetting(R.string.controller_hotkeys))
Settings.hotKeys.forEachIndexed { i: Int, key: String -> Settings.hotKeys.forEachIndexed { i: Int, key: String ->
val button = getInputObject(key) val button = InputBindingSetting.getInputObject(key, preferences)
add(InputBindingSetting(button, Settings.hotkeyTitles[i])) add(InputBindingSetting(button, Settings.hotkeyTitles[i]))
} }
add(HeaderSetting(R.string.miscellaneous)) add(HeaderSetting(R.string.miscellaneous))
@ -699,23 +711,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>) { private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_graphics)) settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_graphics))
sl.apply { 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

@ -169,6 +169,7 @@
<string name="home_menu_warning_description">Due to how slow Android\'s storage access framework is for accessing Azahar\'s files, downloading multiple versions of system files can dramatically slow down loading for applications, save states, and the Applications list. Only download the files that you require to avoid any issues with loading speeds.</string> <string name="home_menu_warning_description">Due to how slow Android\'s storage access framework is for accessing Azahar\'s files, downloading multiple versions of system files can dramatically slow down loading for applications, save states, and the Applications list. Only download the files that you require to avoid any issues with loading speeds.</string>
<!-- Generic buttons (Shared with lots of stuff) --> <!-- Generic buttons (Shared with lots of stuff) -->
<string name="auto_configure">Auto Configuration</string>
<string name="generic_buttons">Buttons</string> <string name="generic_buttons">Buttons</string>
<string name="button">Button</string> <string name="button">Button</string>
@ -765,6 +766,8 @@
<string name="artic_base_enter_address">Enter Artic Base server address</string> <string name="artic_base_enter_address">Enter Artic Base server address</string>
<string name="delay_render_thread">Delay game render thread</string> <string name="delay_render_thread">Delay game render thread</string>
<string name="delay_render_thread_description">Delay the game render thread when it submits data to the GPU. Helps with performance issues in the (very few) applications with dynamic framerates.</string> <string name="delay_render_thread_description">Delay the game render thread when it submits data to the GPU. Helps with performance issues in the (very few) applications with dynamic framerates.</string>
<string name="automapping_dialog_finish">Finish</string>
<string name="automapping_unassigned">Unassigned</string>
<!-- Quickload&Save--> <!-- Quickload&Save-->
<string name="emulation_quicksave_slot">Quicksave</string> <string name="emulation_quicksave_slot">Quicksave</string>