diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/ScreenContent.kt b/src/android/app/src/main/java/info/cemu/cemu/guicore/components/ScreenContent.kt index 5dc5e4ea..044b3322 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/ScreenContent.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/guicore/components/ScreenContent.kt @@ -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, ) { diff --git a/src/android/app/src/main/java/info/cemu/cemu/input/InputManager.kt b/src/android/app/src/main/java/info/cemu/cemu/input/InputManager.kt index 429d2f5d..5a7abf3c 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/input/InputManager.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/input/InputManager.kt @@ -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> { + val gameControllers = mutableListOf>() + + 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().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 { + 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 { diff --git a/src/android/app/src/main/java/info/cemu/cemu/input/InputMappings.kt b/src/android/app/src/main/java/info/cemu/cemu/input/InputMappings.kt new file mode 100644 index 00000000..ee68c7b4 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/input/InputMappings.kt @@ -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), +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeFiles.kt b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeFiles.kt index 45f41603..5c944b04 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeFiles.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeFiles.kt @@ -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 { - Log.i("NATIVE_FILES", "listFiles $uri") +//// Log.i("NATIVE_FILES", "listFiles $uri") val files = ArrayList() val directoryUri = uri.fromNativePath() @@ -93,7 +93,7 @@ fun listFiles(uri: String): Array { @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 -> diff --git a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeInput.kt b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeInput.kt index 78aa2d62..d3139a1f 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeInput.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeInput.kt @@ -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 { + 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 @@ -204,7 +328,7 @@ object NativeInput { gyroZ: Float, accelX: Float, accelY: Float, - accelZ: Float + accelZ: Float, ) @JvmStatic diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerInputSettingsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerInputSettingsScreen.kt index e861680f..8a8d4dc4 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerInputSettingsScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerInputSettingsScreen.kt @@ -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>, + 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? -> diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllersViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllersViewModel.kt index a65eab1d..f3c531ef 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllersViewModel.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllersViewModel.kt @@ -23,13 +23,15 @@ class ControllersViewModel(val controllerIndex: Int) : ViewModel() { private val _controls = MutableStateFlow>(emptyMap()) val controls = _controls.asStateFlow() + private val _controllers = MutableStateFlow>?>(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 } diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index bf732e27..ceb598d5 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -332,4 +332,7 @@ Filter titles Install title Show dropdown menu for title + Setup all inputs + No controllers available + Select a controller \ No newline at end of file