mirror of
https://github.com/Lime3DS/Lime3DS.git
synced 2026-04-07 09:01:29 -06:00
refactor android input settings completely to enable per-game settings and saving in config.ini, as well as more efficient storage of setting information
This commit is contained in:
parent
7f94e80e8d
commit
0c6e7c6cc5
@ -234,6 +234,37 @@ if (ANDROID)
|
||||
"android_hide_images"
|
||||
"screen_orientation"
|
||||
"performance_overlay_position"
|
||||
"button_a"
|
||||
"button_b"
|
||||
"button_x"
|
||||
"button_y"
|
||||
"button_home"
|
||||
"circlepad_up"
|
||||
"circlepad_down"
|
||||
"circlepad_left"
|
||||
"circlepad_right"
|
||||
"button_r"
|
||||
"button_l"
|
||||
"button_zr"
|
||||
"button_zl"
|
||||
"button_start"
|
||||
"button_select"
|
||||
"dpad_up"
|
||||
"dpad_down"
|
||||
"dpad_left"
|
||||
"dpad_right"
|
||||
"cstick_up"
|
||||
"cstick_down"
|
||||
"cstick_left"
|
||||
"cstick_right"
|
||||
"hotkey_cycle_layout"
|
||||
"hotkey_close"
|
||||
"hotkey_swap"
|
||||
"hotkey_pause_resume"
|
||||
"hotkey_quicksave"
|
||||
"hotkey_turbo_limit"
|
||||
"hotkey_quickload"
|
||||
"hotkey_enable"
|
||||
)
|
||||
string(REPLACE "_" "_1" KEY_JNI_ESCAPED ${KEY})
|
||||
set(SETTING_KEY_LIST "${SETTING_KEY_LIST}\n\"${KEY}\",")
|
||||
|
||||
@ -959,6 +959,7 @@ object NativeLibrary {
|
||||
const val DPAD = 780
|
||||
const val BUTTON_DEBUG = 781
|
||||
const val BUTTON_GPIO14 = 782
|
||||
// TODO: replace these with Hotkey buttons - they aren't native!
|
||||
const val BUTTON_SWAP = 800
|
||||
const val BUTTON_TURBO = 801
|
||||
}
|
||||
|
||||
@ -7,7 +7,6 @@ package org.citra.citra_emu.activities
|
||||
import android.Manifest.permission
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
@ -26,25 +25,23 @@ import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.NativeLibrary.ButtonState
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.camera.StillImageCameraHelper.OnFilePickerResult
|
||||
import org.citra.citra_emu.contracts.OpenFileResultContract
|
||||
import org.citra.citra_emu.databinding.ActivityEmulationBinding
|
||||
import org.citra.citra_emu.display.ScreenAdjustmentUtil
|
||||
import org.citra.citra_emu.display.SecondaryDisplay
|
||||
import org.citra.citra_emu.features.hotkeys.HotkeyUtility
|
||||
import org.citra.citra_emu.features.input.GamepadHelper
|
||||
import org.citra.citra_emu.features.input.HotkeyUtility
|
||||
import org.citra.citra_emu.features.settings.model.BooleanSetting
|
||||
import org.citra.citra_emu.features.settings.model.IntSetting
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
|
||||
import org.citra.citra_emu.fragments.EmulationFragment
|
||||
import org.citra.citra_emu.fragments.MessageDialogFragment
|
||||
import org.citra.citra_emu.model.Game
|
||||
import org.citra.citra_emu.utils.BuildUtil
|
||||
import org.citra.citra_emu.utils.ControllerMappingHelper
|
||||
import org.citra.citra_emu.utils.FileBrowserHelper
|
||||
import org.citra.citra_emu.utils.EmulationLifecycleUtil
|
||||
import org.citra.citra_emu.utils.EmulationMenuSettings
|
||||
@ -53,10 +50,9 @@ import org.citra.citra_emu.utils.RefreshRateUtil
|
||||
import org.citra.citra_emu.utils.ThemeUtil
|
||||
import org.citra.citra_emu.viewmodel.EmulationViewModel
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile
|
||||
import kotlin.math.abs
|
||||
|
||||
class EmulationActivity : AppCompatActivity() {
|
||||
private val preferences: SharedPreferences
|
||||
get() = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
var isActivityRecreated = false
|
||||
val emulationViewModel: EmulationViewModel by viewModels()
|
||||
private lateinit var binding: ActivityEmulationBinding
|
||||
@ -288,13 +284,13 @@ class EmulationActivity : AppCompatActivity() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return hotkeyUtility.handleKeyPress(event)
|
||||
return hotkeyUtility.handleKeyPress(if (event.keyCode == 0) event.scanCode else event.keyCode, event.device.descriptor)
|
||||
}
|
||||
KeyEvent.ACTION_UP -> {
|
||||
return hotkeyUtility.handleKeyRelease(event)
|
||||
return hotkeyUtility.handleKeyRelease(if (event.keyCode == 0) event.scanCode else event.keyCode, event.device.descriptor)
|
||||
}
|
||||
else -> {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -314,7 +310,8 @@ class EmulationActivity : AppCompatActivity() {
|
||||
// TODO: Move this check into native code - prevents crash if input pressed before starting emulation
|
||||
if (!NativeLibrary.isRunning() ||
|
||||
(event.source and InputDevice.SOURCE_CLASS_JOYSTICK == 0) ||
|
||||
emulationFragment.isDrawerOpen()) {
|
||||
emulationFragment.isDrawerOpen()
|
||||
) {
|
||||
return super.dispatchGenericMotionEvent(event)
|
||||
}
|
||||
|
||||
@ -322,206 +319,76 @@ class EmulationActivity : AppCompatActivity() {
|
||||
if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
||||
return true
|
||||
}
|
||||
val input = event.device
|
||||
val motions = input.motionRanges
|
||||
val axisValuesCirclePad = floatArrayOf(0.0f, 0.0f)
|
||||
val axisValuesCStick = floatArrayOf(0.0f, 0.0f)
|
||||
val axisValuesDPad = floatArrayOf(0.0f, 0.0f)
|
||||
var isTriggerPressedLMapped = false
|
||||
var isTriggerPressedRMapped = false
|
||||
var isTriggerPressedZLMapped = false
|
||||
var isTriggerPressedZRMapped = false
|
||||
var isTriggerPressedL = false
|
||||
var isTriggerPressedR = false
|
||||
var isTriggerPressedZL = false
|
||||
var isTriggerPressedZR = false
|
||||
for (range in motions) {
|
||||
val device = event.device
|
||||
val manager = emulationViewModel.settings.inputMappingManager
|
||||
|
||||
val stickAccumulator = HashMap<Int, Pair<Float, Float>>()
|
||||
|
||||
for (range in device.motionRanges) {
|
||||
val axis = range.axis
|
||||
val origValue = event.getAxisValue(axis)
|
||||
var value = ControllerMappingHelper.scaleAxis(input, axis, origValue)
|
||||
val nextMapping =
|
||||
preferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1)
|
||||
val guestOrientation =
|
||||
preferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1)
|
||||
val inverted = preferences.getBoolean(InputBindingSetting.getInputAxisInvertedKey(axis),false);
|
||||
if (nextMapping == -1 || guestOrientation == -1) {
|
||||
// Axis is unmapped
|
||||
var value = GamepadHelper.scaleAxis(device, axis, origValue)
|
||||
if (value > -0.1f && value < 0.1f) value = 0f
|
||||
|
||||
val axisPair = Pair(axis, if (value >= 0f) 1 else -1)
|
||||
|
||||
// special case where the axis value is 0 - we need to send releases to both directions
|
||||
if (value == 0f) {
|
||||
listOf(Pair(axis, 1), Pair(axis, -1)).forEach { zeroPair ->
|
||||
manager.getOutAxesForAxis(zeroPair).forEach { (outAxis, outDir) ->
|
||||
val component =
|
||||
GamepadHelper.getJoystickComponent(outAxis) ?: return@forEach
|
||||
val current =
|
||||
stickAccumulator.getOrDefault(component.joystickType, Pair(0f, 0f))
|
||||
stickAccumulator[component.joystickType] = if (component.isVertical) {
|
||||
Pair(current.first, 0f)
|
||||
} else {
|
||||
Pair(0f, current.second)
|
||||
}
|
||||
}
|
||||
manager.getOutButtonsForAxis(zeroPair).forEach { outButton ->
|
||||
NativeLibrary.onGamePadEvent(
|
||||
device.descriptor,
|
||||
outButton,
|
||||
ButtonState.RELEASED
|
||||
)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (value > 0f && value < 0.1f || value < 0f && value > -0.1f) {
|
||||
// Skip joystick wobble
|
||||
value = 0f
|
||||
// Axis to Axis mappings
|
||||
manager.getOutAxesForAxis(axisPair).forEach { (outAxis, outDir) ->
|
||||
val component = GamepadHelper.getJoystickComponent(outAxis) ?: return@forEach
|
||||
val current =
|
||||
stickAccumulator.getOrDefault(component.joystickType, Pair(0f, 0f))
|
||||
val contribution = abs(value) * outDir
|
||||
stickAccumulator[component.joystickType] = if (component.isVertical) {
|
||||
Pair(current.first, contribution)
|
||||
} else {
|
||||
Pair(contribution, current.second)
|
||||
}
|
||||
}
|
||||
if (inverted) value = -value;
|
||||
|
||||
when (nextMapping) {
|
||||
NativeLibrary.ButtonType.STICK_LEFT -> {
|
||||
axisValuesCirclePad[guestOrientation] = value
|
||||
}
|
||||
|
||||
NativeLibrary.ButtonType.STICK_C -> {
|
||||
axisValuesCStick[guestOrientation] = value
|
||||
}
|
||||
|
||||
NativeLibrary.ButtonType.DPAD -> {
|
||||
axisValuesDPad[guestOrientation] = value
|
||||
}
|
||||
|
||||
NativeLibrary.ButtonType.TRIGGER_L -> {
|
||||
isTriggerPressedLMapped = true
|
||||
isTriggerPressedL = value != 0f
|
||||
}
|
||||
|
||||
NativeLibrary.ButtonType.TRIGGER_R -> {
|
||||
isTriggerPressedRMapped = true
|
||||
isTriggerPressedR = value != 0f
|
||||
}
|
||||
|
||||
NativeLibrary.ButtonType.BUTTON_ZL -> {
|
||||
isTriggerPressedZLMapped = true
|
||||
isTriggerPressedZL = value != 0f
|
||||
}
|
||||
|
||||
NativeLibrary.ButtonType.BUTTON_ZR -> {
|
||||
isTriggerPressedZRMapped = true
|
||||
isTriggerPressedZR = value != 0f
|
||||
// Axis to Button mappings
|
||||
manager.getOutButtonsForAxis(axisPair).forEach { button ->
|
||||
val mapping = manager.getMappingForButton(button)
|
||||
if (abs(value) > (mapping?.threshold ?: 0.5f)) {
|
||||
hotkeyUtility.handleKeyPress(button, device.descriptor)
|
||||
NativeLibrary.onGamePadEvent(device.descriptor, button, ButtonState.PRESSED)
|
||||
} else {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
device.descriptor,
|
||||
button,
|
||||
ButtonState.RELEASED
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Circle-Pad and C-Stick status
|
||||
NativeLibrary.onGamePadMoveEvent(
|
||||
input.descriptor,
|
||||
NativeLibrary.ButtonType.STICK_LEFT,
|
||||
axisValuesCirclePad[0],
|
||||
axisValuesCirclePad[1]
|
||||
)
|
||||
NativeLibrary.onGamePadMoveEvent(
|
||||
input.descriptor,
|
||||
NativeLibrary.ButtonType.STICK_C,
|
||||
axisValuesCStick[0],
|
||||
axisValuesCStick[1]
|
||||
)
|
||||
|
||||
// Triggers L/R and ZL/ZR
|
||||
if (isTriggerPressedLMapped) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.TRIGGER_L,
|
||||
if (isTriggerPressedL) {
|
||||
NativeLibrary.ButtonState.PRESSED
|
||||
} else {
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
}
|
||||
)
|
||||
}
|
||||
if (isTriggerPressedRMapped) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.TRIGGER_R,
|
||||
if (isTriggerPressedR) {
|
||||
NativeLibrary.ButtonState.PRESSED
|
||||
} else {
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
}
|
||||
)
|
||||
}
|
||||
if (isTriggerPressedZLMapped) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.BUTTON_ZL,
|
||||
if (isTriggerPressedZL) {
|
||||
NativeLibrary.ButtonState.PRESSED
|
||||
} else {
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
}
|
||||
)
|
||||
}
|
||||
if (isTriggerPressedZRMapped) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.BUTTON_ZR,
|
||||
if (isTriggerPressedZR) {
|
||||
NativeLibrary.ButtonState.PRESSED
|
||||
} else {
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
}
|
||||
)
|
||||
stickAccumulator.forEach { (outAxis, value) ->
|
||||
NativeLibrary.onGamePadMoveEvent(device.descriptor, outAxis, value.first, value.second)
|
||||
}
|
||||
|
||||
// Work-around to allow D-pad axis to be bound to emulated buttons
|
||||
if (axisValuesDPad[0] == 0f) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_LEFT,
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
)
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_RIGHT,
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
)
|
||||
}
|
||||
if (axisValuesDPad[0] < 0f) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_LEFT,
|
||||
NativeLibrary.ButtonState.PRESSED
|
||||
)
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_RIGHT,
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
)
|
||||
}
|
||||
if (axisValuesDPad[0] > 0f) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_LEFT,
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
)
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_RIGHT,
|
||||
NativeLibrary.ButtonState.PRESSED
|
||||
)
|
||||
}
|
||||
if (axisValuesDPad[1] == 0f) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_UP,
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
)
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_DOWN,
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
)
|
||||
}
|
||||
if (axisValuesDPad[1] < 0f) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_UP,
|
||||
NativeLibrary.ButtonState.PRESSED
|
||||
)
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_DOWN,
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
)
|
||||
}
|
||||
if (axisValuesDPad[1] > 0f) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_UP,
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
)
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_DOWN,
|
||||
NativeLibrary.ButtonState.PRESSED
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,374 @@
|
||||
package org.citra.citra_emu.features.input
|
||||
|
||||
import android.view.InputDevice
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.features.settings.model.InputMappingSetting
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
|
||||
object GamepadHelper {
|
||||
private const val BUTTON_NAME_L3 = "Button L3"
|
||||
private const val BUTTON_NAME_R3 = "Button R3"
|
||||
private const val NINTENDO_VENDOR_ID = 0x057e
|
||||
|
||||
// Linux BTN_DPAD_* values (0x220-0x223). Joy-Con D-pad buttons arrive as
|
||||
// KEYCODE_UNKNOWN with these scan codes because Android's input layer doesn't
|
||||
// translate them to KEYCODE_DPAD_*. translateEventToKeyId() falls back to
|
||||
// the scan code in that case.
|
||||
private const val LINUX_BTN_DPAD_UP = 0x220 // 544
|
||||
private const val LINUX_BTN_DPAD_DOWN = 0x221 // 545
|
||||
private const val LINUX_BTN_DPAD_LEFT = 0x222 // 546
|
||||
private const val LINUX_BTN_DPAD_RIGHT = 0x223 // 547
|
||||
|
||||
|
||||
private val buttonNameOverrides = mapOf(
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBL to BUTTON_NAME_L3,
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBR to BUTTON_NAME_R3,
|
||||
LINUX_BTN_DPAD_UP to "Dpad Up",
|
||||
LINUX_BTN_DPAD_DOWN to "Dpad Down",
|
||||
LINUX_BTN_DPAD_LEFT to "Dpad Left",
|
||||
LINUX_BTN_DPAD_RIGHT to "Dpad Right"
|
||||
)
|
||||
|
||||
fun getButtonName(keyCode: Int): String =
|
||||
buttonNameOverrides[keyCode]
|
||||
?: toTitleCase(KeyEvent.keyCodeToString(keyCode).removePrefix("KEYCODE_"))
|
||||
|
||||
fun getAxisName(axis: Int, direction: Int?): String = "Axis " + axis + direction?.let { if(it > 0) "+" else "-" }
|
||||
|
||||
private fun toTitleCase(raw: String): String =
|
||||
raw.replace("_", " ").lowercase()
|
||||
.split(" ").joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } }
|
||||
|
||||
private data class DefaultButtonMapping(
|
||||
val setting: InputMappingSetting,
|
||||
val hostKeyCode: Int
|
||||
)
|
||||
// Auto-map always sets inverted = false. Users needing inverted axes should remap manually.
|
||||
private data class DefaultAxisMapping(
|
||||
val setting: InputMappingSetting,
|
||||
val hostAxis: Int,
|
||||
val hostDirection: Int
|
||||
)
|
||||
|
||||
private val xboxFaceButtonMappings = listOf(
|
||||
DefaultButtonMapping(InputMappingSetting.BUTTON_A, KeyEvent.KEYCODE_BUTTON_B),
|
||||
DefaultButtonMapping(InputMappingSetting.BUTTON_B, KeyEvent.KEYCODE_BUTTON_A),
|
||||
DefaultButtonMapping(InputMappingSetting.BUTTON_X, KeyEvent.KEYCODE_BUTTON_Y),
|
||||
DefaultButtonMapping(InputMappingSetting.BUTTON_Y, KeyEvent.KEYCODE_BUTTON_X)
|
||||
)
|
||||
|
||||
private val nintendoFaceButtonMappings = listOf(
|
||||
DefaultButtonMapping(InputMappingSetting.BUTTON_A, KeyEvent.KEYCODE_BUTTON_A),
|
||||
DefaultButtonMapping(InputMappingSetting.BUTTON_B, KeyEvent.KEYCODE_BUTTON_B),
|
||||
DefaultButtonMapping(InputMappingSetting.BUTTON_X, KeyEvent.KEYCODE_BUTTON_X),
|
||||
DefaultButtonMapping(InputMappingSetting.BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y)
|
||||
)
|
||||
|
||||
private val commonButtonMappings = listOf(
|
||||
DefaultButtonMapping(InputMappingSetting.BUTTON_L, KeyEvent.KEYCODE_BUTTON_L1),
|
||||
DefaultButtonMapping(InputMappingSetting.BUTTON_R, KeyEvent.KEYCODE_BUTTON_R1),
|
||||
DefaultButtonMapping(InputMappingSetting.BUTTON_ZL, KeyEvent.KEYCODE_BUTTON_L2),
|
||||
DefaultButtonMapping(InputMappingSetting.BUTTON_ZR, KeyEvent.KEYCODE_BUTTON_R2),
|
||||
DefaultButtonMapping(InputMappingSetting.BUTTON_SELECT, KeyEvent.KEYCODE_BUTTON_SELECT),
|
||||
DefaultButtonMapping(InputMappingSetting.BUTTON_START, KeyEvent.KEYCODE_BUTTON_START)
|
||||
)
|
||||
|
||||
private val dpadButtonMappings = listOf(
|
||||
DefaultButtonMapping(InputMappingSetting.DPAD_UP, KeyEvent.KEYCODE_DPAD_UP),
|
||||
DefaultButtonMapping(InputMappingSetting.DPAD_DOWN, KeyEvent.KEYCODE_DPAD_DOWN),
|
||||
DefaultButtonMapping(InputMappingSetting.DPAD_LEFT, KeyEvent.KEYCODE_DPAD_LEFT),
|
||||
DefaultButtonMapping(InputMappingSetting.DPAD_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT)
|
||||
)
|
||||
|
||||
private val stickAxisMappings = listOf(
|
||||
DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_LEFT, MotionEvent.AXIS_X, -1),
|
||||
DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_RIGHT, MotionEvent.AXIS_X, 1),
|
||||
DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_UP, MotionEvent.AXIS_Y, -1),
|
||||
DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_DOWN, MotionEvent.AXIS_Y, 1),
|
||||
DefaultAxisMapping(InputMappingSetting.CSTICK_LEFT, MotionEvent.AXIS_Z, -1),
|
||||
DefaultAxisMapping(InputMappingSetting.CSTICK_RIGHT, MotionEvent.AXIS_Z, 1),
|
||||
DefaultAxisMapping(InputMappingSetting.CSTICK_UP, MotionEvent.AXIS_RZ, -1),
|
||||
DefaultAxisMapping(InputMappingSetting.CSTICK_DOWN, MotionEvent.AXIS_RZ, 1)
|
||||
)
|
||||
|
||||
private val dpadAxisMappings = listOf(
|
||||
DefaultAxisMapping(InputMappingSetting.DPAD_UP, MotionEvent.AXIS_HAT_Y, -1),
|
||||
DefaultAxisMapping(InputMappingSetting.DPAD_DOWN, MotionEvent.AXIS_HAT_Y, 1),
|
||||
DefaultAxisMapping(InputMappingSetting.DPAD_LEFT, MotionEvent.AXIS_HAT_X, -1),
|
||||
DefaultAxisMapping(InputMappingSetting.DPAD_RIGHT, MotionEvent.AXIS_HAT_X, 1)
|
||||
)
|
||||
|
||||
// Nintendo Switch Joy-Con specific mappings.
|
||||
// Joy-Cons connected via Bluetooth on Android have several quirks:
|
||||
// - They register as two separate InputDevices (left and right)
|
||||
// - Android's evdev translation swaps A<->B (BTN_EAST->BUTTON_B, BTN_SOUTH->BUTTON_A)
|
||||
// but does NOT swap X<->Y (BTN_NORTH->BUTTON_X, BTN_WEST->BUTTON_Y)
|
||||
// - D-pad buttons arrive as KEYCODE_UNKNOWN (0) with Linux BTN_DPAD_* scan codes
|
||||
// - Right stick uses AXIS_RX/AXIS_RY instead of AXIS_Z/AXIS_RZ
|
||||
|
||||
// Joy-Con face buttons: A/B are swapped by Android's evdev layer, but X/Y are not.
|
||||
// This is different from both the standard Xbox table (full swap) and the
|
||||
// Nintendo table (no swap).
|
||||
private val joyconFaceButtonMappings = listOf(
|
||||
DefaultButtonMapping(InputMappingSetting.BUTTON_A, KeyEvent.KEYCODE_BUTTON_B),
|
||||
DefaultButtonMapping(InputMappingSetting.BUTTON_B, KeyEvent.KEYCODE_BUTTON_A),
|
||||
DefaultButtonMapping(InputMappingSetting.BUTTON_X, KeyEvent.KEYCODE_BUTTON_X),
|
||||
DefaultButtonMapping(InputMappingSetting.BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y)
|
||||
)
|
||||
|
||||
// Joy-Con D-pad: uses Linux scan codes because Android reports BTN_DPAD_* as KEYCODE_UNKNOWN
|
||||
private val joyconDpadButtonMappings = listOf(
|
||||
DefaultButtonMapping(InputMappingSetting.DPAD_UP, LINUX_BTN_DPAD_UP),
|
||||
DefaultButtonMapping(InputMappingSetting.DPAD_DOWN, LINUX_BTN_DPAD_DOWN),
|
||||
DefaultButtonMapping(InputMappingSetting.DPAD_LEFT, LINUX_BTN_DPAD_LEFT),
|
||||
DefaultButtonMapping(InputMappingSetting.DPAD_RIGHT, LINUX_BTN_DPAD_RIGHT)
|
||||
)
|
||||
|
||||
// Joy-Con sticks: left stick is AXIS_X/Y (standard), right stick is AXIS_RX/RY
|
||||
// (not Z/RZ like most controllers). The horizontal axis is inverted relative to
|
||||
// the standard orientation - verified empirically on paired Joy-Cons via Bluetooth.
|
||||
private val joyconStickAxisMappings = listOf(
|
||||
DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_UP, MotionEvent.AXIS_Y, -1),
|
||||
DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_DOWN, MotionEvent.AXIS_Y, 1),
|
||||
DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_LEFT, MotionEvent.AXIS_X, -1),
|
||||
DefaultAxisMapping(InputMappingSetting.CIRCLEPAD_RIGHT, MotionEvent.AXIS_X, 1),
|
||||
DefaultAxisMapping(InputMappingSetting.CSTICK_UP, MotionEvent.AXIS_RY, -1),
|
||||
DefaultAxisMapping(InputMappingSetting.CSTICK_DOWN, MotionEvent.AXIS_RY, 1),
|
||||
DefaultAxisMapping(InputMappingSetting.CSTICK_LEFT, MotionEvent.AXIS_RX, 1),
|
||||
DefaultAxisMapping(InputMappingSetting.CSTICK_RIGHT, MotionEvent.AXIS_RX, -1)
|
||||
)
|
||||
|
||||
/**
|
||||
* Detects whether a device is a Nintendo Switch Joy-Con (as opposed to a
|
||||
* Pro Controller or other Nintendo device) by checking vendor ID + device
|
||||
* capabilities. Joy-Cons lack AXIS_HAT_X/Y and use AXIS_RX/RY for the
|
||||
* right stick, while the Pro Controller has standard HAT axes and Z/RZ.
|
||||
*/
|
||||
fun isJoyCon(device: InputDevice?): Boolean {
|
||||
if (device == null) return false
|
||||
if (device.vendorId != NINTENDO_VENDOR_ID) return false
|
||||
|
||||
// Pro Controllers have HAT_X/HAT_Y (D-pad) and Z/RZ (right stick).
|
||||
// Joy-Cons lack both: no HAT axes, right stick on RX/RY instead of Z/RZ.
|
||||
var hasHatAxes = false
|
||||
var hasStandardRightStick = false
|
||||
for (range in device.motionRanges) {
|
||||
when (range.axis) {
|
||||
MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y -> hasHatAxes = true
|
||||
MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ -> hasStandardRightStick = true
|
||||
}
|
||||
}
|
||||
return !hasHatAxes && !hasStandardRightStick
|
||||
}
|
||||
|
||||
fun clearAllBindings(settings: Settings) {
|
||||
InputMappingSetting.values().forEach { settings.set(it, Input()) }
|
||||
}
|
||||
|
||||
private fun applyBindings(
|
||||
settings: Settings,
|
||||
buttonMappings: List<DefaultButtonMapping>,
|
||||
axisMappings: List<DefaultAxisMapping>
|
||||
) {
|
||||
|
||||
buttonMappings.forEach { mapping ->
|
||||
settings.set(mapping.setting, Input(key = mapping.hostKeyCode))
|
||||
}
|
||||
axisMappings.forEach { mapping ->
|
||||
settings.set(
|
||||
mapping.setting,
|
||||
Input(
|
||||
axis = mapping.hostAxis,
|
||||
direction = mapping.hostDirection
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies Joy-Con specific bindings: scan code D-pad, partial face button
|
||||
* swap, and AXIS_RX/RY right stick.
|
||||
*/
|
||||
fun applyJoyConBindings(settings: Settings) {
|
||||
applyBindings(
|
||||
settings,
|
||||
joyconFaceButtonMappings + commonButtonMappings + joyconDpadButtonMappings,
|
||||
joyconStickAxisMappings
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies auto-mapped bindings based on detected controller layout and d-pad type.
|
||||
*
|
||||
* @param isNintendoLayout true if the controller uses Nintendo face button layout
|
||||
* (A=east, B=south), false for Xbox layout (A=south, B=east)
|
||||
* @param useAxisDpad true if the d-pad should be mapped as axis (HAT_X/HAT_Y),
|
||||
* false if it should be mapped as individual button keycodes (DPAD_UP/DOWN/LEFT/RIGHT)
|
||||
*/
|
||||
fun applyAutoMapBindings(settings: Settings, isNintendoLayout: Boolean, useAxisDpad: Boolean) {
|
||||
val faceButtons = if (isNintendoLayout) nintendoFaceButtonMappings else xboxFaceButtonMappings
|
||||
val buttonMappings = if (useAxisDpad) {
|
||||
faceButtons + commonButtonMappings
|
||||
} else {
|
||||
faceButtons + commonButtonMappings + dpadButtonMappings
|
||||
}
|
||||
val axisMappings = if (useAxisDpad) {
|
||||
stickAxisMappings + dpadAxisMappings
|
||||
} else {
|
||||
stickAxisMappings
|
||||
}
|
||||
applyBindings(settings,buttonMappings, axisMappings)
|
||||
}
|
||||
|
||||
/**
|
||||
* Some controllers report extra button presses that can be ignored.
|
||||
*/
|
||||
fun shouldKeyBeIgnored(inputDevice: InputDevice, keyCode: Int): Boolean {
|
||||
return if (isDualShock4(inputDevice)) {
|
||||
// The two analog triggers generate analog motion events as well as a keycode.
|
||||
// We always prefer to use the analog values, so throw away the button press
|
||||
keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2
|
||||
} else false
|
||||
}
|
||||
|
||||
/**
|
||||
* Scale an axis to be zero-centered with a proper range.
|
||||
*/
|
||||
fun scaleAxis(inputDevice: InputDevice, axis: Int, value: Float): Float {
|
||||
if (isDualShock4(inputDevice)) {
|
||||
// Android doesn't have correct mappings for this controller's triggers. It reports them
|
||||
// as RX & RY, centered at -1.0, and with a range of [-1.0, 1.0]
|
||||
// Scale them to properly zero-centered with a range of [0.0, 1.0].
|
||||
if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY) {
|
||||
return (value + 1) / 2.0f
|
||||
}
|
||||
} else if (isXboxOneWireless(inputDevice)) {
|
||||
// Same as the DualShock 4, the mappings are missing.
|
||||
if (axis == MotionEvent.AXIS_Z || axis == MotionEvent.AXIS_RZ) {
|
||||
return (value + 1) / 2.0f
|
||||
}
|
||||
if (axis == MotionEvent.AXIS_GENERIC_1) {
|
||||
// This axis is stuck at ~.5. Ignore it.
|
||||
return 0.0f
|
||||
}
|
||||
} else if (isMogaPro2Hid(inputDevice)) {
|
||||
// This controller has a broken axis that reports a constant value. Ignore it.
|
||||
if (axis == MotionEvent.AXIS_GENERIC_1) {
|
||||
return 0.0f
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
private fun isDualShock4(inputDevice: InputDevice): Boolean {
|
||||
// Sony DualShock 4 controller
|
||||
return inputDevice.vendorId == 0x54c && inputDevice.productId == 0x9cc
|
||||
}
|
||||
|
||||
private fun isXboxOneWireless(inputDevice: InputDevice): Boolean {
|
||||
// Microsoft Xbox One controller
|
||||
return inputDevice.vendorId == 0x45e && inputDevice.productId == 0x2e0
|
||||
}
|
||||
|
||||
private fun isMogaPro2Hid(inputDevice: InputDevice): Boolean {
|
||||
// Moga Pro 2 HID
|
||||
return inputDevice.vendorId == 0x20d6 && inputDevice.productId == 0x6271
|
||||
}
|
||||
|
||||
data class JoystickComponent(val joystickType: Int, val isVertical: Boolean)
|
||||
|
||||
fun getJoystickComponent(buttonType: Int): JoystickComponent? = when (buttonType) {
|
||||
NativeLibrary.ButtonType.STICK_LEFT_UP,
|
||||
NativeLibrary.ButtonType.STICK_LEFT_DOWN -> JoystickComponent(NativeLibrary.ButtonType.STICK_LEFT, true)
|
||||
NativeLibrary.ButtonType.STICK_LEFT_LEFT,
|
||||
NativeLibrary.ButtonType.STICK_LEFT_RIGHT -> JoystickComponent(NativeLibrary.ButtonType.STICK_LEFT, false)
|
||||
NativeLibrary.ButtonType.STICK_C_UP,
|
||||
NativeLibrary.ButtonType.STICK_C_DOWN -> JoystickComponent(NativeLibrary.ButtonType.STICK_C, true)
|
||||
NativeLibrary.ButtonType.STICK_C_LEFT,
|
||||
NativeLibrary.ButtonType.STICK_C_RIGHT -> JoystickComponent(NativeLibrary.ButtonType.STICK_C, false)
|
||||
else -> null
|
||||
}
|
||||
|
||||
val buttonKeys = listOf(
|
||||
InputMappingSetting.BUTTON_A,
|
||||
InputMappingSetting.BUTTON_B,
|
||||
InputMappingSetting.BUTTON_X,
|
||||
InputMappingSetting.BUTTON_Y,
|
||||
InputMappingSetting.BUTTON_SELECT,
|
||||
InputMappingSetting.BUTTON_START,
|
||||
InputMappingSetting.BUTTON_HOME
|
||||
)
|
||||
val buttonTitles = listOf(
|
||||
R.string.button_a,
|
||||
R.string.button_b,
|
||||
R.string.button_x,
|
||||
R.string.button_y,
|
||||
R.string.button_select,
|
||||
R.string.button_start,
|
||||
R.string.button_home
|
||||
)
|
||||
val circlePadKeys = listOf(
|
||||
InputMappingSetting.CIRCLEPAD_UP,
|
||||
InputMappingSetting.CIRCLEPAD_DOWN,
|
||||
InputMappingSetting.CIRCLEPAD_LEFT,
|
||||
InputMappingSetting.CIRCLEPAD_RIGHT
|
||||
)
|
||||
val cStickKeys = listOf(
|
||||
InputMappingSetting.CSTICK_UP,
|
||||
InputMappingSetting.CSTICK_DOWN,
|
||||
InputMappingSetting.CSTICK_LEFT,
|
||||
InputMappingSetting.CSTICK_RIGHT
|
||||
)
|
||||
|
||||
val dPadButtonKeys = listOf(
|
||||
InputMappingSetting.DPAD_UP,
|
||||
InputMappingSetting.DPAD_DOWN,
|
||||
InputMappingSetting.DPAD_LEFT,
|
||||
InputMappingSetting.DPAD_RIGHT
|
||||
)
|
||||
|
||||
val dPadTitles = listOf(
|
||||
R.string.direction_up,
|
||||
R.string.direction_down,
|
||||
R.string.direction_left,
|
||||
R.string.direction_right
|
||||
)
|
||||
val axisTitles = dPadTitles
|
||||
|
||||
val triggerKeys = listOf(
|
||||
InputMappingSetting.BUTTON_L,
|
||||
InputMappingSetting.BUTTON_R,
|
||||
InputMappingSetting.BUTTON_ZL,
|
||||
InputMappingSetting.BUTTON_ZR
|
||||
)
|
||||
val triggerTitles = listOf(
|
||||
R.string.button_l,
|
||||
R.string.button_r,
|
||||
R.string.button_zl,
|
||||
R.string.button_zr
|
||||
)
|
||||
val hotKeys = listOf(
|
||||
InputMappingSetting.HOTKEY_ENABLE,
|
||||
InputMappingSetting.HOTKEY_SWAP,
|
||||
InputMappingSetting.HOTKEY_CYCLE_LAYOUT,
|
||||
InputMappingSetting.HOTKEY_CLOSE_GAME,
|
||||
InputMappingSetting.HOTKEY_PAUSE_OR_RESUME,
|
||||
InputMappingSetting.HOTKEY_QUICKSAVE,
|
||||
InputMappingSetting.HOTKEY_QUICKLOAD,
|
||||
InputMappingSetting.HOTKEY_TURBO_LIMIT
|
||||
)
|
||||
val hotkeyTitles = listOf(
|
||||
R.string.controller_hotkey_enable_button,
|
||||
R.string.emulation_swap_screens,
|
||||
R.string.emulation_cycle_landscape_layouts,
|
||||
R.string.emulation_close_game,
|
||||
R.string.emulation_toggle_pause,
|
||||
R.string.emulation_quicksave,
|
||||
R.string.emulation_quickload,
|
||||
R.string.turbo_limit_hotkey
|
||||
)
|
||||
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.features.hotkeys
|
||||
package org.citra.citra_emu.features.input
|
||||
|
||||
enum class Hotkey(val button: Int) {
|
||||
SWAP_SCREEN(10001),
|
||||
@ -2,7 +2,7 @@
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.features.hotkeys
|
||||
package org.citra.citra_emu.features.input
|
||||
|
||||
import android.content.Context
|
||||
import android.view.KeyEvent
|
||||
@ -16,6 +16,7 @@ import org.citra.citra_emu.utils.TurboHelper
|
||||
import org.citra.citra_emu.display.ScreenAdjustmentUtil
|
||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
import kotlin.math.abs
|
||||
|
||||
class HotkeyUtility(
|
||||
private val screenAdjustmentUtil: ScreenAdjustmentUtil,
|
||||
@ -27,17 +28,18 @@ class HotkeyUtility(
|
||||
private var hotkeyIsEnabled = false
|
||||
var hotkeyIsPressed = false
|
||||
private val currentlyPressedButtons = mutableSetOf<Int>()
|
||||
/** Store which axis directions are currently pressed as (axis, direction) pairs. */
|
||||
private val pressedAxisDirections = HashSet<Pair<Int, Int>>() // (outAxis, outDir)
|
||||
|
||||
fun handleKeyPress(keyEvent: KeyEvent): Boolean {
|
||||
fun handleKeyPress(key: Int, descriptor: String): Boolean {
|
||||
var handled = false
|
||||
val buttonSet = InputBindingSetting.getButtonSet(keyEvent)
|
||||
val enableButton =
|
||||
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
.getString(Settings.HOTKEY_ENABLE, "")
|
||||
val buttonSet = settings.inputMappingManager.getOutButtonsForKey(key)
|
||||
val axisSet = settings.inputMappingManager.getOutAxesForKey(key)
|
||||
val enableButtonMapped = settings.inputMappingManager.getMappingForButton(Hotkey.ENABLE.button) != null
|
||||
val thisKeyIsEnableButton = buttonSet.contains(Hotkey.ENABLE.button)
|
||||
val thisKeyIsHotkey =
|
||||
!thisKeyIsEnableButton && Hotkey.entries.any { buttonSet.contains(it.button) }
|
||||
hotkeyIsEnabled = hotkeyIsEnabled || enableButton == "" || thisKeyIsEnableButton
|
||||
hotkeyIsEnabled = hotkeyIsEnabled || !enableButtonMapped || thisKeyIsEnableButton
|
||||
|
||||
// Now process all internal buttons associated with this keypress
|
||||
for (button in buttonSet) {
|
||||
@ -58,19 +60,23 @@ class HotkeyUtility(
|
||||
// the normal key event.
|
||||
if (!thisKeyIsHotkey || !hotkeyIsEnabled) {
|
||||
handled = NativeLibrary.onGamePadEvent(
|
||||
keyEvent.device.descriptor,
|
||||
descriptor,
|
||||
button,
|
||||
NativeLibrary.ButtonState.PRESSED
|
||||
) || handled
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle axes in helper functions
|
||||
updateAxisStateForKey(axisSet,true)
|
||||
handled = sendAxisState(descriptor, axisSet) || handled
|
||||
return handled
|
||||
}
|
||||
|
||||
fun handleKeyRelease(keyEvent: KeyEvent): Boolean {
|
||||
fun handleKeyRelease(key: Int, descriptor: String): Boolean {
|
||||
var handled = false
|
||||
val buttonSet = InputBindingSetting.getButtonSet(keyEvent)
|
||||
val buttonSet = settings.inputMappingManager.getOutButtonsForKey(key)
|
||||
val axisSet = settings.inputMappingManager.getOutAxesForKey(key)
|
||||
val thisKeyIsEnableButton = buttonSet.contains(Hotkey.ENABLE.button)
|
||||
val thisKeyIsHotkey =
|
||||
!thisKeyIsEnableButton && Hotkey.entries.any { buttonSet.contains(it.button) }
|
||||
@ -96,7 +102,7 @@ class HotkeyUtility(
|
||||
)
|
||||
) {
|
||||
handled = NativeLibrary.onGamePadEvent(
|
||||
keyEvent.device.descriptor,
|
||||
descriptor,
|
||||
button,
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
) || handled
|
||||
@ -104,6 +110,8 @@ class HotkeyUtility(
|
||||
}
|
||||
}
|
||||
}
|
||||
updateAxisStateForKey(axisSet,false)
|
||||
handled = sendAxisState(descriptor, axisSet) || handled
|
||||
return handled
|
||||
}
|
||||
|
||||
@ -142,4 +150,42 @@ class HotkeyUtility(
|
||||
hotkeyIsPressed = true
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateAxisStateForKey(axisSet: List<Pair<Int, Int>>, pressed: Boolean) {
|
||||
axisSet.forEach { (outAxis, outDir) ->
|
||||
if (pressed) pressedAxisDirections.add(Pair(outAxis, outDir))
|
||||
else pressedAxisDirections.remove(Pair(outAxis, outDir))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update axis state based on currently pressed buttons
|
||||
*/
|
||||
private fun sendAxisState(descriptor: String, affectedAxes: List<Pair<Int, Int>>): Boolean {
|
||||
val stickAccumulator = HashMap<Int, Pair<Float, Float>>()
|
||||
// to make sure that when both directions are released, we still get a return to 0, but only for
|
||||
// axes that have been touched by buttons
|
||||
affectedAxes.forEach { (outAxis, outDir) ->
|
||||
val component = GamepadHelper.getJoystickComponent(outAxis) ?: return@forEach
|
||||
stickAccumulator.putIfAbsent(component.joystickType, Pair(0f,0f))
|
||||
}
|
||||
|
||||
pressedAxisDirections.forEach{ (outAxis, outDir) ->
|
||||
val component = GamepadHelper.getJoystickComponent(outAxis) ?: return@forEach
|
||||
val current =
|
||||
stickAccumulator.getOrDefault(component.joystickType, Pair(0f, 0f))
|
||||
// if opposite directions of the same stick are held, this will let them cancel each other out
|
||||
stickAccumulator[component.joystickType] = if (component.isVertical) {
|
||||
Pair(current.first, current.second + outDir)
|
||||
} else {
|
||||
Pair(current.first + outDir, current.second)
|
||||
}
|
||||
}
|
||||
var handled = false
|
||||
stickAccumulator.forEach { (joystickType, value) ->
|
||||
handled = NativeLibrary.onGamePadMoveEvent(descriptor, joystickType, value.first, value.second) || handled
|
||||
}
|
||||
return handled
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package org.citra.citra_emu.features.input
|
||||
|
||||
/**
|
||||
* Stores information about a particular input
|
||||
*/
|
||||
data class Input(
|
||||
val key: Int? = null,
|
||||
val axis: Int? = null,
|
||||
// +1 or -1
|
||||
val direction: Int? = null,
|
||||
val threshold: Float? = null
|
||||
) {
|
||||
val empty: Boolean
|
||||
get() = key == null && axis == null
|
||||
|
||||
}
|
||||
@ -0,0 +1,149 @@
|
||||
package org.citra.citra_emu.features.input
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.InputMappingSetting
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
|
||||
class InputMappingManager() {
|
||||
|
||||
/**
|
||||
* input keys are represented by a single int, and can be mapped to 3ds buttons or axis directions
|
||||
* input axes are represented by an <axis, direction> pair, where direction is either 1 or -1
|
||||
* and can be mapped to output axes or an output button
|
||||
* output keys are a single int
|
||||
* output axes are also a pair
|
||||
*
|
||||
* For now, we are only allowing one output *axis* per input, but the code is designed to change
|
||||
* that if necessary. There can be more than on output *button* per input because hotkeys are treated
|
||||
* as buttons and it is possible to map an input to both a 3ds button and a hotkey.
|
||||
*/
|
||||
|
||||
private val keyToOutButtons = HashMap<Int, MutableList<Int>>()
|
||||
private val keyToOutAxes = HashMap<Int, MutableList<Pair<Int, Int>>>()
|
||||
|
||||
private val axisToOutAxes = HashMap<Pair<Int, Int>, MutableList<Pair<Int, Int>>>()
|
||||
private val axisToOutButtons = HashMap<Pair<Int, Int>, MutableList<Int>>()
|
||||
|
||||
private val outAxisToMapping = HashMap<Pair<Int, Int>, Input>()
|
||||
private val buttonToMapping = HashMap<Int, Input>()
|
||||
|
||||
|
||||
/** Rebuilds the input maps from the given settings instance */
|
||||
fun rebuild(settings: Settings) {
|
||||
clear()
|
||||
InputMappingSetting.values().forEach { setting ->
|
||||
val mapping = settings.get(setting) ?: return@forEach
|
||||
register(setting, mapping)
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
axisToOutButtons.clear()
|
||||
buttonToMapping.clear()
|
||||
axisToOutAxes.clear()
|
||||
outAxisToMapping.clear()
|
||||
keyToOutButtons.clear()
|
||||
keyToOutAxes.clear()
|
||||
}
|
||||
|
||||
/** Rebind a particular setting */
|
||||
fun rebind(setting: InputMappingSetting, newMapping: Input?) {
|
||||
clearMapping(setting)
|
||||
if (newMapping != null) register(setting, newMapping)
|
||||
}
|
||||
|
||||
/** Clear a mapping from all hashmaps */
|
||||
fun clearMapping(setting: InputMappingSetting) {
|
||||
val outPair = if (setting.outAxis != null && setting.outDirection != null) Pair(
|
||||
setting.outAxis,
|
||||
setting.outDirection
|
||||
) else null
|
||||
|
||||
val oldMapping = if (setting.outKey != null) {
|
||||
buttonToMapping.get(setting.outKey)
|
||||
} else if (setting.outAxis != null && setting.outDirection != null) {
|
||||
outAxisToMapping.get(Pair(setting.outAxis, setting.outDirection))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val oldPair = if (oldMapping?.axis != null && oldMapping?.direction != null) Pair(
|
||||
oldMapping.axis,
|
||||
oldMapping.direction
|
||||
) else null
|
||||
|
||||
// if our old mapping was a key, remove its binds
|
||||
if (oldMapping?.key != null) {
|
||||
keyToOutButtons[oldMapping.key]?.remove(setting.outKey)
|
||||
if (outPair != null) {
|
||||
keyToOutAxes[oldMapping.key]?.remove(outPair)
|
||||
}
|
||||
}
|
||||
// if our old mapping was an axis, remove its binds
|
||||
if (oldPair != null) {
|
||||
if (setting.outAxis != null && setting.outDirection != null)
|
||||
axisToOutAxes[oldPair]?.remove(outPair)
|
||||
if (setting.outKey != null)
|
||||
axisToOutButtons[oldPair]?.remove(setting.outKey)
|
||||
}
|
||||
|
||||
// remove the reverse binds
|
||||
if (setting.outKey != null) {
|
||||
buttonToMapping.remove(setting.outKey)
|
||||
}
|
||||
if (outPair != null) {
|
||||
outAxisToMapping.remove(outPair)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single item to the maps based on the value of the InputMapping and the InputMappingSetting
|
||||
*/
|
||||
private fun register(setting: InputMappingSetting, mapping: Input) {
|
||||
val outPair = if (setting.outAxis != null && setting.outDirection != null) Pair(
|
||||
setting.outAxis,
|
||||
setting.outDirection
|
||||
) else null
|
||||
|
||||
val inPair = if (mapping.axis != null && mapping.direction != null) Pair(
|
||||
mapping.axis,
|
||||
mapping.direction
|
||||
) else null
|
||||
|
||||
if (setting.outKey != null) {
|
||||
if (mapping.key != null)
|
||||
keyToOutButtons.getOrPut(mapping.key, { mutableListOf() }).add(setting.outKey)
|
||||
if (inPair != null) {
|
||||
if (setting.outKey != null)
|
||||
axisToOutButtons.getOrPut(inPair, { mutableListOf() }).add(setting.outKey)
|
||||
}
|
||||
buttonToMapping[setting.outKey] = mapping
|
||||
}
|
||||
if (outPair != null) {
|
||||
if (mapping.key != null)
|
||||
keyToOutAxes.getOrPut(mapping.key, { mutableListOf() })
|
||||
.add(outPair)
|
||||
if (inPair != null)
|
||||
axisToOutAxes.getOrPut(inPair, { mutableListOf() })
|
||||
.add(outPair)
|
||||
outAxisToMapping[outPair] = mapping
|
||||
}
|
||||
}
|
||||
|
||||
fun getOutAxesForAxis(pair: Pair<Int, Int>): List<Pair<Int, Int>> =
|
||||
axisToOutAxes[pair] ?: emptyList()
|
||||
|
||||
fun getOutButtonsForAxis(pair: Pair<Int, Int>): List<Int> =
|
||||
axisToOutButtons[pair] ?: emptyList()
|
||||
|
||||
fun getMappingForOutAxis(pair: Pair<Int, Int>): Input? =
|
||||
outAxisToMapping[pair]
|
||||
|
||||
fun getOutButtonsForKey(keyCode: Int): List<Int> =
|
||||
keyToOutButtons[keyCode] ?: emptyList()
|
||||
|
||||
fun getOutAxesForKey(keyCode: Int): List<Pair<Int, Int>> =
|
||||
keyToOutAxes[keyCode] ?: emptyList()
|
||||
|
||||
fun getMappingForButton(outKey: Int): Input? =
|
||||
buttonToMapping[outKey]
|
||||
}
|
||||
@ -138,4 +138,38 @@ object SettingKeys {
|
||||
external fun android_hide_images(): String
|
||||
external fun screen_orientation(): String
|
||||
external fun performance_overlay_position(): String
|
||||
|
||||
external fun button_a(): String
|
||||
external fun button_b(): String
|
||||
external fun button_x(): String
|
||||
external fun button_y(): String
|
||||
external fun button_home(): String
|
||||
external fun circlepad_up(): String
|
||||
external fun circlepad_down(): String
|
||||
external fun circlepad_left(): String
|
||||
external fun circlepad_right(): String
|
||||
external fun button_r(): String
|
||||
external fun button_l(): String
|
||||
external fun button_zr(): String
|
||||
external fun button_zl(): String
|
||||
external fun button_start(): String
|
||||
external fun button_select(): String
|
||||
external fun dpad_up(): String
|
||||
external fun dpad_down(): String
|
||||
external fun dpad_left(): String
|
||||
external fun dpad_right(): String
|
||||
external fun cstick_up(): String
|
||||
external fun cstick_down(): String
|
||||
external fun cstick_left(): String
|
||||
external fun cstick_right(): String
|
||||
external fun hotkey_cycle_layout(): String
|
||||
external fun hotkey_close(): String
|
||||
external fun hotkey_swap(): String
|
||||
external fun hotkey_pause_resume(): String
|
||||
external fun hotkey_quicksave(): String
|
||||
external fun hotkey_turbo_limit(): String
|
||||
external fun hotkey_quickload(): String
|
||||
external fun hotkey_enable(): String
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,221 @@
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.features.settings.model
|
||||
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.features.input.Input
|
||||
import org.citra.citra_emu.features.input.GamepadHelper
|
||||
import org.citra.citra_emu.features.input.Hotkey
|
||||
import org.citra.citra_emu.features.settings.SettingKeys
|
||||
|
||||
enum class InputMappingSetting(
|
||||
override val key: String,
|
||||
override val section: String,
|
||||
override val defaultValue: Input = Input(),
|
||||
val outKey: Int? = null,
|
||||
val outAxis: Int? = null,
|
||||
val outDirection: Int? = null
|
||||
) : AbstractSetting<Input?> {
|
||||
BUTTON_A(
|
||||
SettingKeys.button_a(),
|
||||
Settings.SECTION_CONTROLS,
|
||||
outKey = NativeLibrary.ButtonType.BUTTON_A
|
||||
),
|
||||
BUTTON_B(
|
||||
SettingKeys.button_b(),
|
||||
Settings.SECTION_CONTROLS,
|
||||
outKey = NativeLibrary.ButtonType.BUTTON_B
|
||||
),
|
||||
BUTTON_X(
|
||||
SettingKeys.button_x(),
|
||||
Settings.SECTION_CONTROLS,
|
||||
outKey = NativeLibrary.ButtonType.BUTTON_X
|
||||
),
|
||||
BUTTON_Y(
|
||||
SettingKeys.button_y(),
|
||||
Settings.SECTION_CONTROLS,
|
||||
outKey = NativeLibrary.ButtonType.BUTTON_Y
|
||||
),
|
||||
BUTTON_HOME(
|
||||
SettingKeys.button_home(),
|
||||
Settings.SECTION_CONTROLS,
|
||||
outKey = NativeLibrary.ButtonType.BUTTON_HOME
|
||||
),
|
||||
BUTTON_L(
|
||||
SettingKeys.button_l(),
|
||||
Settings.SECTION_CONTROLS,
|
||||
outKey = NativeLibrary.ButtonType.TRIGGER_L
|
||||
),
|
||||
BUTTON_R(
|
||||
SettingKeys.button_r(),
|
||||
Settings.SECTION_CONTROLS,
|
||||
outKey = NativeLibrary.ButtonType.TRIGGER_R
|
||||
),
|
||||
BUTTON_SELECT(
|
||||
SettingKeys.button_select(),
|
||||
Settings.SECTION_CONTROLS,
|
||||
outKey = NativeLibrary.ButtonType.BUTTON_SELECT
|
||||
),
|
||||
BUTTON_START(
|
||||
SettingKeys.button_start(),
|
||||
Settings.SECTION_CONTROLS,
|
||||
outKey = NativeLibrary.ButtonType.BUTTON_START
|
||||
),
|
||||
BUTTON_ZL(
|
||||
SettingKeys.button_zl(),
|
||||
Settings.SECTION_CONTROLS,
|
||||
outKey = NativeLibrary.ButtonType.BUTTON_ZL
|
||||
),
|
||||
BUTTON_ZR(
|
||||
SettingKeys.button_zr(),
|
||||
Settings.SECTION_CONTROLS,
|
||||
outKey = NativeLibrary.ButtonType.BUTTON_ZR
|
||||
),
|
||||
DPAD_UP(
|
||||
SettingKeys.dpad_up(),
|
||||
Settings.SECTION_CONTROLS,
|
||||
outKey = NativeLibrary.ButtonType.DPAD_UP
|
||||
),
|
||||
DPAD_DOWN(
|
||||
SettingKeys.dpad_down(),
|
||||
Settings.SECTION_CONTROLS,
|
||||
outKey = NativeLibrary.ButtonType.DPAD_DOWN
|
||||
),
|
||||
DPAD_LEFT(
|
||||
SettingKeys.dpad_left(),
|
||||
Settings.SECTION_CONTROLS,
|
||||
outKey = NativeLibrary.ButtonType.DPAD_LEFT
|
||||
),
|
||||
DPAD_RIGHT(
|
||||
SettingKeys.dpad_right(),
|
||||
Settings.SECTION_CONTROLS,
|
||||
outKey = NativeLibrary.ButtonType.DPAD_RIGHT
|
||||
),
|
||||
|
||||
CIRCLEPAD_UP(
|
||||
SettingKeys.circlepad_up(),
|
||||
Settings.SECTION_CONTROLS,
|
||||
outAxis = NativeLibrary.ButtonType.STICK_LEFT_UP,
|
||||
outDirection = -1
|
||||
),
|
||||
CIRCLEPAD_DOWN(
|
||||
SettingKeys.circlepad_down(), Settings.SECTION_CONTROLS,
|
||||
outAxis = NativeLibrary.ButtonType.STICK_LEFT_DOWN, outDirection = 1
|
||||
),
|
||||
CIRCLEPAD_LEFT(
|
||||
SettingKeys.circlepad_left(), Settings.SECTION_CONTROLS,
|
||||
outAxis = NativeLibrary.ButtonType.STICK_LEFT_LEFT, outDirection = -1
|
||||
),
|
||||
CIRCLEPAD_RIGHT(
|
||||
SettingKeys.circlepad_right(), Settings.SECTION_CONTROLS,
|
||||
outAxis = NativeLibrary.ButtonType.STICK_LEFT_RIGHT, outDirection = 1
|
||||
),
|
||||
CSTICK_UP(
|
||||
SettingKeys.cstick_up(),
|
||||
Settings.SECTION_CONTROLS,
|
||||
outAxis = NativeLibrary.ButtonType.STICK_C_UP,
|
||||
outDirection = -1
|
||||
),
|
||||
CSTICK_DOWN(
|
||||
SettingKeys.cstick_down(), Settings.SECTION_CONTROLS,
|
||||
outAxis = NativeLibrary.ButtonType.STICK_C_DOWN, outDirection = 1
|
||||
),
|
||||
CSTICK_LEFT(
|
||||
SettingKeys.cstick_left(), Settings.SECTION_CONTROLS,
|
||||
outAxis = NativeLibrary.ButtonType.STICK_C_LEFT, outDirection = -1
|
||||
),
|
||||
CSTICK_RIGHT(
|
||||
SettingKeys.cstick_right(), Settings.SECTION_CONTROLS,
|
||||
outAxis = NativeLibrary.ButtonType.STICK_C_RIGHT, outDirection = 1
|
||||
),
|
||||
HOTKEY_CYCLE_LAYOUT(
|
||||
SettingKeys.hotkey_cycle_layout(), Settings.SECTION_CONTROLS,
|
||||
outKey = Hotkey.CYCLE_LAYOUT.button
|
||||
),
|
||||
HOTKEY_CLOSE_GAME(
|
||||
SettingKeys.hotkey_close(), Settings.SECTION_CONTROLS,
|
||||
outKey = Hotkey.CLOSE_GAME.button
|
||||
),
|
||||
HOTKEY_SWAP(
|
||||
SettingKeys.hotkey_swap(), Settings.SECTION_CONTROLS,
|
||||
outKey = Hotkey.SWAP_SCREEN.button
|
||||
),
|
||||
HOTKEY_PAUSE_OR_RESUME(
|
||||
SettingKeys.hotkey_pause_resume(), Settings.SECTION_CONTROLS,
|
||||
outKey = Hotkey.PAUSE_OR_RESUME.button
|
||||
),
|
||||
HOTKEY_QUICKSAVE(
|
||||
SettingKeys.hotkey_quicksave(), Settings.SECTION_CONTROLS,
|
||||
outKey = Hotkey.QUICKSAVE.button
|
||||
),
|
||||
HOTKEY_TURBO_LIMIT(
|
||||
SettingKeys.hotkey_turbo_limit(), Settings.SECTION_CONTROLS,
|
||||
outKey = Hotkey.TURBO_LIMIT.button
|
||||
),
|
||||
HOTKEY_QUICKLOAD(
|
||||
SettingKeys.hotkey_quickload(), Settings.SECTION_CONTROLS,
|
||||
outKey = Hotkey.QUICKLOAD.button
|
||||
),
|
||||
HOTKEY_ENABLE(
|
||||
SettingKeys.hotkey_enable(), Settings.SECTION_CONTROLS,
|
||||
outKey = Hotkey.ENABLE.button
|
||||
);
|
||||
|
||||
|
||||
/** Parse a configuration string into an input binding */
|
||||
override fun valueFromString(string: String): Input {
|
||||
if (string.isBlank()) return defaultValue
|
||||
|
||||
val params = string.split(",")
|
||||
.mapNotNull { part ->
|
||||
val split = part.split(":", limit = 2)
|
||||
if (split.size < 2) null else split[0] to split[1]
|
||||
}.toMap()
|
||||
if (params["engine"] != "gamepad") return defaultValue
|
||||
return Input(
|
||||
key = params["code"]?.toIntOrNull(),
|
||||
axis = params["axis"]?.toIntOrNull(),
|
||||
direction = params["direction"]?.toIntOrNull(),
|
||||
threshold = params["threshold"]?.toFloatOrNull(),
|
||||
).takeIf { it.key != null || it.axis != null } ?: defaultValue
|
||||
}
|
||||
|
||||
|
||||
/** Create a configuration string from an input binding */
|
||||
override fun valueToString(binding: Input?): String {
|
||||
binding ?: return ""
|
||||
if (binding.empty) return ""
|
||||
val ret = "engine:gamepad"
|
||||
return ret + when {
|
||||
binding.key != null -> ",code:${binding.key}"
|
||||
binding.axis != null -> buildString {
|
||||
append(",axis:${binding.axis}")
|
||||
binding.threshold?.let { append(",threshold:$it") }
|
||||
binding.direction?.let { append(",direction:$it") }
|
||||
}
|
||||
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
/** What will display is different from what is saved in this case */
|
||||
fun displayValue(binding: Input?): String {
|
||||
if (binding?.key != null) {
|
||||
return GamepadHelper.getButtonName(binding.key)
|
||||
} else if (binding?.axis != null) {
|
||||
return GamepadHelper.getAxisName(binding.axis, binding.direction)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
override val isRuntimeEditable: Boolean = true
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(key: String): InputMappingSetting? = values().firstOrNull { it.key == key }
|
||||
|
||||
}
|
||||
}
|
||||
@ -4,12 +4,16 @@
|
||||
|
||||
package org.citra.citra_emu.features.settings.model
|
||||
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.features.input.Input
|
||||
import org.citra.citra_emu.features.input.InputMappingManager
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile
|
||||
|
||||
class Settings {
|
||||
private val globalValues = HashMap<String, Any>()
|
||||
private val perGameOverrides = HashMap<String, Any>()
|
||||
|
||||
val inputMappingManager = InputMappingManager()
|
||||
|
||||
var gameId: String? = null
|
||||
|
||||
fun isPerGame(): Boolean = gameId != null && gameId != ""
|
||||
@ -26,24 +30,33 @@ class Settings {
|
||||
return (globalValues[setting.key] ?: setting.defaultValue) as T
|
||||
}
|
||||
|
||||
/** Sets the global value specifically */
|
||||
fun <T> setGlobal(setting: AbstractSetting<T>, value: T) {
|
||||
globalValues[setting.key] = value as Any
|
||||
// only update the InputMapping if this global setting actually is in effect now
|
||||
if (setting is InputMappingSetting && !hasOverride(setting)) {
|
||||
inputMappingManager.rebind(setting, value as? Input)
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets the override specifically */
|
||||
fun <T> setOverride(setting: AbstractSetting<T>, value: T) {
|
||||
perGameOverrides[setting.key] = value as Any
|
||||
if (setting is InputMappingSetting) {
|
||||
inputMappingManager.rebind(setting, value as? Input)
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets the per-game or global setting based on whether this file has ANY per-game setting.
|
||||
* This should be used, for example, by the Settings Activity
|
||||
* This should be used by the Custom Settings Activity
|
||||
*/
|
||||
fun <T> set(setting: AbstractSetting<T>, value: T) {
|
||||
if (isPerGame()) setOverride(setting, value) else setGlobal(setting, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing setting honoring whether it is *currently* global or local. This will
|
||||
* be used by the Quick Menu
|
||||
* Updates an existing setting honoring whether this particular setting is *currently* global or local.
|
||||
* This should be used by the Quick Menu
|
||||
*/
|
||||
fun <T> update(setting: AbstractSetting<T>, value: T) {
|
||||
if (hasOverride(setting)) setOverride(setting, value) else setGlobal(setting, value)
|
||||
@ -61,10 +74,15 @@ class Settings {
|
||||
other.perGameOverrides.forEach{ (key, value) ->
|
||||
perGameOverrides[key] = value
|
||||
}
|
||||
|
||||
inputMappingManager.rebuild(this)
|
||||
}
|
||||
|
||||
fun <T> clearOverride(setting: AbstractSetting<T>) {
|
||||
perGameOverrides.remove(setting.key)
|
||||
if (setting is InputMappingSetting) {
|
||||
inputMappingManager.rebind(setting, getGlobal(setting))
|
||||
}
|
||||
}
|
||||
|
||||
fun hasOverride(setting: AbstractSetting<*>): Boolean {
|
||||
@ -78,10 +96,12 @@ class Settings {
|
||||
fun clearAll() {
|
||||
globalValues.clear()
|
||||
perGameOverrides.clear()
|
||||
inputMappingManager.clear()
|
||||
}
|
||||
|
||||
fun clearOverrides() {
|
||||
perGameOverrides.clear()
|
||||
inputMappingManager.rebuild(this)
|
||||
}
|
||||
|
||||
fun removePerGameSettings() {
|
||||
@ -91,7 +111,6 @@ class Settings {
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
const val SECTION_CORE = "Core"
|
||||
const val SECTION_SYSTEM = "System"
|
||||
const val SECTION_CAMERA = "Camera"
|
||||
@ -108,115 +127,6 @@ class Settings {
|
||||
const val SECTION_STORAGE = "Storage"
|
||||
const val SECTION_MISC = "Miscellaneous"
|
||||
|
||||
const val KEY_BUTTON_A = "button_a"
|
||||
const val KEY_BUTTON_B = "button_b"
|
||||
const val KEY_BUTTON_X = "button_x"
|
||||
const val KEY_BUTTON_Y = "button_y"
|
||||
const val KEY_BUTTON_SELECT = "button_select"
|
||||
const val KEY_BUTTON_START = "button_start"
|
||||
const val KEY_BUTTON_HOME = "button_home"
|
||||
const val KEY_BUTTON_UP = "button_up"
|
||||
const val KEY_BUTTON_DOWN = "button_down"
|
||||
const val KEY_BUTTON_LEFT = "button_left"
|
||||
const val KEY_BUTTON_RIGHT = "button_right"
|
||||
const val KEY_BUTTON_L = "button_l"
|
||||
const val KEY_BUTTON_R = "button_r"
|
||||
const val KEY_BUTTON_ZL = "button_zl"
|
||||
const val KEY_BUTTON_ZR = "button_zr"
|
||||
const val KEY_CIRCLEPAD_AXIS_VERTICAL = "circlepad_axis_vertical"
|
||||
const val KEY_CIRCLEPAD_AXIS_HORIZONTAL = "circlepad_axis_horizontal"
|
||||
const val KEY_CSTICK_AXIS_VERTICAL = "cstick_axis_vertical"
|
||||
const val KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal"
|
||||
const val KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical"
|
||||
const val KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal"
|
||||
const val HOTKEY_ENABLE = "hotkey_enable"
|
||||
const val HOTKEY_SCREEN_SWAP = "hotkey_screen_swap"
|
||||
const val HOTKEY_CYCLE_LAYOUT = "hotkey_toggle_layout"
|
||||
const val HOTKEY_CLOSE_GAME = "hotkey_close_game"
|
||||
const val HOTKEY_PAUSE_OR_RESUME = "hotkey_pause_or_resume_game"
|
||||
const val HOTKEY_QUICKSAVE = "hotkey_quickload"
|
||||
const val HOTKEY_QUICKlOAD = "hotkey_quickpause"
|
||||
const val HOTKEY_TURBO_LIMIT = "hotkey_turbo_limit"
|
||||
|
||||
val buttonKeys = listOf(
|
||||
KEY_BUTTON_A,
|
||||
KEY_BUTTON_B,
|
||||
KEY_BUTTON_X,
|
||||
KEY_BUTTON_Y,
|
||||
KEY_BUTTON_SELECT,
|
||||
KEY_BUTTON_START,
|
||||
KEY_BUTTON_HOME
|
||||
)
|
||||
val buttonTitles = listOf(
|
||||
R.string.button_a,
|
||||
R.string.button_b,
|
||||
R.string.button_x,
|
||||
R.string.button_y,
|
||||
R.string.button_select,
|
||||
R.string.button_start,
|
||||
R.string.button_home
|
||||
)
|
||||
val circlePadKeys = listOf(
|
||||
KEY_CIRCLEPAD_AXIS_VERTICAL,
|
||||
KEY_CIRCLEPAD_AXIS_HORIZONTAL
|
||||
)
|
||||
val cStickKeys = listOf(
|
||||
KEY_CSTICK_AXIS_VERTICAL,
|
||||
KEY_CSTICK_AXIS_HORIZONTAL
|
||||
)
|
||||
val dPadAxisKeys = listOf(
|
||||
KEY_DPAD_AXIS_VERTICAL,
|
||||
KEY_DPAD_AXIS_HORIZONTAL
|
||||
)
|
||||
val dPadButtonKeys = listOf(
|
||||
KEY_BUTTON_UP,
|
||||
KEY_BUTTON_DOWN,
|
||||
KEY_BUTTON_LEFT,
|
||||
KEY_BUTTON_RIGHT
|
||||
)
|
||||
val axisTitles = listOf(
|
||||
R.string.controller_axis_vertical,
|
||||
R.string.controller_axis_horizontal
|
||||
)
|
||||
val dPadTitles = listOf(
|
||||
R.string.direction_up,
|
||||
R.string.direction_down,
|
||||
R.string.direction_left,
|
||||
R.string.direction_right
|
||||
)
|
||||
val triggerKeys = listOf(
|
||||
KEY_BUTTON_L,
|
||||
KEY_BUTTON_R,
|
||||
KEY_BUTTON_ZL,
|
||||
KEY_BUTTON_ZR
|
||||
)
|
||||
val triggerTitles = listOf(
|
||||
R.string.button_l,
|
||||
R.string.button_r,
|
||||
R.string.button_zl,
|
||||
R.string.button_zr
|
||||
)
|
||||
val hotKeys = listOf(
|
||||
HOTKEY_ENABLE,
|
||||
HOTKEY_SCREEN_SWAP,
|
||||
HOTKEY_CYCLE_LAYOUT,
|
||||
HOTKEY_CLOSE_GAME,
|
||||
HOTKEY_PAUSE_OR_RESUME,
|
||||
HOTKEY_QUICKSAVE,
|
||||
HOTKEY_QUICKlOAD,
|
||||
HOTKEY_TURBO_LIMIT
|
||||
)
|
||||
val hotkeyTitles = listOf(
|
||||
R.string.controller_hotkey_enable_button,
|
||||
R.string.emulation_swap_screens,
|
||||
R.string.emulation_cycle_landscape_layouts,
|
||||
R.string.emulation_close_game,
|
||||
R.string.emulation_toggle_pause,
|
||||
R.string.emulation_quicksave,
|
||||
R.string.emulation_quickload,
|
||||
R.string.turbo_limit_hotkey
|
||||
)
|
||||
|
||||
// TODO: Move these in with the other setting keys in GenerateSettingKeys.cmake
|
||||
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
|
||||
const val PREF_MATERIAL_YOU = "MaterialYouTheme"
|
||||
|
||||
@ -4,593 +4,58 @@
|
||||
|
||||
package org.citra.citra_emu.features.settings.model.view
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.view.InputDevice
|
||||
import android.view.InputDevice.MotionRange
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.widget.Toast
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
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.input.Input
|
||||
import org.citra.citra_emu.features.input.GamepadHelper
|
||||
import org.citra.citra_emu.features.settings.model.InputMappingSetting
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
|
||||
class InputBindingSetting(
|
||||
val abstractSetting: AbstractSetting<*>,
|
||||
val inputSetting: InputMappingSetting,
|
||||
val settings: Settings,
|
||||
titleId: Int
|
||||
) : SettingsItem(abstractSetting, titleId, 0) {
|
||||
private val context: Context get() = CitraApplication.appContext
|
||||
private val preferences: SharedPreferences
|
||||
get() = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
var value: String
|
||||
get() = preferences.getString(abstractSetting.key, "")!!
|
||||
set(string) {
|
||||
preferences.edit()
|
||||
.putString(abstractSetting.key, string)
|
||||
.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this key is for the 3DS Circle Pad
|
||||
*/
|
||||
fun isCirclePad(): Boolean =
|
||||
when (abstractSetting.key) {
|
||||
Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL,
|
||||
Settings.KEY_CIRCLEPAD_AXIS_VERTICAL -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this key is for a horizontal axis for a 3DS analog stick or D-pad
|
||||
*/
|
||||
fun isHorizontalOrientation(): Boolean =
|
||||
when (abstractSetting.key) {
|
||||
Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL,
|
||||
Settings.KEY_CSTICK_AXIS_HORIZONTAL,
|
||||
Settings.KEY_DPAD_AXIS_HORIZONTAL -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this key is for the 3DS C-Stick
|
||||
*/
|
||||
fun isCStick(): Boolean =
|
||||
when (abstractSetting.key) {
|
||||
Settings.KEY_CSTICK_AXIS_HORIZONTAL,
|
||||
Settings.KEY_CSTICK_AXIS_VERTICAL -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this key is for the 3DS D-Pad
|
||||
*/
|
||||
fun isDPad(): Boolean =
|
||||
when (abstractSetting.key) {
|
||||
Settings.KEY_DPAD_AXIS_HORIZONTAL,
|
||||
Settings.KEY_DPAD_AXIS_VERTICAL -> 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.
|
||||
*/
|
||||
fun isTrigger(): Boolean =
|
||||
when (abstractSetting.key) {
|
||||
Settings.KEY_BUTTON_L,
|
||||
Settings.KEY_BUTTON_R,
|
||||
Settings.KEY_BUTTON_ZL,
|
||||
Settings.KEY_BUTTON_ZR -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a gamepad axis can be used to map this key.
|
||||
*/
|
||||
fun isAxisMappingSupported(): Boolean {
|
||||
return isCirclePad() || isCStick() || isDPad() || isTrigger()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a gamepad button can be used to map this key.
|
||||
*/
|
||||
fun isButtonMappingSupported(): Boolean {
|
||||
return !isAxisMappingSupported() || isTrigger()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Citra button code for the settings key.
|
||||
*/
|
||||
private val buttonCode: Int
|
||||
get() =
|
||||
when (abstractSetting.key) {
|
||||
Settings.KEY_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A
|
||||
Settings.KEY_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B
|
||||
Settings.KEY_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
|
||||
Settings.KEY_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
|
||||
Settings.KEY_BUTTON_L -> NativeLibrary.ButtonType.TRIGGER_L
|
||||
Settings.KEY_BUTTON_R -> NativeLibrary.ButtonType.TRIGGER_R
|
||||
Settings.KEY_BUTTON_ZL -> NativeLibrary.ButtonType.BUTTON_ZL
|
||||
Settings.KEY_BUTTON_ZR -> NativeLibrary.ButtonType.BUTTON_ZR
|
||||
Settings.KEY_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_SELECT
|
||||
Settings.KEY_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_START
|
||||
Settings.KEY_BUTTON_HOME -> NativeLibrary.ButtonType.BUTTON_HOME
|
||||
Settings.KEY_BUTTON_UP -> NativeLibrary.ButtonType.DPAD_UP
|
||||
Settings.KEY_BUTTON_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN
|
||||
Settings.KEY_BUTTON_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT
|
||||
Settings.KEY_BUTTON_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT
|
||||
Settings.HOTKEY_ENABLE -> Hotkey.ENABLE.button
|
||||
Settings.HOTKEY_SCREEN_SWAP -> Hotkey.SWAP_SCREEN.button
|
||||
Settings.HOTKEY_CYCLE_LAYOUT -> Hotkey.CYCLE_LAYOUT.button
|
||||
Settings.HOTKEY_CLOSE_GAME -> Hotkey.CLOSE_GAME.button
|
||||
Settings.HOTKEY_PAUSE_OR_RESUME -> Hotkey.PAUSE_OR_RESUME.button
|
||||
Settings.HOTKEY_QUICKSAVE -> Hotkey.QUICKSAVE.button
|
||||
Settings.HOTKEY_QUICKlOAD -> Hotkey.QUICKLOAD.button
|
||||
Settings.HOTKEY_TURBO_LIMIT -> Hotkey.TURBO_LIMIT.button
|
||||
else -> -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key used to lookup the reverse mapping for this key, which is used to cleanup old
|
||||
* settings on re-mapping or clearing of a setting.
|
||||
*/
|
||||
private val reverseKey: String
|
||||
) : SettingsItem(inputSetting, titleId, 0) {
|
||||
val value: String
|
||||
get() {
|
||||
var reverseKey = "${INPUT_MAPPING_PREFIX}_ReverseMapping_${abstractSetting.key}"
|
||||
if (isAxisMappingSupported() && !isTrigger()) {
|
||||
// Triggers are the only axis-supported mappings without orientation
|
||||
reverseKey += "_" + if (isHorizontalOrientation()) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
return reverseKey
|
||||
val mapping = settings.get(inputSetting) ?: return ""
|
||||
return inputSetting.displayValue(mapping)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the old mapping for this key from the settings, e.g. on user clearing the setting.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun removeOldMapping() {
|
||||
// Try remove all possible keys we wrote for this setting
|
||||
val oldKey = preferences.getString(reverseKey, "")
|
||||
if (oldKey != "") {
|
||||
//settings.set(setting as AbstractSetting<String>,"")
|
||||
preferences.edit()
|
||||
.remove(abstractSetting.key) // Used for ui text
|
||||
.remove(oldKey + "_GuestOrientation") // Used for axis orientation
|
||||
.remove(oldKey + "_GuestButton") // Used for axis button
|
||||
.remove(oldKey + "_Inverted") // used for axis inversion
|
||||
.remove(reverseKey)
|
||||
val buttonCodes = try {
|
||||
preferences.getStringSet(oldKey, mutableSetOf<String>())!!.toMutableSet()
|
||||
} catch (e: ClassCastException) {
|
||||
// if this is an int pref, either old button or an axis, so just remove it
|
||||
preferences.edit().remove(oldKey).apply()
|
||||
return;
|
||||
}
|
||||
buttonCodes.remove(buttonCode.toString());
|
||||
preferences.edit().putStringSet(oldKey,buttonCodes).apply()
|
||||
}
|
||||
/** Default value is cleared */
|
||||
settings.set(inputSetting, inputSetting.defaultValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to write a gamepad button mapping for the setting.
|
||||
*/
|
||||
private fun writeButtonMapping(keyEvent: KeyEvent) {
|
||||
val editor = preferences.edit()
|
||||
val key = getInputButtonKey(keyEvent)
|
||||
// Pull in all codes associated with this key
|
||||
// Migrate from the old int preference if need be
|
||||
val buttonCodes = InputBindingSetting.getButtonSet(keyEvent)
|
||||
buttonCodes.add(buttonCode)
|
||||
// Cleanup old mapping for this setting
|
||||
removeOldMapping()
|
||||
|
||||
editor.putStringSet(key, buttonCodes.mapTo(mutableSetOf()) {it.toString()})
|
||||
|
||||
// Write next reverse mapping for future cleanup
|
||||
editor.putString(reverseKey, key)
|
||||
|
||||
// Apply changes
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to write a gamepad axis mapping for the setting.
|
||||
*/
|
||||
private fun writeAxisMapping(axis: Int, value: Int, inverted: Boolean) {
|
||||
// Cleanup old mapping
|
||||
removeOldMapping()
|
||||
|
||||
// Write new mapping
|
||||
preferences.edit()
|
||||
.putInt(getInputAxisOrientationKey(axis), if (isHorizontalOrientation()) 0 else 1)
|
||||
.putInt(getInputAxisButtonKey(axis), value)
|
||||
.putBoolean(getInputAxisInvertedKey(axis),inverted)
|
||||
// Write next reverse mapping for future cleanup
|
||||
.putString(reverseKey, getInputAxisKey(axis))
|
||||
.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the provided key input setting as an Android preference.
|
||||
* Saves the provided key input to the setting
|
||||
*
|
||||
* @param keyEvent KeyEvent of this key press.
|
||||
*/
|
||||
fun onKeyInput(keyEvent: KeyEvent) {
|
||||
if (!isButtonMappingSupported()) {
|
||||
Toast.makeText(context, R.string.input_message_analog_only, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
val code = translateEventToKeyId(keyEvent)
|
||||
writeButtonMapping(keyEvent)
|
||||
value = "${keyEvent.device.name}: ${getButtonName(code)}"
|
||||
val mapping = Input(key = code)
|
||||
settings.set(inputSetting, mapping)
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the provided motion input setting as an Android preference.
|
||||
*
|
||||
* @param device InputDevice from which the input event originated.
|
||||
* @param motionRange MotionRange of the movement
|
||||
* @param axisDir Either '-' or '+'
|
||||
* @param axisDir Either -1 or 1
|
||||
*/
|
||||
fun onMotionInput(device: InputDevice, motionRange: MotionRange, axisDir: Char) {
|
||||
if (!isAxisMappingSupported()) {
|
||||
Toast.makeText(context, R.string.input_message_button_only, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
val button = if (isCirclePad()) {
|
||||
NativeLibrary.ButtonType.STICK_LEFT
|
||||
} else if (isCStick()) {
|
||||
NativeLibrary.ButtonType.STICK_C
|
||||
} else if (isDPad()) {
|
||||
NativeLibrary.ButtonType.DPAD
|
||||
fun onMotionInput(motionRange: MotionRange, axisDir: Int) {
|
||||
val mapping = Input(axis = motionRange.axis, direction = axisDir, threshold = 0.5f)
|
||||
settings.set(inputSetting, mapping)
|
||||
}
|
||||
|
||||
private fun translateEventToKeyId(event: KeyEvent): Int {
|
||||
return if (event.keyCode == 0) {
|
||||
event.scanCode
|
||||
} else {
|
||||
buttonCode
|
||||
event.keyCode
|
||||
}
|
||||
// use UP (-) to map vertical, but use RIGHT (+) to map horizontal
|
||||
val inverted = if (isHorizontalOrientation()) axisDir == '-' else axisDir == '+'
|
||||
writeAxisMapping(motionRange.axis, button, inverted)
|
||||
value = "Axis ${motionRange.axis}$axisDir"
|
||||
}
|
||||
|
||||
override val type = TYPE_INPUT_BINDING
|
||||
|
||||
companion object {
|
||||
private const val INPUT_MAPPING_PREFIX = "InputMapping"
|
||||
|
||||
private fun toTitleCase(raw: String): String =
|
||||
raw.replace("_", " ").lowercase()
|
||||
.split(" ").joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } }
|
||||
|
||||
private const val BUTTON_NAME_L3 = "Button L3"
|
||||
private const val BUTTON_NAME_R3 = "Button R3"
|
||||
|
||||
private val buttonNameOverrides = mapOf(
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBL to BUTTON_NAME_L3,
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBR to BUTTON_NAME_R3,
|
||||
LINUX_BTN_DPAD_UP to "Dpad Up",
|
||||
LINUX_BTN_DPAD_DOWN to "Dpad Down",
|
||||
LINUX_BTN_DPAD_LEFT to "Dpad Left",
|
||||
LINUX_BTN_DPAD_RIGHT to "Dpad Right"
|
||||
)
|
||||
|
||||
fun getButtonName(keyCode: Int): String =
|
||||
buttonNameOverrides[keyCode]
|
||||
?: toTitleCase(KeyEvent.keyCodeToString(keyCode).removePrefix("KEYCODE_"))
|
||||
|
||||
private data class DefaultButtonMapping(
|
||||
val settingKey: String,
|
||||
val hostKeyCode: Int,
|
||||
val guestButtonCode: Int
|
||||
)
|
||||
// Auto-map always sets inverted = false. Users needing inverted axes should remap manually.
|
||||
private data class DefaultAxisMapping(
|
||||
val settingKey: String,
|
||||
val hostAxis: Int,
|
||||
val guestButton: Int,
|
||||
val orientation: Int,
|
||||
val inverted: Boolean
|
||||
)
|
||||
|
||||
private val xboxFaceButtonMappings = listOf(
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_A),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_B),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_X),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_Y)
|
||||
)
|
||||
|
||||
private val nintendoFaceButtonMappings = listOf(
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_A),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_B),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_X),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_Y)
|
||||
)
|
||||
|
||||
private val commonButtonMappings = listOf(
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_L, KeyEvent.KEYCODE_BUTTON_L1, NativeLibrary.ButtonType.TRIGGER_L),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_R, KeyEvent.KEYCODE_BUTTON_R1, NativeLibrary.ButtonType.TRIGGER_R),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_ZL, KeyEvent.KEYCODE_BUTTON_L2, NativeLibrary.ButtonType.BUTTON_ZL),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_ZR, KeyEvent.KEYCODE_BUTTON_R2, NativeLibrary.ButtonType.BUTTON_ZR),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_SELECT, KeyEvent.KEYCODE_BUTTON_SELECT, NativeLibrary.ButtonType.BUTTON_SELECT),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_START, KeyEvent.KEYCODE_BUTTON_START, NativeLibrary.ButtonType.BUTTON_START)
|
||||
)
|
||||
|
||||
private val dpadButtonMappings = listOf(
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_UP, KeyEvent.KEYCODE_DPAD_UP, NativeLibrary.ButtonType.DPAD_UP),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_DOWN, KeyEvent.KEYCODE_DPAD_DOWN, NativeLibrary.ButtonType.DPAD_DOWN),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_LEFT, KeyEvent.KEYCODE_DPAD_LEFT, NativeLibrary.ButtonType.DPAD_LEFT),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT, NativeLibrary.ButtonType.DPAD_RIGHT)
|
||||
)
|
||||
|
||||
private val stickAxisMappings = listOf(
|
||||
DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_X, NativeLibrary.ButtonType.STICK_LEFT, 0, false),
|
||||
DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_VERTICAL, MotionEvent.AXIS_Y, NativeLibrary.ButtonType.STICK_LEFT, 1, false),
|
||||
DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_HORIZONTAL, MotionEvent.AXIS_Z, NativeLibrary.ButtonType.STICK_C, 0, false),
|
||||
DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_VERTICAL, MotionEvent.AXIS_RZ, NativeLibrary.ButtonType.STICK_C, 1, false)
|
||||
)
|
||||
|
||||
private val dpadAxisMappings = listOf(
|
||||
DefaultAxisMapping(Settings.KEY_DPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_HAT_X, NativeLibrary.ButtonType.DPAD, 0, false),
|
||||
DefaultAxisMapping(Settings.KEY_DPAD_AXIS_VERTICAL, MotionEvent.AXIS_HAT_Y, NativeLibrary.ButtonType.DPAD, 1, false)
|
||||
)
|
||||
|
||||
// Nintendo Switch Joy-Con specific mappings.
|
||||
// Joy-Cons connected via Bluetooth on Android have several quirks:
|
||||
// - They register as two separate InputDevices (left and right)
|
||||
// - Android's evdev translation swaps A<->B (BTN_EAST->BUTTON_B, BTN_SOUTH->BUTTON_A)
|
||||
// but does NOT swap X<->Y (BTN_NORTH->BUTTON_X, BTN_WEST->BUTTON_Y)
|
||||
// - D-pad buttons arrive as KEYCODE_UNKNOWN (0) with Linux BTN_DPAD_* scan codes
|
||||
// - Right stick uses AXIS_RX/AXIS_RY instead of AXIS_Z/AXIS_RZ
|
||||
private const val NINTENDO_VENDOR_ID = 0x057e
|
||||
|
||||
// Linux BTN_DPAD_* values (0x220-0x223). Joy-Con D-pad buttons arrive as
|
||||
// KEYCODE_UNKNOWN with these scan codes because Android's input layer doesn't
|
||||
// translate them to KEYCODE_DPAD_*. translateEventToKeyId() falls back to
|
||||
// the scan code in that case.
|
||||
private const val LINUX_BTN_DPAD_UP = 0x220 // 544
|
||||
private const val LINUX_BTN_DPAD_DOWN = 0x221 // 545
|
||||
private const val LINUX_BTN_DPAD_LEFT = 0x222 // 546
|
||||
private const val LINUX_BTN_DPAD_RIGHT = 0x223 // 547
|
||||
|
||||
// Joy-Con face buttons: A/B are swapped by Android's evdev layer, but X/Y are not.
|
||||
// This is different from both the standard Xbox table (full swap) and the
|
||||
// Nintendo table (no swap).
|
||||
private val joyconFaceButtonMappings = listOf(
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_A),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_B),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_X),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_Y)
|
||||
)
|
||||
|
||||
// Joy-Con D-pad: uses Linux scan codes because Android reports BTN_DPAD_* as KEYCODE_UNKNOWN
|
||||
private val joyconDpadButtonMappings = listOf(
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_UP, LINUX_BTN_DPAD_UP, NativeLibrary.ButtonType.DPAD_UP),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_DOWN, LINUX_BTN_DPAD_DOWN, NativeLibrary.ButtonType.DPAD_DOWN),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_LEFT, LINUX_BTN_DPAD_LEFT, NativeLibrary.ButtonType.DPAD_LEFT),
|
||||
DefaultButtonMapping(Settings.KEY_BUTTON_RIGHT, LINUX_BTN_DPAD_RIGHT, NativeLibrary.ButtonType.DPAD_RIGHT)
|
||||
)
|
||||
|
||||
// Joy-Con sticks: left stick is AXIS_X/Y (standard), right stick is AXIS_RX/RY
|
||||
// (not Z/RZ like most controllers). The horizontal axis is inverted relative to
|
||||
// the standard orientation - verified empirically on paired Joy-Cons via Bluetooth.
|
||||
private val joyconStickAxisMappings = listOf(
|
||||
DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_X, NativeLibrary.ButtonType.STICK_LEFT, 0, false),
|
||||
DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_VERTICAL, MotionEvent.AXIS_Y, NativeLibrary.ButtonType.STICK_LEFT, 1, false),
|
||||
DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_HORIZONTAL, MotionEvent.AXIS_RX, NativeLibrary.ButtonType.STICK_C, 0, true),
|
||||
DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_VERTICAL, MotionEvent.AXIS_RY, NativeLibrary.ButtonType.STICK_C, 1, false)
|
||||
)
|
||||
|
||||
/**
|
||||
* Detects whether a device is a Nintendo Switch Joy-Con (as opposed to a
|
||||
* Pro Controller or other Nintendo device) by checking vendor ID + device
|
||||
* capabilities. Joy-Cons lack AXIS_HAT_X/Y and use AXIS_RX/RY for the
|
||||
* right stick, while the Pro Controller has standard HAT axes and Z/RZ.
|
||||
*/
|
||||
fun isJoyCon(device: InputDevice?): Boolean {
|
||||
if (device == null) return false
|
||||
if (device.vendorId != NINTENDO_VENDOR_ID) return false
|
||||
|
||||
// Pro Controllers have HAT_X/HAT_Y (D-pad) and Z/RZ (right stick).
|
||||
// Joy-Cons lack both: no HAT axes, right stick on RX/RY instead of Z/RZ.
|
||||
var hasHatAxes = false
|
||||
var hasStandardRightStick = false
|
||||
for (range in device.motionRanges) {
|
||||
when (range.axis) {
|
||||
MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y -> hasHatAxes = true
|
||||
MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ -> hasStandardRightStick = true
|
||||
}
|
||||
}
|
||||
return !hasHatAxes && !hasStandardRightStick
|
||||
}
|
||||
|
||||
private val allBindingKeys: Set<String> by lazy {
|
||||
(Settings.buttonKeys + Settings.triggerKeys +
|
||||
Settings.circlePadKeys + Settings.cStickKeys + Settings.dPadAxisKeys +
|
||||
Settings.dPadButtonKeys).toSet()
|
||||
}
|
||||
|
||||
fun clearAllBindings() {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
val editor = prefs.edit()
|
||||
val allKeys = prefs.all.keys.toList()
|
||||
for (key in allKeys) {
|
||||
if (key.startsWith(INPUT_MAPPING_PREFIX) || key in allBindingKeys) {
|
||||
editor.remove(key)
|
||||
}
|
||||
}
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
private fun applyBindings(
|
||||
buttonMappings: List<DefaultButtonMapping>,
|
||||
axisMappings: List<DefaultAxisMapping>
|
||||
) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
val editor = prefs.edit()
|
||||
buttonMappings.forEach { applyDefaultButtonMapping(editor, it) }
|
||||
axisMappings.forEach { applyDefaultAxisMapping(editor, it) }
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies Joy-Con specific bindings: scan code D-pad, partial face button
|
||||
* swap, and AXIS_RX/RY right stick.
|
||||
*/
|
||||
fun applyJoyConBindings() {
|
||||
applyBindings(
|
||||
joyconFaceButtonMappings + commonButtonMappings + joyconDpadButtonMappings,
|
||||
joyconStickAxisMappings
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies auto-mapped bindings based on detected controller layout and d-pad type.
|
||||
*
|
||||
* @param isNintendoLayout true if the controller uses Nintendo face button layout
|
||||
* (A=east, B=south), false for Xbox layout (A=south, B=east)
|
||||
* @param useAxisDpad true if the d-pad should be mapped as axis (HAT_X/HAT_Y),
|
||||
* false if it should be mapped as individual button keycodes (DPAD_UP/DOWN/LEFT/RIGHT)
|
||||
*/
|
||||
fun applyAutoMapBindings(isNintendoLayout: Boolean, useAxisDpad: Boolean) {
|
||||
val faceButtons = if (isNintendoLayout) nintendoFaceButtonMappings else xboxFaceButtonMappings
|
||||
val buttonMappings = if (useAxisDpad) {
|
||||
faceButtons + commonButtonMappings
|
||||
} else {
|
||||
faceButtons + commonButtonMappings + dpadButtonMappings
|
||||
}
|
||||
val axisMappings = if (useAxisDpad) {
|
||||
stickAxisMappings + dpadAxisMappings
|
||||
} else {
|
||||
stickAxisMappings
|
||||
}
|
||||
applyBindings(buttonMappings, axisMappings)
|
||||
}
|
||||
|
||||
private fun applyDefaultButtonMapping(
|
||||
editor: SharedPreferences.Editor,
|
||||
mapping: DefaultButtonMapping
|
||||
) {
|
||||
val prefKey = getInputButtonKey(mapping.hostKeyCode)
|
||||
editor.putInt(prefKey, mapping.guestButtonCode)
|
||||
editor.putString(mapping.settingKey, getButtonName(mapping.hostKeyCode))
|
||||
editor.putString(
|
||||
"${INPUT_MAPPING_PREFIX}_ReverseMapping_${mapping.settingKey}",
|
||||
prefKey
|
||||
)
|
||||
}
|
||||
|
||||
private fun applyDefaultAxisMapping(
|
||||
editor: SharedPreferences.Editor,
|
||||
mapping: DefaultAxisMapping
|
||||
) {
|
||||
val axisKey = getInputAxisKey(mapping.hostAxis)
|
||||
editor.putInt(getInputAxisOrientationKey(mapping.hostAxis), mapping.orientation)
|
||||
editor.putInt(getInputAxisButtonKey(mapping.hostAxis), mapping.guestButton)
|
||||
editor.putBoolean(getInputAxisInvertedKey(mapping.hostAxis), mapping.inverted)
|
||||
val dir = if (mapping.orientation == 0) '+' else '-'
|
||||
editor.putString(mapping.settingKey, "Axis ${mapping.hostAxis}$dir")
|
||||
val reverseKey = "${INPUT_MAPPING_PREFIX}_ReverseMapping_${mapping.settingKey}_${mapping.orientation}"
|
||||
editor.putString(reverseKey, axisKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the settings key for the specified Citra button code.
|
||||
*/
|
||||
private fun getButtonKey(buttonCode: Int): String =
|
||||
when (buttonCode) {
|
||||
NativeLibrary.ButtonType.BUTTON_A -> Settings.KEY_BUTTON_A
|
||||
NativeLibrary.ButtonType.BUTTON_B -> Settings.KEY_BUTTON_B
|
||||
NativeLibrary.ButtonType.BUTTON_X -> Settings.KEY_BUTTON_X
|
||||
NativeLibrary.ButtonType.BUTTON_Y -> Settings.KEY_BUTTON_Y
|
||||
NativeLibrary.ButtonType.TRIGGER_L -> Settings.KEY_BUTTON_L
|
||||
NativeLibrary.ButtonType.TRIGGER_R -> Settings.KEY_BUTTON_R
|
||||
NativeLibrary.ButtonType.BUTTON_ZL -> Settings.KEY_BUTTON_ZL
|
||||
NativeLibrary.ButtonType.BUTTON_ZR -> Settings.KEY_BUTTON_ZR
|
||||
NativeLibrary.ButtonType.BUTTON_SELECT -> Settings.KEY_BUTTON_SELECT
|
||||
NativeLibrary.ButtonType.BUTTON_START -> Settings.KEY_BUTTON_START
|
||||
NativeLibrary.ButtonType.BUTTON_HOME -> Settings.KEY_BUTTON_HOME
|
||||
NativeLibrary.ButtonType.DPAD_UP -> Settings.KEY_BUTTON_UP
|
||||
NativeLibrary.ButtonType.DPAD_DOWN -> Settings.KEY_BUTTON_DOWN
|
||||
NativeLibrary.ButtonType.DPAD_LEFT -> Settings.KEY_BUTTON_LEFT
|
||||
NativeLibrary.ButtonType.DPAD_RIGHT -> Settings.KEY_BUTTON_RIGHT
|
||||
else -> ""
|
||||
}
|
||||
/**
|
||||
* Get the mutable set of int button values this key should map to given an event
|
||||
*/
|
||||
fun getButtonSet(keyCode: KeyEvent):MutableSet<Int> {
|
||||
val key = getInputButtonKey(keyCode)
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
var buttonCodes = try {
|
||||
preferences.getStringSet(key, mutableSetOf<String>())
|
||||
} catch (e: ClassCastException) {
|
||||
val prefInt = preferences.getInt(key, -1);
|
||||
val migratedSet = if (prefInt != -1) {
|
||||
mutableSetOf(prefInt.toString())
|
||||
} else {
|
||||
mutableSetOf<String>()
|
||||
}
|
||||
migratedSet
|
||||
}
|
||||
if (buttonCodes == null) buttonCodes = mutableSetOf<String>()
|
||||
return buttonCodes.mapNotNull { it.toIntOrNull() }.toMutableSet()
|
||||
}
|
||||
|
||||
private fun getInputButtonKey(keyId: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${keyId}"
|
||||
|
||||
/** Falls back to the scan code when keyCode is KEYCODE_UNKNOWN. */
|
||||
fun getInputButtonKey(event: KeyEvent): String = getInputButtonKey(translateEventToKeyId(event))
|
||||
|
||||
/**
|
||||
* Helper function to get the settings key for an gamepad axis.
|
||||
*/
|
||||
fun getInputAxisKey(axis: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${axis}"
|
||||
|
||||
/**
|
||||
* Helper function to get the settings key for an gamepad axis button (stick or trigger).
|
||||
*/
|
||||
fun getInputAxisButtonKey(axis: Int): String = "${getInputAxisKey(axis)}_GuestButton"
|
||||
|
||||
/**
|
||||
* Helper function to get the settings key for an whether a gamepad axis is inverted.
|
||||
*/
|
||||
fun getInputAxisInvertedKey(axis: Int): String = "${getInputAxisKey(axis)}_Inverted"
|
||||
|
||||
/**
|
||||
* Helper function to get the settings key for an gamepad axis orientation.
|
||||
*/
|
||||
fun getInputAxisOrientationKey(axis: Int): String =
|
||||
"${getInputAxisKey(axis)}_GuestOrientation"
|
||||
|
||||
|
||||
/**
|
||||
* This function translates a keyEvent into an "keyid"
|
||||
* This key id is either the keyCode from the event, or
|
||||
* the raw scanCode.
|
||||
* Only when the keyCode itself is 0, (so it is an unknown key)
|
||||
* we fall back to the raw scan code.
|
||||
* This handles keys like the media-keys on google statia-controllers
|
||||
* that don't have a conventional "mapping" and report as "unknown"
|
||||
*/
|
||||
fun translateEventToKeyId(event: KeyEvent): Int {
|
||||
return if (event.keyCode == 0) {
|
||||
event.scanCode
|
||||
} else {
|
||||
event.keyCode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +19,6 @@ import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
@ -199,13 +198,6 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
|
||||
// Prevents saving to a non-existent settings file
|
||||
presenter.onSettingsReset()
|
||||
|
||||
val controllerKeys = Settings.buttonKeys + Settings.circlePadKeys + Settings.cStickKeys +
|
||||
Settings.dPadAxisKeys + Settings.dPadButtonKeys + Settings.triggerKeys
|
||||
val editor =
|
||||
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext).edit()
|
||||
controllerKeys.forEach { editor.remove(it) }
|
||||
editor.apply()
|
||||
|
||||
// Delete settings file because the user may have changed values that do not exist in the UI
|
||||
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
|
||||
if (!settingsFile.delete()) {
|
||||
|
||||
@ -33,7 +33,7 @@ class SettingsActivityPresenter(private val activityView: SettingsActivityView,
|
||||
this.menuTag = menuTag
|
||||
this.gameId = gameId
|
||||
|
||||
perGameInGlobalContext = gameId != "" && !Settings.settings.isPerGame()
|
||||
perGameInGlobalContext = gameId != "" && ! Settings.settings.isPerGame()
|
||||
|
||||
// sync the active settings into my local settings appropriately
|
||||
// if we are editing global settings rom a game, this should just work
|
||||
|
||||
@ -248,7 +248,7 @@ class SettingsAdapter(
|
||||
private fun onMultiChoiceClick(item: MultiChoiceSetting) {
|
||||
clickedItem = item
|
||||
|
||||
val value: BooleanArray = getSelectionForMultiChoiceValue(item);
|
||||
val value: BooleanArray = getSelectionForMultiChoiceValue(item)
|
||||
dialog = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(item.nameId)
|
||||
.setMultiChoiceItems(item.choicesId, value, this)
|
||||
@ -268,7 +268,7 @@ class SettingsAdapter(
|
||||
|
||||
private fun onStringSingleChoiceClick(item: StringSingleChoiceSetting) {
|
||||
clickedItem = item
|
||||
dialog = context?.let {
|
||||
dialog = context.let {
|
||||
MaterialAlertDialogBuilder(it)
|
||||
.setTitle(item.nameId)
|
||||
.setSingleChoiceItems(item.choices, item.selectValueIndex, this)
|
||||
@ -370,9 +370,9 @@ class SettingsAdapter(
|
||||
value = sliderProgress
|
||||
textSliderValue?.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
var textValue = s.toString().toFloatOrNull();
|
||||
var textValue = s.toString().toFloatOrNull()
|
||||
if (item.setting !is FloatSetting) {
|
||||
textValue = textValue?.roundToInt()?.toFloat();
|
||||
textValue = textValue?.roundToInt()?.toFloat()
|
||||
}
|
||||
if (textValue == null || textValue < valueFrom || textValue > valueTo) {
|
||||
textInputLayout?.error = "Inappropriate value"
|
||||
@ -469,7 +469,7 @@ class SettingsAdapter(
|
||||
val scSetting = clickedItem as? SingleChoiceSetting
|
||||
scSetting?.let {
|
||||
val value = getValueForSingleChoiceSelection(it, which)
|
||||
if (it.selectedValue != value) fragmentView?.onSettingChanged()
|
||||
if (it.selectedValue != value) fragmentView.onSettingChanged()
|
||||
it.setSelectedValue(value)
|
||||
fragmentView.loadSettingsList()
|
||||
closeDialog()
|
||||
@ -480,7 +480,7 @@ class SettingsAdapter(
|
||||
val scSetting = clickedItem as? StringSingleChoiceSetting
|
||||
scSetting?.let {
|
||||
val value = it.getValueAt(which) ?: ""
|
||||
if (it.selectedValue != value) fragmentView?.onSettingChanged()
|
||||
if (it.selectedValue != value) fragmentView.onSettingChanged()
|
||||
it.setSelectedValue(value)
|
||||
fragmentView.loadSettingsList()
|
||||
closeDialog()
|
||||
@ -492,7 +492,6 @@ class SettingsAdapter(
|
||||
sliderSetting?.let {
|
||||
val sliderval = it.roundedFloat(sliderProgress)
|
||||
if (sliderval != it.selectedFloat) fragmentView.onSettingChanged()
|
||||
val s = it.setting
|
||||
when {
|
||||
it.setting?.defaultValue is Int -> it.setSelectedValue(sliderProgress.roundToInt())
|
||||
else -> it.setSelectedValue(sliderProgress)
|
||||
@ -506,7 +505,7 @@ class SettingsAdapter(
|
||||
val inputSetting = clickedItem as? StringInputSetting
|
||||
inputSetting?.let {
|
||||
if (it.selectedValue != textInputValue) {
|
||||
fragmentView?.onSettingChanged()
|
||||
fragmentView.onSettingChanged()
|
||||
}
|
||||
it.setSelectedValue(textInputValue)
|
||||
fragmentView.loadSettingsList()
|
||||
@ -602,8 +601,9 @@ class SettingsAdapter(
|
||||
}
|
||||
|
||||
fun onLongClickAutoMap(): Boolean {
|
||||
val settings = fragmentView.activityView?.settings ?: return false
|
||||
showConfirmationDialog(R.string.controller_clear_all, R.string.controller_clear_all_confirm) {
|
||||
InputBindingSetting.clearAllBindings()
|
||||
settings.inputMappingManager.clear()
|
||||
fragmentView.loadSettingsList()
|
||||
fragmentView.onSettingChanged()
|
||||
}
|
||||
@ -682,18 +682,18 @@ class SettingsAdapter(
|
||||
}
|
||||
|
||||
private fun getSelectionForMultiChoiceValue(item: MultiChoiceSetting): BooleanArray {
|
||||
val value = item.selectedValues;
|
||||
val valuesId = item.valuesId;
|
||||
val value = item.selectedValues
|
||||
val valuesId = item.valuesId
|
||||
if (valuesId > 0) {
|
||||
val valuesArray = context.resources.getIntArray(valuesId);
|
||||
val valuesArray = context.resources.getIntArray(valuesId)
|
||||
val res = BooleanArray(valuesArray.size){false}
|
||||
for (index in valuesArray.indices) {
|
||||
if (value.contains(valuesArray[index])) {
|
||||
res[index] = true;
|
||||
res[index] = true
|
||||
}
|
||||
}
|
||||
return res;
|
||||
return res
|
||||
}
|
||||
return BooleanArray(1){false};
|
||||
return BooleanArray(1){false}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +19,6 @@ import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.display.ScreenLayout
|
||||
import org.citra.citra_emu.display.StereoMode
|
||||
import org.citra.citra_emu.display.StereoWhichDisplay
|
||||
import org.citra.citra_emu.features.settings.model.AbstractSetting
|
||||
import org.citra.citra_emu.features.settings.model.BooleanSetting
|
||||
import org.citra.citra_emu.features.settings.model.FloatSetting
|
||||
import org.citra.citra_emu.features.settings.model.IntSetting
|
||||
@ -45,6 +44,8 @@ import org.citra.citra_emu.utils.Log
|
||||
import org.citra.citra_emu.utils.SystemSaveGame
|
||||
import org.citra.citra_emu.utils.ThemeUtil
|
||||
import kotlin.math.roundToInt
|
||||
import org.citra.citra_emu.features.input.GamepadHelper
|
||||
import org.citra.citra_emu.features.settings.model.InputMappingSetting
|
||||
|
||||
class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) {
|
||||
private var menuTag: String? = null
|
||||
@ -754,45 +755,36 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
||||
)
|
||||
)
|
||||
add(HeaderSetting(R.string.generic_buttons))
|
||||
Settings.buttonKeys.forEachIndexed { i: Int, key: String ->
|
||||
val button = getInputObject(key)
|
||||
add(InputBindingSetting(button, Settings.buttonTitles[i]))
|
||||
GamepadHelper.buttonKeys.forEachIndexed { i: Int, setting: InputMappingSetting ->
|
||||
add(InputBindingSetting(setting, settings, GamepadHelper.buttonTitles[i]))
|
||||
}
|
||||
|
||||
add(HeaderSetting(R.string.controller_circlepad))
|
||||
Settings.circlePadKeys.forEachIndexed { i: Int, key: String ->
|
||||
val button = getInputObject(key)
|
||||
add(InputBindingSetting(button, Settings.axisTitles[i]))
|
||||
GamepadHelper.circlePadKeys.forEachIndexed { i: Int, setting: InputMappingSetting ->
|
||||
add(InputBindingSetting(setting, settings,GamepadHelper.axisTitles[i]))
|
||||
}
|
||||
|
||||
add(HeaderSetting(R.string.controller_c))
|
||||
Settings.cStickKeys.forEachIndexed { i: Int, key: String ->
|
||||
val button = getInputObject(key)
|
||||
add(InputBindingSetting(button, Settings.axisTitles[i]))
|
||||
GamepadHelper.cStickKeys.forEachIndexed { i: Int, setting: InputMappingSetting ->
|
||||
add(InputBindingSetting(setting, settings, GamepadHelper.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)
|
||||
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)
|
||||
add(InputBindingSetting(button, Settings.dPadTitles[i]))
|
||||
add(HeaderSetting(R.string.controller_dpad,R.string.controller_dpad_axis_description))
|
||||
|
||||
GamepadHelper.dPadButtonKeys.forEachIndexed { i: Int, setting: InputMappingSetting ->
|
||||
add(InputBindingSetting(setting, settings, GamepadHelper.axisTitles[i]))
|
||||
}
|
||||
|
||||
add(HeaderSetting(R.string.controller_triggers))
|
||||
Settings.triggerKeys.forEachIndexed { i: Int, key: String ->
|
||||
val button = getInputObject(key)
|
||||
add(InputBindingSetting(button, Settings.triggerTitles[i]))
|
||||
GamepadHelper.triggerKeys.forEachIndexed { i: Int, setting: InputMappingSetting ->
|
||||
add(InputBindingSetting(setting, settings, GamepadHelper.triggerTitles[i]))
|
||||
}
|
||||
|
||||
add(HeaderSetting(R.string.controller_hotkeys,R.string.controller_hotkeys_description))
|
||||
Settings.hotKeys.forEachIndexed { i: Int, key: String ->
|
||||
val button = getInputObject(key)
|
||||
add(InputBindingSetting(button, Settings.hotkeyTitles[i]))
|
||||
GamepadHelper.hotKeys.forEachIndexed { i: Int, setting: InputMappingSetting ->
|
||||
add(InputBindingSetting(setting, settings, GamepadHelper.hotkeyTitles[i]))
|
||||
}
|
||||
|
||||
add(HeaderSetting(R.string.miscellaneous))
|
||||
add(
|
||||
SwitchSetting(
|
||||
@ -807,19 +799,6 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInputObject(key: String): AbstractSetting<String> {
|
||||
return object : AbstractSetting<String> {
|
||||
override val key = key
|
||||
override val section = Settings.SECTION_CONTROLS
|
||||
override val isRuntimeEditable = true
|
||||
override val defaultValue = ""
|
||||
override fun valueFromString(string: String): String = string
|
||||
override fun valueToString(value: String): String = value
|
||||
// TODO: make input mappings also work per-game, which will be easy if we move
|
||||
// them to config files
|
||||
}
|
||||
}
|
||||
|
||||
private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) {
|
||||
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_graphics))
|
||||
sl.apply {
|
||||
|
||||
@ -15,12 +15,10 @@ import org.citra.citra_emu.features.settings.ui.SettingsAdapter
|
||||
class InputBindingSettingViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
|
||||
SettingViewHolder<InputBindingSetting>(binding.root, adapter) {
|
||||
override lateinit var setting: InputBindingSetting
|
||||
|
||||
override fun bind(item: SettingsItem) {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
setting = item as InputBindingSetting
|
||||
binding.textSettingName.setText(item.nameId)
|
||||
val uiString = preferences.getString(setting.abstractSetting.key, "")!!
|
||||
val uiString = setting.value
|
||||
if (uiString.isNotEmpty()) {
|
||||
binding.textSettingDescription.visibility = View.GONE
|
||||
binding.textSettingValue.visibility = View.VISIBLE
|
||||
@ -39,6 +37,8 @@ class InputBindingSettingViewHolder(val binding: ListItemSettingBinding, adapter
|
||||
binding.textSettingDescription.alpha = 0.5f
|
||||
binding.textSettingValue.alpha = 0.5f
|
||||
}
|
||||
|
||||
showGlobalButtonIfNeeded(binding.buttonUseGlobal, position)
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
|
||||
@ -12,6 +12,7 @@ import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.features.settings.model.AbstractSetting
|
||||
import org.citra.citra_emu.features.settings.model.BooleanSetting
|
||||
import org.citra.citra_emu.features.settings.model.FloatSetting
|
||||
import org.citra.citra_emu.features.settings.model.InputMappingSetting
|
||||
import org.citra.citra_emu.features.settings.model.IntListSetting
|
||||
import org.citra.citra_emu.features.settings.model.IntSetting
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
@ -37,7 +38,8 @@ object SettingsFile {
|
||||
IntSetting.values().toList() +
|
||||
FloatSetting.values().toList() +
|
||||
StringSetting.values().toList() +
|
||||
IntListSetting.values().toList()
|
||||
IntListSetting.values().toList() +
|
||||
InputMappingSetting.values().toList()
|
||||
}
|
||||
|
||||
private fun findSettingByKey(key: String): AbstractSetting<*>? =
|
||||
@ -96,6 +98,7 @@ object SettingsFile {
|
||||
*/
|
||||
fun loadSettings(settings: Settings, view: SettingsActivityView? = null) {
|
||||
readFile(getSettingsFile(FILE_NAME_CONFIG),settings,false,view)
|
||||
settings.inputMappingManager.rebuild(settings)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -106,6 +109,7 @@ object SettingsFile {
|
||||
loadSettings(settings, view)
|
||||
val file = findCustomGameSettingsFile(gameId) ?: return
|
||||
readFile(file, settings, true, view)
|
||||
settings.inputMappingManager.rebuild(settings)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -11,12 +11,14 @@ import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.databinding.DialogAutoMapBinding
|
||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
|
||||
import org.citra.citra_emu.utils.Log
|
||||
import org.citra.citra_emu.features.input.GamepadHelper
|
||||
import org.citra.citra_emu.features.settings.model.SettingsViewModel
|
||||
|
||||
/**
|
||||
* Captures a single button press to detect controller layout (Xbox vs Nintendo)
|
||||
@ -25,7 +27,7 @@ import org.citra.citra_emu.utils.Log
|
||||
class AutoMapDialogFragment : BottomSheetDialogFragment() {
|
||||
private var _binding: DialogAutoMapBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val settingsViewModel: SettingsViewModel by activityViewModels()
|
||||
private var onComplete: (() -> Unit)? = null
|
||||
|
||||
override fun onCreateView(
|
||||
@ -72,12 +74,11 @@ class AutoMapDialogFragment : BottomSheetDialogFragment() {
|
||||
// Check if this is a Nintendo Switch Joy-Con (not Pro Controller).
|
||||
// Joy-Cons have unique quirks: split devices, non-standard D-pad scan codes,
|
||||
// partial A/B swap but no X/Y swap from Android's evdev layer.
|
||||
val isJoyCon = InputBindingSetting.isJoyCon(device)
|
||||
val isJoyCon = GamepadHelper.isJoyCon(device)
|
||||
|
||||
if (isJoyCon) {
|
||||
Log.info("[AutoMap] Detected Joy-Con - using Joy-Con mappings")
|
||||
InputBindingSetting.clearAllBindings()
|
||||
InputBindingSetting.applyJoyConBindings()
|
||||
GamepadHelper.applyJoyConBindings(settingsViewModel.settings)
|
||||
onComplete?.invoke()
|
||||
dismiss()
|
||||
return true
|
||||
@ -106,8 +107,7 @@ class AutoMapDialogFragment : BottomSheetDialogFragment() {
|
||||
val dpadName = if (useAxisDpad) "axis" else "button"
|
||||
Log.info("[AutoMap] Detected $dpadName d-pad (device=${device?.name})")
|
||||
|
||||
InputBindingSetting.clearAllBindings()
|
||||
InputBindingSetting.applyAutoMapBindings(isNintendoLayout, useAxisDpad)
|
||||
GamepadHelper.applyAutoMapBindings(settingsViewModel.settings, isNintendoLayout, useAxisDpad)
|
||||
|
||||
onComplete?.invoke()
|
||||
dismiss()
|
||||
|
||||
@ -1270,6 +1270,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
||||
binding.surfaceInputOverlay.resetButtonPlacement()
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun updateShowPerformanceOverlay() {
|
||||
if (perfStatsUpdater != null) {
|
||||
perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!)
|
||||
|
||||
@ -56,36 +56,18 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
isCancelable = false
|
||||
view.requestFocus()
|
||||
view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
|
||||
if (setting!!.isButtonMappingSupported()) {
|
||||
dialog?.setOnKeyListener { _, _, event -> onKeyEvent(event) }
|
||||
}
|
||||
if (setting!!.isAxisMappingSupported()) {
|
||||
binding.root.setOnGenericMotionListener { _, event -> onMotionEvent(event) }
|
||||
}
|
||||
dialog?.setOnKeyListener { _, _, event -> onKeyEvent(event) }
|
||||
binding.root.setOnGenericMotionListener { _, event -> onMotionEvent(event) }
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
binding.textTitle.text =
|
||||
String.format(
|
||||
getString(R.string.input_dialog_title),
|
||||
getString(inputTypeId),
|
||||
getString(setting!!.nameId)
|
||||
setting!!.value,""
|
||||
)
|
||||
|
||||
var messageResId: Int = R.string.input_dialog_description
|
||||
if (setting!!.isAxisMappingSupported() && !setting!!.isTrigger()) {
|
||||
// Use specialized message for axis left/right or up/down
|
||||
messageResId = if (setting!!.isHorizontalOrientation()) {
|
||||
R.string.input_binding_description_horizontal_axis
|
||||
} else {
|
||||
R.string.input_binding_description_vertical_axis
|
||||
}
|
||||
}
|
||||
val messageResId: Int = R.string.input_dialog_description
|
||||
|
||||
binding.textMessage.text = getString(messageResId)
|
||||
|
||||
binding.buttonClear.setOnClickListener {
|
||||
@ -140,7 +122,7 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
var numMovedAxis = 0
|
||||
var axisMoveValue = 0.0f
|
||||
var lastMovedRange: InputDevice.MotionRange? = null
|
||||
var lastMovedDir = '?'
|
||||
var lastMovedDir = 0
|
||||
if (waitingForEvent) {
|
||||
for (i in motionRanges.indices) {
|
||||
val range = motionRanges[i]
|
||||
@ -164,7 +146,7 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
axisMoveValue = origValue
|
||||
numMovedAxis++
|
||||
lastMovedRange = range
|
||||
lastMovedDir = if (origValue < 0.0f) '-' else '+'
|
||||
lastMovedDir = if (origValue < 0.0f) -1 else 1
|
||||
}
|
||||
} 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
|
||||
@ -172,7 +154,7 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
// due to the first press being caught by the "if (firstEvent)" case further up.
|
||||
numMovedAxis++
|
||||
lastMovedRange = range
|
||||
lastMovedDir = if (previousValue < 0.0f) '-' else '+'
|
||||
lastMovedDir = if (previousValue < 0.0f) -1 else 1
|
||||
}
|
||||
}
|
||||
previousValues[i] = origValue
|
||||
@ -181,7 +163,7 @@ class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
// If only one axis moved, that's the winner.
|
||||
if (numMovedAxis == 1) {
|
||||
waitingForEvent = false
|
||||
setting?.onMotionInput(input, lastMovedRange!!, lastMovedDir)
|
||||
setting?.onMotionInput(lastMovedRange!!, lastMovedDir)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,7 +173,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
||||
continue
|
||||
}
|
||||
anyOverlayStateChanged = true
|
||||
|
||||
// TODO - switch these to using standard hotkey buttons instead of nativelibrary buttons
|
||||
if (button.id == NativeLibrary.ButtonType.BUTTON_SWAP && button.status == NativeLibrary.ButtonState.PRESSED) {
|
||||
swapScreen()
|
||||
}
|
||||
|
||||
@ -1,69 +0,0 @@
|
||||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.utils
|
||||
|
||||
import android.view.InputDevice
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
|
||||
/**
|
||||
* Some controllers have incorrect mappings. This class has special-case fixes for them.
|
||||
*/
|
||||
object ControllerMappingHelper {
|
||||
/**
|
||||
* Some controllers report extra button presses that can be ignored.
|
||||
*/
|
||||
fun shouldKeyBeIgnored(inputDevice: InputDevice, keyCode: Int): Boolean {
|
||||
return if (isDualShock4(inputDevice)) {
|
||||
// The two analog triggers generate analog motion events as well as a keycode.
|
||||
// We always prefer to use the analog values, so throw away the button press
|
||||
keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2
|
||||
} else false
|
||||
}
|
||||
|
||||
/**
|
||||
* Scale an axis to be zero-centered with a proper range.
|
||||
*/
|
||||
fun scaleAxis(inputDevice: InputDevice, axis: Int, value: Float): Float {
|
||||
if (isDualShock4(inputDevice)) {
|
||||
// Android doesn't have correct mappings for this controller's triggers. It reports them
|
||||
// as RX & RY, centered at -1.0, and with a range of [-1.0, 1.0]
|
||||
// Scale them to properly zero-centered with a range of [0.0, 1.0].
|
||||
if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY) {
|
||||
return (value + 1) / 2.0f
|
||||
}
|
||||
} else if (isXboxOneWireless(inputDevice)) {
|
||||
// Same as the DualShock 4, the mappings are missing.
|
||||
if (axis == MotionEvent.AXIS_Z || axis == MotionEvent.AXIS_RZ) {
|
||||
return (value + 1) / 2.0f
|
||||
}
|
||||
if (axis == MotionEvent.AXIS_GENERIC_1) {
|
||||
// This axis is stuck at ~.5. Ignore it.
|
||||
return 0.0f
|
||||
}
|
||||
} else if (isMogaPro2Hid(inputDevice)) {
|
||||
// This controller has a broken axis that reports a constant value. Ignore it.
|
||||
if (axis == MotionEvent.AXIS_GENERIC_1) {
|
||||
return 0.0f
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
private fun isDualShock4(inputDevice: InputDevice): Boolean {
|
||||
// Sony DualShock 4 controller
|
||||
return inputDevice.vendorId == 0x54c && inputDevice.productId == 0x9cc
|
||||
}
|
||||
|
||||
private fun isXboxOneWireless(inputDevice: InputDevice): Boolean {
|
||||
// Microsoft Xbox One controller
|
||||
return inputDevice.vendorId == 0x45e && inputDevice.productId == 0x2e0
|
||||
}
|
||||
|
||||
private fun isMogaPro2Hid(inputDevice: InputDevice): Boolean {
|
||||
// Moga Pro 2 HID
|
||||
return inputDevice.vendorId == 0x20d6 && inputDevice.productId == 0x6271
|
||||
}
|
||||
}
|
||||
@ -141,21 +141,17 @@ void Config::ReadSetting(const std::string& group, Settings::Setting<Type, range
|
||||
}
|
||||
|
||||
void Config::ReadValues() {
|
||||
// Controls
|
||||
// Controls - Always use the default_buttons and default_analogs on the native side, don't
|
||||
// actually read the INI file. It is only used by Kotlin, which will correctly dispatch
|
||||
// the codes as needed.
|
||||
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
|
||||
std::string default_param = InputManager::GenerateButtonParamPackage(default_buttons[i]);
|
||||
Settings::values.current_input_profile.buttons[i] = android_config->GetString(
|
||||
"Controls", Settings::NativeButton::mapping[i], default_param);
|
||||
if (Settings::values.current_input_profile.buttons[i].empty())
|
||||
Settings::values.current_input_profile.buttons[i] = default_param;
|
||||
Settings::values.current_input_profile.buttons[i] =
|
||||
InputManager::GenerateButtonParamPackage(default_buttons[i]);
|
||||
}
|
||||
|
||||
for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
|
||||
std::string default_param = InputManager::GenerateAnalogParamPackage(default_analogs[i]);
|
||||
Settings::values.current_input_profile.analogs[i] = android_config->GetString(
|
||||
"Controls", Settings::NativeAnalog::mapping[i], default_param);
|
||||
if (Settings::values.current_input_profile.analogs[i].empty())
|
||||
Settings::values.current_input_profile.analogs[i] = default_param;
|
||||
Settings::values.current_input_profile.analogs[i] =
|
||||
InputManager::GenerateAnalogParamPackage(default_analogs[i]);
|
||||
}
|
||||
|
||||
Settings::values.current_input_profile.motion_device = android_config->GetString(
|
||||
|
||||
@ -77,6 +77,39 @@ static const char* android_config_default_file_content = (BOOST_HANA_STRING(R"(
|
||||
# Use Artic Controller when connected to Artic Base Server. (Default 0)
|
||||
)") DECLARE_KEY(use_artic_base_controller) BOOST_HANA_STRING(R"(
|
||||
|
||||
# Configuration strings for 3ds buttons and axes. On Android, all such strings use "engine:gamepad" once bound. Default unbound.
|
||||
)") DECLARE_KEY(button_a) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(button_b) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(button_x) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(button_y) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(button_l) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(button_r) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(button_zl) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(button_zr) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(button_start) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(button_select) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(button_home) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(dpad_up) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(dpad_down) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(dpad_left) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(dpad_right) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(circlepad_up) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(circlepad_down) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(circlepad_left) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(circlepad_right) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(cstick_up) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(cstick_down) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(cstick_left) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(cstick_right) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(hotkey_close) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(hotkey_cycle_layout) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(hotkey_enable) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(hotkey_pause_resume) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(hotkey_quickload) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(hotkey_quicksave) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(hotkey_swap) BOOST_HANA_STRING(R"(
|
||||
)") DECLARE_KEY(hotkey_turbo_limit) BOOST_HANA_STRING(R"(
|
||||
|
||||
[Core]
|
||||
# Whether to use the Just-In-Time (JIT) compiler for CPU emulation
|
||||
# 0: Interpreter (slow), 1 (default): JIT (fast)
|
||||
@ -88,6 +121,7 @@ static const char* android_config_default_file_content = (BOOST_HANA_STRING(R"(
|
||||
# Range is any positive integer (but we suspect 25 - 400 is a good idea) Default is 100
|
||||
)") DECLARE_KEY(cpu_clock_percentage) BOOST_HANA_STRING(R"(
|
||||
|
||||
|
||||
[Renderer]
|
||||
# Whether to render using OpenGL
|
||||
# 1: OpenGL ES (default), 2: Vulkan
|
||||
|
||||
Loading…
Reference in New Issue
Block a user