refactor android input code

This commit is contained in:
SSimco
2025-04-28 08:39:14 +03:00
parent 119b39ab19
commit a0531916ec
8 changed files with 638 additions and 25 deletions
@@ -34,6 +34,7 @@ import info.cemu.cemu.R
@Composable
fun ScreenContent(
appBarText: String,
snackbarHost: @Composable () -> Unit = {},
navigateBack: () -> Unit,
actions: @Composable RowScope.() -> Unit = {},
contentModifier: Modifier = Modifier.padding(8.dp),
@@ -43,6 +44,7 @@ fun ScreenContent(
) {
ScreenContentGeneric(
appBarTitle = { DefaultAppBarTitle(appBarText) },
snackbarHost = snackbarHost,
navigateBack = navigateBack,
actions = actions,
) {
@@ -4,9 +4,14 @@ import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import info.cemu.cemu.nativeinterface.NativeInput
import info.cemu.cemu.nativeinterface.NativeInput.VPadButtons
import info.cemu.cemu.nativeinterface.NativeInput.ProControllerButtons
import info.cemu.cemu.nativeinterface.NativeInput.ClassicControllerButtons
import info.cemu.cemu.nativeinterface.NativeInput.WiimoteButtons
import info.cemu.cemu.nativeinterface.NativeInput.onNativeAxis
import info.cemu.cemu.nativeinterface.NativeInput.onNativeKey
import info.cemu.cemu.nativeinterface.NativeInput.setControllerMapping
import kotlin.math.abs
class InputManager {
@@ -40,13 +45,14 @@ class InputManager {
}
private fun isMotionEventFromJoystickOrGamepad(event: MotionEvent): Boolean {
return (event.source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK || (event.source and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD
return (event.source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
|| (event.source and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD
}
fun mapMotionEventToMappingId(
fun tryMapMotionEventToMappingId(
controllerIndex: Int,
mappingId: Int,
event: MotionEvent
event: MotionEvent,
): Boolean {
if (!isMotionEventFromJoystickOrGamepad(event)) {
return false
@@ -127,7 +133,7 @@ class InputManager {
return true
}
fun mapKeyEventToMappingId(controllerIndex: Int, mappingId: Int, event: KeyEvent): Boolean {
fun mapKeyEventToMappingId(controllerIndex: Int, mappingId: Int, event: KeyEvent) {
val device = event.device
setControllerMapping(
device.descriptor,
@@ -136,7 +142,263 @@ class InputManager {
mappingId,
event.keyCode
)
return true
}
private fun mapAxisCodeToMappingId(
controllerIndex: Int,
mappingId: Int,
deviceDescriptor: String,
deviceName: String,
axisCode: Int,
isPositive: Boolean,
) {
setControllerMapping(
deviceDescriptor,
deviceName,
controllerIndex,
mappingId,
getNativeAxisKey(axisCode, isPositive)
)
}
private fun mapKeyCodeToMappingId(
controllerIndex: Int,
mappingId: Int,
deviceDescriptor: String,
deviceName: String,
keyCode: Int,
) {
setControllerMapping(
deviceDescriptor,
deviceName,
controllerIndex,
mappingId,
keyCode
)
}
private fun InputDevice.isGameController(): Boolean {
return !isVirtual && (
sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD
|| sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK
|| sources and InputDevice.SOURCE_DPAD == InputDevice.SOURCE_DPAD
)
}
fun getGameControllers(): List<Pair<String, Int>> {
val gameControllers = mutableListOf<Pair<String, Int>>()
InputDevice.getDeviceIds().forEach { deviceId ->
val device = InputDevice.getDevice(deviceId) ?: return@forEach
if (gameControllers.any { (_, id) -> id == deviceId }) {
return@forEach
}
if (device.isGameController()) {
gameControllers.add(Pair(device.name, deviceId))
}
}
return gameControllers
}
fun mapAllInputs(deviceId: Int, controllerIndex: Int) {
if (NativeInput.isControllerDisabled(controllerIndex)) {
return
}
val controllerType = NativeInput.getControllerType(controllerIndex)
val device = InputDevice.getDevice(deviceId) ?: return
val inputs = mutableMapOf<InputMapping, Boolean>().apply {
val buttonKeyCodes =
ButtonInputMapping.entries.map { it.keyCode }.toIntArray()
ButtonInputMapping.entries
.zip(device.hasKeys(*buttonKeyCodes).toTypedArray())
.forEach { (button, hasKey) -> put(button, hasKey) }
device.motionRanges.forEach { motionRange ->
AxisInputMapping.entries.filter {
val isPositive = motionRange.min < 0 && !it.isPositive
val isNegative = motionRange.max > 0 && it.isPositive
(isPositive || isNegative) && it.axisCode == motionRange.axis
}.forEach { put(it, true) }
}
}
val buttons = NativeInput.getNativeButtonsForControllerType(controllerType)
for (button in buttons) {
val mapping = getButtonMappings(button).firstOrNull { inputs[it] == true }
?: FALLBACK_BUTTONS.firstOrNull { inputs[it] == true }
if (mapping == null) {
continue
}
inputs[mapping] = false
val buttonId = button.nativeKeyCode
if (mapping is ButtonInputMapping) {
mapKeyCodeToMappingId(
controllerIndex,
buttonId,
device.descriptor,
device.name,
mapping.keyCode
)
}
if (mapping is AxisInputMapping) {
mapAxisCodeToMappingId(
controllerIndex,
buttonId,
device.descriptor,
device.name,
mapping.axisCode,
mapping.isPositive,
)
}
}
}
private fun getButtonMappings(button: NativeInput.NativeInputButton): Array<InputMapping> {
return when (button) {
VPadButtons.A,
ProControllerButtons.A,
ClassicControllerButtons.A,
WiimoteButtons.A,
-> arrayOf(ButtonInputMapping.BUTTON_A)
VPadButtons.B,
ProControllerButtons.B,
ClassicControllerButtons.B,
WiimoteButtons.B,
-> arrayOf(ButtonInputMapping.BUTTON_B)
VPadButtons.X,
ProControllerButtons.X,
ClassicControllerButtons.X,
WiimoteButtons.ONE,
-> arrayOf(ButtonInputMapping.BUTTON_X)
VPadButtons.Y,
ProControllerButtons.Y,
ClassicControllerButtons.Y,
WiimoteButtons.TWO,
-> arrayOf(ButtonInputMapping.BUTTON_Y)
VPadButtons.L,
ProControllerButtons.L,
ClassicControllerButtons.L,
WiimoteButtons.NUNCHUCK_C,
-> arrayOf(ButtonInputMapping.BUTTON_L1)
VPadButtons.R,
ProControllerButtons.R,
ClassicControllerButtons.R,
WiimoteButtons.NUNCHUCK_Z,
-> arrayOf(ButtonInputMapping.BUTTON_R1)
VPadButtons.ZL,
ProControllerButtons.ZL,
ClassicControllerButtons.ZL,
-> arrayOf(ButtonInputMapping.BUTTON_L2, AxisInputMapping.LTRIGGER)
VPadButtons.ZR,
ProControllerButtons.ZR,
ClassicControllerButtons.ZR,
-> arrayOf(ButtonInputMapping.BUTTON_R2, AxisInputMapping.RTRIGGER)
VPadButtons.PLUS,
ProControllerButtons.PLUS,
ClassicControllerButtons.PLUS,
WiimoteButtons.PLUS,
-> arrayOf(ButtonInputMapping.BUTTON_START)
VPadButtons.MINUS,
ProControllerButtons.MINUS,
ClassicControllerButtons.MINUS,
WiimoteButtons.MINUS,
-> arrayOf(ButtonInputMapping.BUTTON_SELECT)
VPadButtons.STICKL_UP,
ProControllerButtons.STICKL_UP,
ClassicControllerButtons.STICKL_UP,
WiimoteButtons.NUNCHUCK_UP,
-> arrayOf(AxisInputMapping.Y_NEG)
VPadButtons.STICKL_DOWN,
ProControllerButtons.STICKL_DOWN,
ClassicControllerButtons.STICKL_DOWN,
WiimoteButtons.NUNCHUCK_DOWN,
-> arrayOf(AxisInputMapping.Y_POS)
VPadButtons.STICKL_LEFT,
ProControllerButtons.STICKL_LEFT,
ClassicControllerButtons.STICKL_LEFT,
WiimoteButtons.NUNCHUCK_LEFT,
-> arrayOf(AxisInputMapping.X_NEG)
VPadButtons.STICKL_RIGHT,
ProControllerButtons.STICKL_RIGHT,
ClassicControllerButtons.STICKL_RIGHT,
WiimoteButtons.NUNCHUCK_RIGHT,
-> arrayOf(AxisInputMapping.X_POS)
VPadButtons.STICKR_UP,
ProControllerButtons.STICKR_UP,
ClassicControllerButtons.STICKR_UP,
-> arrayOf(AxisInputMapping.RY_NEG, AxisInputMapping.RZ_NEG)
VPadButtons.STICKR_DOWN,
ProControllerButtons.STICKR_DOWN,
ClassicControllerButtons.STICKR_DOWN,
-> arrayOf(AxisInputMapping.RY_POS, AxisInputMapping.RZ_POS)
VPadButtons.STICKR_LEFT,
ProControllerButtons.STICKR_LEFT,
ClassicControllerButtons.STICKR_LEFT,
-> arrayOf(AxisInputMapping.RX_NEG, AxisInputMapping.Z_NEG)
VPadButtons.STICKR_RIGHT,
ProControllerButtons.STICKR_RIGHT,
ClassicControllerButtons.STICKR_RIGHT,
-> arrayOf(AxisInputMapping.RX_POS, AxisInputMapping.Z_POS)
VPadButtons.UP,
ProControllerButtons.UP,
ClassicControllerButtons.UP,
WiimoteButtons.UP,
-> arrayOf(AxisInputMapping.HAT_Y_NEG, ButtonInputMapping.DPAD_UP)
VPadButtons.DOWN,
ProControllerButtons.DOWN,
ClassicControllerButtons.DOWN,
WiimoteButtons.DOWN,
-> arrayOf(AxisInputMapping.HAT_Y_POS, ButtonInputMapping.DPAD_DOWN)
VPadButtons.LEFT,
ProControllerButtons.LEFT,
ClassicControllerButtons.LEFT,
WiimoteButtons.LEFT,
-> arrayOf(AxisInputMapping.HAT_X_NEG, ButtonInputMapping.DPAD_LEFT)
VPadButtons.RIGHT,
ProControllerButtons.RIGHT,
ClassicControllerButtons.RIGHT,
WiimoteButtons.RIGHT,
-> arrayOf(AxisInputMapping.HAT_X_POS, ButtonInputMapping.DPAD_RIGHT)
VPadButtons.STICKL,
ProControllerButtons.STICKL,
-> arrayOf(ButtonInputMapping.BUTTON_THUMBL)
VPadButtons.STICKR,
ProControllerButtons.STICKR,
-> arrayOf(ButtonInputMapping.BUTTON_THUMBR)
else -> arrayOf()
}
}
companion object {
@@ -0,0 +1,84 @@
package info.cemu.cemu.input
import android.view.KeyEvent
import android.view.MotionEvent
sealed interface InputMapping
enum class ButtonInputMapping(val keyCode: Int) : InputMapping {
BUTTON_1(KeyEvent.KEYCODE_BUTTON_1),
BUTTON_2(KeyEvent.KEYCODE_BUTTON_2),
BUTTON_3(KeyEvent.KEYCODE_BUTTON_3),
BUTTON_4(KeyEvent.KEYCODE_BUTTON_4),
BUTTON_5(KeyEvent.KEYCODE_BUTTON_5),
BUTTON_6(KeyEvent.KEYCODE_BUTTON_6),
BUTTON_7(KeyEvent.KEYCODE_BUTTON_7),
BUTTON_8(KeyEvent.KEYCODE_BUTTON_8),
BUTTON_9(KeyEvent.KEYCODE_BUTTON_9),
BUTTON_10(KeyEvent.KEYCODE_BUTTON_10),
BUTTON_11(KeyEvent.KEYCODE_BUTTON_11),
BUTTON_12(KeyEvent.KEYCODE_BUTTON_12),
BUTTON_13(KeyEvent.KEYCODE_BUTTON_13),
BUTTON_14(KeyEvent.KEYCODE_BUTTON_14),
BUTTON_15(KeyEvent.KEYCODE_BUTTON_15),
BUTTON_16(KeyEvent.KEYCODE_BUTTON_16),
BUTTON_A(KeyEvent.KEYCODE_BUTTON_A),
BUTTON_B(KeyEvent.KEYCODE_BUTTON_B),
BUTTON_C(KeyEvent.KEYCODE_BUTTON_C),
BUTTON_L1(KeyEvent.KEYCODE_BUTTON_L1),
BUTTON_L2(KeyEvent.KEYCODE_BUTTON_L2),
BUTTON_MODE(KeyEvent.KEYCODE_BUTTON_MODE),
BUTTON_R1(KeyEvent.KEYCODE_BUTTON_R1),
BUTTON_R2(KeyEvent.KEYCODE_BUTTON_R2),
BUTTON_SELECT(KeyEvent.KEYCODE_BUTTON_SELECT),
BUTTON_START(KeyEvent.KEYCODE_BUTTON_START),
BUTTON_THUMBL(KeyEvent.KEYCODE_BUTTON_THUMBL),
BUTTON_THUMBR(KeyEvent.KEYCODE_BUTTON_THUMBR),
BUTTON_X(KeyEvent.KEYCODE_BUTTON_X),
BUTTON_Y(KeyEvent.KEYCODE_BUTTON_Y),
BUTTON_Z(KeyEvent.KEYCODE_BUTTON_Z),
DPAD_DOWN(KeyEvent.KEYCODE_DPAD_DOWN),
DPAD_LEFT(KeyEvent.KEYCODE_DPAD_LEFT),
DPAD_RIGHT(KeyEvent.KEYCODE_DPAD_RIGHT),
DPAD_UP(KeyEvent.KEYCODE_DPAD_UP),
}
val FALLBACK_BUTTONS = arrayOf(
ButtonInputMapping.BUTTON_1,
ButtonInputMapping.BUTTON_2,
ButtonInputMapping.BUTTON_3,
ButtonInputMapping.BUTTON_4,
ButtonInputMapping.BUTTON_5,
ButtonInputMapping.BUTTON_6,
ButtonInputMapping.BUTTON_7,
ButtonInputMapping.BUTTON_8,
ButtonInputMapping.BUTTON_9,
ButtonInputMapping.BUTTON_10,
ButtonInputMapping.BUTTON_11,
ButtonInputMapping.BUTTON_12,
ButtonInputMapping.BUTTON_13,
ButtonInputMapping.BUTTON_14,
ButtonInputMapping.BUTTON_15,
ButtonInputMapping.BUTTON_16,
)
enum class AxisInputMapping(val axisCode: Int, val isPositive: Boolean) : InputMapping {
HAT_X_POS(MotionEvent.AXIS_HAT_X, true),
HAT_X_NEG(MotionEvent.AXIS_HAT_X, false),
HAT_Y_POS(MotionEvent.AXIS_HAT_Y, true),
HAT_Y_NEG(MotionEvent.AXIS_HAT_Y, false),
RX_POS(MotionEvent.AXIS_RX, true),
RX_NEG(MotionEvent.AXIS_RX, false),
RY_POS(MotionEvent.AXIS_RY, true),
RY_NEG(MotionEvent.AXIS_RY, false),
RZ_POS(MotionEvent.AXIS_RZ, true),
RZ_NEG(MotionEvent.AXIS_RZ, false),
LTRIGGER(MotionEvent.AXIS_LTRIGGER, true),
RTRIGGER(MotionEvent.AXIS_RTRIGGER, true),
X_POS(MotionEvent.AXIS_X, true),
X_NEG(MotionEvent.AXIS_X, false),
Y_POS(MotionEvent.AXIS_Y, true),
Y_NEG(MotionEvent.AXIS_Y, false),
Z_POS(MotionEvent.AXIS_Z, true),
Z_NEG(MotionEvent.AXIS_Z, false),
}
@@ -42,7 +42,7 @@ fun String.fromNativePath(): Uri {
@Keep
fun openContentUri(uri: String): Int {
try {
Log.i("NATIVE_FILES", "openContentUri $uri")
// Log.i("NATIVE_FILES", "openContentUri $uri")
val parcelFileDescriptor =
ContentResolver.openFileDescriptor(
uri.fromNativePath(), MODE
@@ -60,7 +60,7 @@ fun openContentUri(uri: String): Int {
@Keep
fun listFiles(uri: String): Array<String?> {
Log.i("NATIVE_FILES", "listFiles $uri")
//// Log.i("NATIVE_FILES", "listFiles $uri")
val files = ArrayList<String>()
val directoryUri = uri.fromNativePath()
@@ -93,7 +93,7 @@ fun listFiles(uri: String): Array<String?> {
@Keep
fun isDirectory(uri: String): Boolean {
Log.i("NATIVE_FILES", "isDirectory $uri")
// Log.i("NATIVE_FILES", "isDirectory $uri")
val mimeType = ContentResolver.getType(
uri.fromNativePath()
@@ -108,7 +108,7 @@ fun isFile(uri: String): Boolean {
@Keep
fun exists(uri: String): Boolean {
Log.i("NATIVE_FILES", "exists $uri")
// Log.i("NATIVE_FILES", "exists $uri")
try {
ContentResolver.query(uri.fromNativePath(), null, null, null, null).use { cursor ->
@@ -1,6 +1,10 @@
package info.cemu.cemu.nativeinterface
object NativeInput {
sealed interface NativeInputButton {
val nativeKeyCode: Int
}
const val VPAD_BUTTON_NONE: Int = 0
const val VPAD_BUTTON_A: Int = 1
const val VPAD_BUTTON_B: Int = 2
@@ -31,6 +35,36 @@ object NativeInput {
const val VPAD_BUTTON_HOME: Int = 27
const val VPAD_BUTTON_MAX: Int = 28
enum class VPadButtons(override val nativeKeyCode: Int) : NativeInputButton {
A(VPAD_BUTTON_A),
B(VPAD_BUTTON_B),
X(VPAD_BUTTON_X),
Y(VPAD_BUTTON_Y),
L(VPAD_BUTTON_L),
R(VPAD_BUTTON_R),
ZL(VPAD_BUTTON_ZL),
ZR(VPAD_BUTTON_ZR),
PLUS(VPAD_BUTTON_PLUS),
MINUS(VPAD_BUTTON_MINUS),
UP(VPAD_BUTTON_UP),
DOWN(VPAD_BUTTON_DOWN),
LEFT(VPAD_BUTTON_LEFT),
RIGHT(VPAD_BUTTON_RIGHT),
STICKL(VPAD_BUTTON_STICKL),
STICKR(VPAD_BUTTON_STICKR),
STICKL_UP(VPAD_BUTTON_STICKL_UP),
STICKL_DOWN(VPAD_BUTTON_STICKL_DOWN),
STICKL_LEFT(VPAD_BUTTON_STICKL_LEFT),
STICKL_RIGHT(VPAD_BUTTON_STICKL_RIGHT),
STICKR_UP(VPAD_BUTTON_STICKR_UP),
STICKR_DOWN(VPAD_BUTTON_STICKR_DOWN),
STICKR_LEFT(VPAD_BUTTON_STICKR_LEFT),
STICKR_RIGHT(VPAD_BUTTON_STICKR_RIGHT),
MIC(VPAD_BUTTON_MIC),
SCREEN(VPAD_BUTTON_SCREEN),
HOME(VPAD_BUTTON_HOME),
}
const val PRO_BUTTON_NONE: Int = 0
const val PRO_BUTTON_A: Int = 1
const val PRO_BUTTON_B: Int = 2
@@ -59,6 +93,34 @@ object NativeInput {
const val PRO_BUTTON_STICKR_RIGHT: Int = 25
const val PRO_BUTTON_MAX: Int = 26
enum class ProControllerButtons(override val nativeKeyCode: Int) : NativeInputButton {
A(PRO_BUTTON_A),
B(PRO_BUTTON_B),
X(PRO_BUTTON_X),
Y(PRO_BUTTON_Y),
L(PRO_BUTTON_L),
R(PRO_BUTTON_R),
ZL(PRO_BUTTON_ZL),
ZR(PRO_BUTTON_ZR),
PLUS(PRO_BUTTON_PLUS),
MINUS(PRO_BUTTON_MINUS),
HOME(PRO_BUTTON_HOME),
UP(PRO_BUTTON_UP),
DOWN(PRO_BUTTON_DOWN),
LEFT(PRO_BUTTON_LEFT),
RIGHT(PRO_BUTTON_RIGHT),
STICKL(PRO_BUTTON_STICKL),
STICKR(PRO_BUTTON_STICKR),
STICKL_UP(PRO_BUTTON_STICKL_UP),
STICKL_DOWN(PRO_BUTTON_STICKL_DOWN),
STICKL_LEFT(PRO_BUTTON_STICKL_LEFT),
STICKL_RIGHT(PRO_BUTTON_STICKL_RIGHT),
STICKR_UP(PRO_BUTTON_STICKR_UP),
STICKR_DOWN(PRO_BUTTON_STICKR_DOWN),
STICKR_LEFT(PRO_BUTTON_STICKR_LEFT),
STICKR_RIGHT(PRO_BUTTON_STICKR_RIGHT),
}
const val CLASSIC_BUTTON_NONE: Int = 0
const val CLASSIC_BUTTON_A: Int = 1
const val CLASSIC_BUTTON_B: Int = 2
@@ -85,6 +147,32 @@ object NativeInput {
const val CLASSIC_BUTTON_STICKR_RIGHT: Int = 23
const val CLASSIC_BUTTON_MAX: Int = 24
enum class ClassicControllerButtons(override val nativeKeyCode: Int) : NativeInputButton {
A(CLASSIC_BUTTON_A),
B(CLASSIC_BUTTON_B),
X(CLASSIC_BUTTON_X),
Y(CLASSIC_BUTTON_Y),
L(CLASSIC_BUTTON_L),
R(CLASSIC_BUTTON_R),
ZL(CLASSIC_BUTTON_ZL),
ZR(CLASSIC_BUTTON_ZR),
PLUS(CLASSIC_BUTTON_PLUS),
MINUS(CLASSIC_BUTTON_MINUS),
HOME(CLASSIC_BUTTON_HOME),
UP(CLASSIC_BUTTON_UP),
DOWN(CLASSIC_BUTTON_DOWN),
LEFT(CLASSIC_BUTTON_LEFT),
RIGHT(CLASSIC_BUTTON_RIGHT),
STICKL_UP(CLASSIC_BUTTON_STICKL_UP),
STICKL_DOWN(CLASSIC_BUTTON_STICKL_DOWN),
STICKL_LEFT(CLASSIC_BUTTON_STICKL_LEFT),
STICKL_RIGHT(CLASSIC_BUTTON_STICKL_RIGHT),
STICKR_UP(CLASSIC_BUTTON_STICKR_UP),
STICKR_DOWN(CLASSIC_BUTTON_STICKR_DOWN),
STICKR_LEFT(CLASSIC_BUTTON_STICKR_LEFT),
STICKR_RIGHT(CLASSIC_BUTTON_STICKR_RIGHT),
}
const val WIIMOTE_BUTTON_NONE: Int = 0
const val WIIMOTE_BUTTON_A: Int = 1
const val WIIMOTE_BUTTON_B: Int = 2
@@ -105,6 +193,36 @@ object NativeInput {
const val WIIMOTE_BUTTON_HOME: Int = 17
const val WIIMOTE_BUTTON_MAX: Int = 18
enum class WiimoteButtons(override val nativeKeyCode: Int) : NativeInputButton {
A(WIIMOTE_BUTTON_A),
B(WIIMOTE_BUTTON_B),
ONE(WIIMOTE_BUTTON_1),
TWO(WIIMOTE_BUTTON_2),
NUNCHUCK_Z(WIIMOTE_BUTTON_NUNCHUCK_Z),
NUNCHUCK_C(WIIMOTE_BUTTON_NUNCHUCK_C),
PLUS(WIIMOTE_BUTTON_PLUS),
MINUS(WIIMOTE_BUTTON_MINUS),
UP(WIIMOTE_BUTTON_UP),
DOWN(WIIMOTE_BUTTON_DOWN),
LEFT(WIIMOTE_BUTTON_LEFT),
RIGHT(WIIMOTE_BUTTON_RIGHT),
NUNCHUCK_UP(WIIMOTE_BUTTON_NUNCHUCK_UP),
NUNCHUCK_DOWN(WIIMOTE_BUTTON_NUNCHUCK_DOWN),
NUNCHUCK_LEFT(WIIMOTE_BUTTON_NUNCHUCK_LEFT),
NUNCHUCK_RIGHT(WIIMOTE_BUTTON_NUNCHUCK_RIGHT),
HOME(WIIMOTE_BUTTON_HOME),
}
fun getNativeButtonsForControllerType(controllerType: Int): Array<NativeInputButton> {
return when (controllerType) {
EMULATED_CONTROLLER_TYPE_VPAD -> VPadButtons.entries.toTypedArray()
EMULATED_CONTROLLER_TYPE_PRO -> ProControllerButtons.entries.toTypedArray()
EMULATED_CONTROLLER_TYPE_CLASSIC -> ClassicControllerButtons.entries.toTypedArray()
EMULATED_CONTROLLER_TYPE_WIIMOTE -> WiimoteButtons.entries.toTypedArray()
else -> arrayOf()
}
}
const val EMULATED_CONTROLLER_TYPE_VPAD: Int = 0
const val EMULATED_CONTROLLER_TYPE_PRO: Int = 1
const val EMULATED_CONTROLLER_TYPE_CLASSIC: Int = 2
@@ -135,7 +253,7 @@ object NativeInput {
deviceDescriptor: String?,
deviceName: String?,
key: Int,
isPressed: Boolean
isPressed: Boolean,
)
@JvmStatic
@@ -143,7 +261,7 @@ object NativeInput {
deviceDescriptor: String?,
deviceName: String?,
axis: Int,
value: Float
value: Float,
)
@JvmStatic
@@ -175,15 +293,21 @@ object NativeInput {
deviceName: String?,
index: Int,
mappingId: Int,
buttonId: Int
buttonId: Int,
)
@JvmStatic
external fun clearControllerMapping(index: Int, mappingId: Int)
fun clearControllerMapping(index: Int, button: NativeInputButton) =
clearControllerMapping(index, button.nativeKeyCode)
@JvmStatic
external fun getControllerMapping(index: Int, mappingId: Int): String
fun getControllerMapping(index: Int, button: NativeInputButton): String =
getControllerMapping(index, button.nativeKeyCode)
@JvmStatic
external fun getControllerMappings(index: Int): Map<Int, String>
@@ -204,7 +328,7 @@ object NativeInput {
gyroZ: Float,
accelX: Float,
accelY: Float,
accelZ: Float
accelZ: Float,
)
@JvmStatic
@@ -4,11 +4,34 @@ import android.content.Context
import android.view.KeyEvent
import android.view.MotionEvent
import android.widget.TextView
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.viewmodel.MutableCreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -17,6 +40,9 @@ import info.cemu.cemu.guicore.components.ScreenContent
import info.cemu.cemu.guicore.components.SingleSelection
import info.cemu.cemu.guicore.nativeenummapper.controllerTypeToStringId
import info.cemu.cemu.nativeinterface.NativeInput
import kotlinx.coroutines.launch
import androidx.compose.material3.Button as MaterialButton
@Composable
fun ControllerInputSettingsScreen(
@@ -32,21 +58,26 @@ fun ControllerInputSettingsScreen(
val context = LocalContext.current
val controllerType by controllersViewModel.controllerType.collectAsState()
val controls by controllersViewModel.controls.collectAsState()
val controllers by controllersViewModel.controllers.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
fun onInputClick(buttonName: String, buttonId: Int) {
openInputDialog(
context = context,
buttonName = buttonName,
onClear = { controllersViewModel.clearButtonMapping(buttonId) },
tryMapKeyEvent = { controllersViewModel.tryMapKeyEvent(it, buttonId) },
mapKeyEvent = { controllersViewModel.mapKeyEvent(it, buttonId) },
tryMapMotionEvent = { controllersViewModel.tryMapMotionEvent(it, buttonId) },
)
}
ScreenContent(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
appBarText = stringResource(R.string.controller_numbered, controllerIndex + 1),
navigateBack = navigateBack,
) {
SingleSelection(
isChoiceEnabled = controllersViewModel::isControllerTypeAllowed,
label = stringResource(R.string.emulated_controller),
@@ -61,6 +92,34 @@ fun ControllerInputSettingsScreen(
choiceToString = { stringResource(controllerTypeToStringId(it)) },
onChoiceChanged = controllersViewModel::setControllerType
)
if (controllerType != NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED) {
MaterialButton(
modifier = Modifier.padding(8.dp),
onClick = {
controllersViewModel.refreshAvailableControllers {
coroutineScope.launch {
snackbarHostState.currentSnackbarData?.dismiss()
snackbarHostState.showSnackbar(context.getString(R.string.no_controllers_available))
}
}
}
) {
Text(stringResource(R.string.controller_setup_all_inputs))
}
}
controllers?.let {
ControllerSelectDialog(
controllers = it,
onDismissRequest = controllersViewModel::clearGameControllers,
onSelect = { deviceId ->
controllersViewModel.mapAllInputs(deviceId)
controllersViewModel.clearGameControllers()
}
)
}
when (controllerType) {
NativeInput.EMULATED_CONTROLLER_TYPE_VPAD -> VPADInputs(
controllerIndex = controllerIndex,
@@ -86,11 +145,67 @@ fun ControllerInputSettingsScreen(
}
}
@Composable
private fun ControllerSelectDialog(
controllers: List<Pair<String, Int>>,
onDismissRequest: () -> Unit,
onSelect: (Int) -> Unit,
) {
Dialog(onDismissRequest = onDismissRequest) {
Card(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
modifier = Modifier
.sizeIn(maxWidth = 560.dp, maxHeight = 560.dp)
.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
) {
Text(
modifier = Modifier.padding(
start = 16.dp,
end = 16.dp,
top = 16.dp,
bottom = 8.dp
),
text = stringResource(R.string.controller_select_dialog_title),
fontSize = 24.sp,
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp)
.weight(weight = 1.0f, fill = false)
.verticalScroll(rememberScrollState()),
) {
controllers.forEach { (controllerName, deviceId) ->
Text(
text = controllerName,
modifier = Modifier
.fillMaxWidth()
.clickable { onSelect(deviceId) }
.padding(vertical = 16.dp, horizontal = 8.dp)
)
}
}
HorizontalDivider()
TextButton(
onClick = onDismissRequest,
modifier = Modifier
.padding(8.dp)
.align(Alignment.End),
) {
Text(stringResource(R.string.cancel))
}
}
}
}
fun openInputDialog(
context: Context,
buttonName: String,
onClear: () -> Unit,
tryMapKeyEvent: (KeyEvent) -> Boolean,
mapKeyEvent: (KeyEvent) -> Unit,
tryMapMotionEvent: (MotionEvent) -> Boolean,
) {
MaterialAlertDialogBuilder(context).setTitle(R.string.inputBindingDialogTitle)
@@ -103,9 +218,8 @@ fun openInputDialog(
isFocusableInTouchMode = true
requestFocus()
setOnKeyListener { _, _, keyEvent: KeyEvent ->
if (tryMapKeyEvent(keyEvent)) {
alertDialog.dismiss()
}
mapKeyEvent(keyEvent)
alertDialog.dismiss()
true
}
setOnGenericMotionListener { _, motionEvent: MotionEvent? ->
@@ -23,13 +23,15 @@ class ControllersViewModel(val controllerIndex: Int) : ViewModel() {
private val _controls = MutableStateFlow<Map<Int, String>>(emptyMap())
val controls = _controls.asStateFlow()
private val _controllers = MutableStateFlow<List<Pair<String, Int>>?>(null)
val controllers = _controllers.asStateFlow()
private var vpadCount = 0
private var wpadCount = 0
private fun getControllerMapping(buttonId: Int) =
buttonId to NativeInput.getControllerMapping(controllerIndex, buttonId)
fun setControllerType(controllerType: Int) {
if (!isControllerTypeAllowed(controllerType) || _controllerType.value == controllerType)
return
@@ -38,16 +40,38 @@ class ControllersViewModel(val controllerIndex: Int) : ViewModel() {
refreshControllerData()
}
fun tryMapKeyEvent(keyEvent: KeyEvent, buttonId: Int): Boolean {
if (inputManager.mapKeyEventToMappingId(controllerIndex, buttonId, keyEvent)) {
_controls.value += getControllerMapping(buttonId)
return true
fun mapKeyEvent(keyEvent: KeyEvent, buttonId: Int) {
inputManager.mapKeyEventToMappingId(controllerIndex, buttonId, keyEvent)
_controls.value += getControllerMapping(buttonId)
}
fun refreshAvailableControllers(onNoControllersAvailable: () -> Unit) {
val newControllers = inputManager.getGameControllers()
if (newControllers.isEmpty()) {
_controllers.value = null
onNoControllersAvailable()
return
}
return false
_controllers.value = newControllers
}
fun clearGameControllers() {
_controllers.value = null
}
fun mapAllInputs(deviceId: Int) {
val oldControls = _controls.value
_controls.value = emptyMap()
oldControls.keys.forEach { NativeInput.clearControllerMapping(controllerIndex, it) }
inputManager.mapAllInputs(deviceId, controllerIndex)
val buttons = NativeInput.getNativeButtonsForControllerType (controllerType.value)
buttons.forEach { _controls.value += getControllerMapping(it.nativeKeyCode) }
}
fun tryMapMotionEvent(motionEvent: MotionEvent, buttonId: Int): Boolean {
if (inputManager.mapMotionEventToMappingId(controllerIndex, buttonId, motionEvent)) {
if (inputManager.tryMapMotionEventToMappingId(controllerIndex, buttonId, motionEvent)) {
_controls.value += getControllerMapping(buttonId)
return true
}
@@ -332,4 +332,7 @@
<string name="title_manager_filter_titles_description">Filter titles</string>
<string name="title_manager_install_title_description">Install title</string>
<string name="title_dropdown_menu_description">Show dropdown menu for title</string>
<string name="controller_setup_all_inputs">Setup all inputs</string>
<string name="no_controllers_available">No controllers available</string>
<string name="controller_select_dialog_title">Select a controller</string>
</resources>