diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController.kt index 2ababa511..cc9167ffc 100644 --- a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController.kt +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController.kt @@ -37,7 +37,7 @@ typealias GamePadConfig = RadialGamePadConfig private const val DUMMY_LEFT_STICK_PRESS_ID = 10001 private const val DUMMY_RIGHT_STICK_PRESS_ID = 10002 -class GameController(var activity: Activity) { +class GameController(var activity: Activity) : IGameController { companion object { private fun init(context: Context, controller: GameController): View { @@ -80,12 +80,13 @@ class GameController(var activity: Activity) { var leftGamePad: GamePad var rightGamePad: GamePad var controllerId: Int = -1 - val isVisible: Boolean + override val isVisible: Boolean get() = controllerView?.isVisible ?: false init { - leftGamePad = GamePad(generateConfig(true), 16f, activity) - rightGamePad = GamePad(generateConfig(false), 16f, activity) + val useSwitchLayout = QuickSettings(activity).useSwitchLayout + leftGamePad = GamePad(generateConfig(true, useSwitchLayout), 16f, activity) + rightGamePad = GamePad(generateConfig(false, useSwitchLayout), 16f, activity) leftGamePad.primaryDialMaxSizeDp = 200f rightGamePad.primaryDialMaxSizeDp = 200f @@ -96,14 +97,14 @@ class GameController(var activity: Activity) { rightGamePad.gravityY = 1f } - fun setVisible(isVisible: Boolean) { + override fun setVisible(isVisible: Boolean) { controllerView?.apply { this.isVisible = isVisible if (isVisible) connect() } } - fun connect() { + override fun connect() { if (controllerId == -1) controllerId = KenjinxNative.inputConnectGamepad(0) } @@ -177,7 +178,7 @@ suspend fun Flow.safeCollect(block: suspend (T) -> Unit) { .collect { block(it) } } -private fun generateConfig(isLeft: Boolean): GamePadConfig { +private fun generateConfig(isLeft: Boolean, useSwitchLayout: Boolean): GamePadConfig { val distance = 0.3f val buttonScale = 1f @@ -293,20 +294,23 @@ private fun generateConfig(isLeft: Boolean): GamePadConfig { /* ringSegments = */ 12, /* Primary (ABXY) */ PrimaryDialConfig.PrimaryButtons( - listOf( - ButtonConfig( - GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null - ), - ButtonConfig( - GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null - ), - ButtonConfig( - GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null - ), - ButtonConfig( - GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null + if (useSwitchLayout) { + // Switch/Nintendo: A=Right, X=Top, Y=Left, B=Bottom + listOf( + ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null), // Right + ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Top + ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Left + ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null) // Bottom ) - ), + } else { + // Xbox-Stil: B=Right, Y=Top, X=Left, A=Bottom + listOf( + ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null), // Right + ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Top + ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Left + ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null) // Bottom + ) + }, null, 0f, true, diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController2.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController2.kt new file mode 100644 index 000000000..d796624b1 --- /dev/null +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController2.kt @@ -0,0 +1,403 @@ +package org.kenjinx.android + +import android.app.Activity +import android.content.Context +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.math.MathUtils +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import com.swordfish.radialgamepad.library.RadialGamePad +import com.swordfish.radialgamepad.library.config.ButtonConfig +import com.swordfish.radialgamepad.library.config.CrossConfig +import com.swordfish.radialgamepad.library.config.CrossContentDescription +import com.swordfish.radialgamepad.library.config.PrimaryDialConfig +import com.swordfish.radialgamepad.library.config.RadialGamePadConfig +import com.swordfish.radialgamepad.library.config.SecondaryDialConfig +import com.swordfish.radialgamepad.library.event.Event +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.launch +import org.kenjinx.android.viewmodels.MainViewModel +import org.kenjinx.android.viewmodels.QuickSettings + +// --- Dummy IDs to disconnect legacy L3/R3 (stick double tap/tap) --- +private const val DUMMY_LEFT_STICK_PRESS_ID = 10001 +private const val DUMMY_RIGHT_STICK_PRESS_ID = 10002 + +/** + * GameController2 + * Layout 2 – aktuell identisch zum Default-Layout (GameController), + * als eigenständige Klasse für Preset-Umschaltung. + */ +class GameController2(var activity: Activity) : IGameController { + + companion object { + private fun init(context: Context, controller: GameController2): View { + val inflater = LayoutInflater.from(context) + val parent = FrameLayout(context) + val view = inflater.inflate(R.layout.game_layout, parent, false) + view.findViewById(R.id.leftcontainer)!!.addView(controller.leftGamePad) + view.findViewById(R.id.rightcontainer)!!.addView(controller.rightGamePad) + return view + } + + @Composable + fun Compose(viewModel: MainViewModel) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + val controller = GameController2(viewModel.activity) + val c = init(context, controller) + + viewModel.activity.lifecycleScope.launch { + val events = merge( + controller.leftGamePad.events(), + controller.rightGamePad.events() + ) + // safeCollect kommt aus GameController.kt (bitte dort belassen) + events.safeCollect { controller.handleEvent(it) } + } + + controller.controllerView = c + viewModel.setGameController(controller) + controller.setVisible(QuickSettings(viewModel.activity).useVirtualController) + c + } + ) + } + } + + private var controllerView: View? = null + var leftGamePad: RadialGamePad + var rightGamePad: RadialGamePad + var controllerId: Int = -1 + override val isVisible: Boolean + get() = controllerView?.isVisible ?: false + + init { + val useSwitchLayout = QuickSettings(activity).useSwitchLayout + leftGamePad = RadialGamePad(generateConfig2(true, useSwitchLayout), 16f, activity) + rightGamePad = RadialGamePad(generateConfig2(false, useSwitchLayout), 16f, activity) + + leftGamePad.primaryDialMaxSizeDp = 200f + rightGamePad.primaryDialMaxSizeDp = 200f + + leftGamePad.gravityX = -1f + leftGamePad.gravityY = 1f + rightGamePad.gravityX = 1f + rightGamePad.gravityY = 1f + } + + override fun setVisible(isVisible: Boolean) { + controllerView?.apply { + this.isVisible = isVisible + if (isVisible) connect() + } + } + + override fun connect() { + if (controllerId == -1) + controllerId = KenjinxNative.inputConnectGamepad(0) + } + + private fun handleEvent(ev: Event) { + if (controllerId == -1) + controllerId = KenjinxNative.inputConnectGamepad(0) + + controllerId.apply { + when (ev) { + is Event.Button -> { + // Ignore legacy L3/R3 via stick press (double tap) + if (ev.id == DUMMY_LEFT_STICK_PRESS_ID || ev.id == DUMMY_RIGHT_STICK_PRESS_ID) { + return + } + when (ev.action) { + KeyEvent.ACTION_UP -> KenjinxNative.inputSetButtonReleased(ev.id, this) + KeyEvent.ACTION_DOWN -> KenjinxNative.inputSetButtonPressed(ev.id, this) + } + } + + is Event.Direction -> { + when (ev.id) { + GamePadButtonInputId.DpadUp.ordinal -> { + // Horizontal + if (ev.xAxis > 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadRight.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this) + } else if (ev.xAxis < 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadLeft.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this) + } else { + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this) + } + // Vertical + if (ev.yAxis < 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadUp.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this) + } else if (ev.yAxis > 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadDown.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this) + } else { + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this) + } + } + + GamePadButtonInputId.LeftStick.ordinal -> { + val setting = QuickSettings(activity) + val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f) + val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f) + KenjinxNative.inputSetStickAxis(1, x, -y, this) + } + + GamePadButtonInputId.RightStick.ordinal -> { + val setting = QuickSettings(activity) + val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f) + val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f) + KenjinxNative.inputSetStickAxis(2, x, -y, this) + } + } + } + } + } + } +} + +// --- Lokale Kopie der Config-Erzeugung (Name = generateConfig2, um Konflikte zu vermeiden) --- +private fun generateConfig2(isLeft: Boolean, useSwitchLayout: Boolean): RadialGamePadConfig { + val distance = 0.3f + val buttonScale = 1f + + if (isLeft) { + return RadialGamePadConfig( + /* ringSegments = */ 12, + /* Primary (Stick) */ + // IMPORTANT: pressButtonId -> DUMMY_LEFT_STICK_PRESS_ID, so that double tap does not trigger L3 + PrimaryDialConfig.Stick( + GamePadButtonInputId.LeftStick.ordinal, + DUMMY_LEFT_STICK_PRESS_ID, + setOf(), + "LeftStick", + null + ), + listOf( + // D-Pad + SecondaryDialConfig.Cross( + /* sector */ 10, + /* size */ 3, + /* gap */ 2.1f, + distance, + CrossConfig( + GamePadButtonInputId.DpadUp.ordinal, + CrossConfig.Shape.STANDARD, + null, + setOf(), + CrossContentDescription(), + true, + null + ), + SecondaryDialConfig.RotationProcessor() + ), + + // Minus + SecondaryDialConfig.SingleButton( + /* sector */ 9, + buttonScale, + 0.3f, + ButtonConfig( + GamePadButtonInputId.Minus.ordinal, + "-", + true, + null, + "Minus", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // L-Bumper + SecondaryDialConfig.DoubleButton( + /* sector */ 2, + 0.2f, + ButtonConfig( + GamePadButtonInputId.LeftShoulder.ordinal, + "L", + true, + null, + "LeftBumper", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // ZL-Trigger + SecondaryDialConfig.DoubleButton( + /* sector */ 2, + 1f, + ButtonConfig( + GamePadButtonInputId.LeftTrigger.ordinal, + "ZL", + true, + null, + "LeftTrigger", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // L3 separat + SecondaryDialConfig.SingleButton( + /* sector */ 1, + buttonScale, + 1.0f, + ButtonConfig( + GamePadButtonInputId.LeftStickButton.ordinal, + "L3", + true, + null, + "LeftStickButton", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + ) + ) + } else { + return RadialGamePadConfig( + /* ringSegments = */ 12, + /* Primary (ABXY) */ + PrimaryDialConfig.PrimaryButtons( + if (useSwitchLayout) { + // Switch/Nintendo: A=Right, X=Top, Y=Left, B=Bottom + listOf( + ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null), // Right + ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Top + ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Left + ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null) // Bottom + ) + } else { + // Xbox-Stil: B=Right, Y=Top, X=Left, A=Bottom + listOf( + ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null), // Right + ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Top + ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Left + ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null) // Bottom + ) + }, + null, + 0f, + true, + null + ), + listOf( + // Right stick + // IMPORTANT: pressButtonId -> DUMMY_RIGHT_STICK_PRESS_ID, so that double tap does not trigger R3 + SecondaryDialConfig.Stick( + /* sector */ 6, + /* size */ 3, + /* gap */ 2.7f, + distance, + GamePadButtonInputId.RightStick.ordinal, + DUMMY_RIGHT_STICK_PRESS_ID, + null, + setOf(), + "RightStick", + SecondaryDialConfig.RotationProcessor() + ), + + // Plus + SecondaryDialConfig.SingleButton( + /* sector */ 9, + buttonScale, + 0.3f, + ButtonConfig( + GamePadButtonInputId.Plus.ordinal, + "+", + true, + null, + "Plus", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // R-Bumper + SecondaryDialConfig.DoubleButton( + /* sector */ 3, + 0.2f, + ButtonConfig( + GamePadButtonInputId.RightShoulder.ordinal, + "R", + true, + null, + "RightBumper", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // ZR-Trigger + SecondaryDialConfig.DoubleButton( + /* sector */ 3, + 1f, + ButtonConfig( + GamePadButtonInputId.RightTrigger.ordinal, + "ZR", + true, + null, + "RightTrigger", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // R3 separat + SecondaryDialConfig.SingleButton( + /* sector */ 5, + buttonScale, + 1.0f, + ButtonConfig( + GamePadButtonInputId.RightStickButton.ordinal, + "R3", + true, + null, + "RightStickButton", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + ) + ) + } +} diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController3.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController3.kt new file mode 100644 index 000000000..5ad6d2db3 --- /dev/null +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController3.kt @@ -0,0 +1,396 @@ +// src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController3.kt +package org.kenjinx.android + +import android.app.Activity +import android.content.Context +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.math.MathUtils +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import com.swordfish.radialgamepad.library.RadialGamePad +import com.swordfish.radialgamepad.library.config.ButtonConfig +import com.swordfish.radialgamepad.library.config.CrossConfig +import com.swordfish.radialgamepad.library.config.CrossContentDescription +import com.swordfish.radialgamepad.library.config.PrimaryDialConfig +import com.swordfish.radialgamepad.library.config.RadialGamePadConfig +import com.swordfish.radialgamepad.library.config.SecondaryDialConfig +import com.swordfish.radialgamepad.library.event.Event +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.launch +import org.kenjinx.android.viewmodels.MainViewModel +import org.kenjinx.android.viewmodels.QuickSettings + +// --- Dummy IDs to disconnect legacy L3/R3 (stick double tap/tap) --- +private const val DUMMY_LEFT_STICK_PRESS_ID = 10001 +private const val DUMMY_RIGHT_STICK_PRESS_ID = 10002 + +/** + * GameController3 + * Layout 3 – aktuell identisch zum Default-Layout, als eigenständige Klasse. + */ +class GameController3(var activity: Activity) : IGameController { + + companion object { + private fun init(context: Context, controller: GameController3): View { + val inflater = LayoutInflater.from(context) + val parent = FrameLayout(context) + val view = inflater.inflate(R.layout.game_layout, parent, false) + view.findViewById(R.id.leftcontainer)!!.addView(controller.leftGamePad) + view.findViewById(R.id.rightcontainer)!!.addView(controller.rightGamePad) + return view + } + + @Composable + fun Compose(viewModel: MainViewModel) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + val controller = GameController3(viewModel.activity) + val c = init(context, controller) + + viewModel.activity.lifecycleScope.launch { + val events = merge( + controller.leftGamePad.events(), + controller.rightGamePad.events() + ) + events.safeCollect { controller.handleEvent(it) } + } + + controller.controllerView = c + viewModel.setGameController(controller) + controller.setVisible(QuickSettings(viewModel.activity).useVirtualController) + c + } + ) + } + } + + private var controllerView: View? = null + var leftGamePad: RadialGamePad + var rightGamePad: RadialGamePad + var controllerId: Int = -1 + override val isVisible: Boolean + get() = controllerView?.isVisible ?: false + + init { + val useSwitchLayout = QuickSettings(activity).useSwitchLayout + leftGamePad = RadialGamePad(generateConfig3(true, useSwitchLayout), 16f, activity) + rightGamePad = RadialGamePad(generateConfig3(false, useSwitchLayout), 16f, activity) + + leftGamePad.primaryDialMaxSizeDp = 200f + rightGamePad.primaryDialMaxSizeDp = 200f + + leftGamePad.gravityX = -1f + leftGamePad.gravityY = 1f + rightGamePad.gravityX = 1f + rightGamePad.gravityY = 1f + } + + override fun setVisible(isVisible: Boolean) { + controllerView?.apply { + this.isVisible = isVisible + if (isVisible) connect() + } + } + + override fun connect() { + if (controllerId == -1) + controllerId = KenjinxNative.inputConnectGamepad(0) + } + + private fun handleEvent(ev: Event) { + if (controllerId == -1) + controllerId = KenjinxNative.inputConnectGamepad(0) + + controllerId.apply { + when (ev) { + is Event.Button -> { + // Ignore legacy L3/R3 via stick press (double tap) + if (ev.id == DUMMY_LEFT_STICK_PRESS_ID || ev.id == DUMMY_RIGHT_STICK_PRESS_ID) return + when (ev.action) { + KeyEvent.ACTION_UP -> KenjinxNative.inputSetButtonReleased(ev.id, this) + KeyEvent.ACTION_DOWN -> KenjinxNative.inputSetButtonPressed(ev.id, this) + } + } + is Event.Direction -> { + when (ev.id) { + GamePadButtonInputId.DpadUp.ordinal -> { + // Horizontal + if (ev.xAxis > 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadRight.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this) + } else if (ev.xAxis < 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadLeft.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this) + } else { + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this) + } + // Vertical + if (ev.yAxis < 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadUp.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this) + } else if (ev.yAxis > 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadDown.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this) + } else { + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this) + } + } + GamePadButtonInputId.LeftStick.ordinal -> { + val setting = QuickSettings(activity) + val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f) + val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f) + KenjinxNative.inputSetStickAxis(1, x, -y, this) + } + GamePadButtonInputId.RightStick.ordinal -> { + val setting = QuickSettings(activity) + val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f) + val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f) + KenjinxNative.inputSetStickAxis(2, x, -y, this) + } + } + } + } + } + } +} + +private fun generateConfig3(isLeft: Boolean, useSwitchLayout: Boolean): RadialGamePadConfig { + val distance = 0.3f + val buttonScale = 1f + + if (isLeft) { + return RadialGamePadConfig( + /* ringSegments = */ 12, + /* Primary (D-Pad) */ + PrimaryDialConfig.Cross( + CrossConfig( + GamePadButtonInputId.DpadUp.ordinal, + CrossConfig.Shape.STANDARD, + null, + setOf(), + CrossContentDescription(), + true, + null + ), + ), + listOf( + // Left stick + // IMPORTANT: pressButtonId -> DUMMY_LEFT_STICK_PRESS_ID, so that double tap does not trigger L3 + SecondaryDialConfig.Stick( + /* sector */ 10, + /* size */ 3, + /* gap */ 2.7f, + distance, + GamePadButtonInputId.LeftStick.ordinal, + DUMMY_LEFT_STICK_PRESS_ID, + null, + setOf(), + "LeftStick", + SecondaryDialConfig.RotationProcessor() + ), + + // Minus + SecondaryDialConfig.SingleButton( + /* sector */ 9, + buttonScale, + 0.3f, + ButtonConfig( + GamePadButtonInputId.Minus.ordinal, + "-", + true, + null, + "Minus", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // L-Bumper + SecondaryDialConfig.DoubleButton( + /* sector */ 2, + 0.2f, + ButtonConfig( + GamePadButtonInputId.LeftShoulder.ordinal, + "L", + true, + null, + "LeftBumper", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // ZL-Trigger + SecondaryDialConfig.DoubleButton( + /* sector */ 2, + 1f, + ButtonConfig( + GamePadButtonInputId.LeftTrigger.ordinal, + "ZL", + true, + null, + "LeftTrigger", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // L3 separat + SecondaryDialConfig.SingleButton( + /* sector */ 1, + buttonScale, + 1.0f, + ButtonConfig( + GamePadButtonInputId.LeftStickButton.ordinal, + "L3", + true, + null, + "LeftStickButton", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + ) + ) + } else { + return RadialGamePadConfig( + /* ringSegments = */ 12, + /* Primary (ABXY) */ + PrimaryDialConfig.PrimaryButtons( + if (useSwitchLayout) { + // Switch/Nintendo: A=Right, X=Top, Y=Left, B=Bottom + listOf( + ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null), // Right + ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Top + ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Left + ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null) // Bottom + ) + } else { + // Xbox-Stil: B=Right, Y=Top, X=Left, A=Bottom + listOf( + ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null), // Right + ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Top + ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Left + ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null) // Bottom + ) + }, + null, + 0f, + true, + null + ), + listOf( + // Right stick + // IMPORTANT: pressButtonId -> DUMMY_RIGHT_STICK_PRESS_ID, so that double tap does not trigger R3 + SecondaryDialConfig.Stick( + /* sector */ 6, + /* size */ 3, + /* gap */ 2.7f, + distance, + GamePadButtonInputId.RightStick.ordinal, + DUMMY_RIGHT_STICK_PRESS_ID, + null, + setOf(), + "RightStick", + SecondaryDialConfig.RotationProcessor() + ), + + // Plus + SecondaryDialConfig.SingleButton( + /* sector */ 9, + buttonScale, + 0.3f, + ButtonConfig( + GamePadButtonInputId.Plus.ordinal, + "+", + true, + null, + "Plus", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // R-Bumper + SecondaryDialConfig.DoubleButton( + /* sector */ 3, + 0.2f, + ButtonConfig( + GamePadButtonInputId.RightShoulder.ordinal, + "R", + true, + null, + "RightBumper", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // ZR-Trigger + SecondaryDialConfig.DoubleButton( + /* sector */ 3, + 1f, + ButtonConfig( + GamePadButtonInputId.RightTrigger.ordinal, + "ZR", + true, + null, + "RightTrigger", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // R3 separat + SecondaryDialConfig.SingleButton( + /* sector */ 5, + buttonScale, + 1.0f, + ButtonConfig( + GamePadButtonInputId.RightStickButton.ordinal, + "R3", + true, + null, + "RightStickButton", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + ) + ) + } +} diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController4.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController4.kt new file mode 100644 index 000000000..92b7a6707 --- /dev/null +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController4.kt @@ -0,0 +1,393 @@ +// src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController4.kt +package org.kenjinx.android + +import android.app.Activity +import android.content.Context +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.math.MathUtils +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import com.swordfish.radialgamepad.library.RadialGamePad +import com.swordfish.radialgamepad.library.config.ButtonConfig +import com.swordfish.radialgamepad.library.config.CrossConfig +import com.swordfish.radialgamepad.library.config.CrossContentDescription +import com.swordfish.radialgamepad.library.config.PrimaryDialConfig +import com.swordfish.radialgamepad.library.config.RadialGamePadConfig +import com.swordfish.radialgamepad.library.config.SecondaryDialConfig +import com.swordfish.radialgamepad.library.event.Event +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.launch +import org.kenjinx.android.viewmodels.MainViewModel +import org.kenjinx.android.viewmodels.QuickSettings + +private const val DUMMY_LEFT_STICK_PRESS_ID = 10001 +private const val DUMMY_RIGHT_STICK_PRESS_ID = 10002 + +/** + * GameController4 + * Layout 4 – aktuell identisch zum Default-Layout, als eigenständige Klasse. + */ +class GameController4(var activity: Activity) : IGameController { + + companion object { + private fun init(context: Context, controller: GameController4): View { + val inflater = LayoutInflater.from(context) + val parent = FrameLayout(context) + val view = inflater.inflate(R.layout.game_layout, parent, false) + view.findViewById(R.id.leftcontainer)!!.addView(controller.leftGamePad) + view.findViewById(R.id.rightcontainer)!!.addView(controller.rightGamePad) + return view + } + + @Composable + fun Compose(viewModel: MainViewModel) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + val controller = GameController4(viewModel.activity) + val c = init(context, controller) + + viewModel.activity.lifecycleScope.launch { + val events = merge( + controller.leftGamePad.events(), + controller.rightGamePad.events() + ) + events.safeCollect { controller.handleEvent(it) } + } + + controller.controllerView = c + viewModel.setGameController(controller) + controller.setVisible(QuickSettings(viewModel.activity).useVirtualController) + c + } + ) + } + } + + private var controllerView: View? = null + var leftGamePad: RadialGamePad + var rightGamePad: RadialGamePad + var controllerId: Int = -1 + override val isVisible: Boolean + get() = controllerView?.isVisible ?: false + + init { + val useSwitchLayout = QuickSettings(activity).useSwitchLayout + leftGamePad = RadialGamePad(generateConfig4(true, useSwitchLayout), 16f, activity) + rightGamePad = RadialGamePad(generateConfig4(false, useSwitchLayout), 16f, activity) + + leftGamePad.primaryDialMaxSizeDp = 200f + rightGamePad.primaryDialMaxSizeDp = 200f + + leftGamePad.gravityX = -1f + leftGamePad.gravityY = 1f + rightGamePad.gravityX = 1f + rightGamePad.gravityY = 1f + } + + override fun setVisible(isVisible: Boolean) { + controllerView?.apply { + this.isVisible = isVisible + if (isVisible) connect() + } + } + + override fun connect() { + if (controllerId == -1) + controllerId = KenjinxNative.inputConnectGamepad(0) + } + + private fun handleEvent(ev: Event) { + if (controllerId == -1) + controllerId = KenjinxNative.inputConnectGamepad(0) + + controllerId.apply { + when (ev) { + is Event.Button -> { + if (ev.id == DUMMY_LEFT_STICK_PRESS_ID || ev.id == DUMMY_RIGHT_STICK_PRESS_ID) return + when (ev.action) { + KeyEvent.ACTION_UP -> KenjinxNative.inputSetButtonReleased(ev.id, this) + KeyEvent.ACTION_DOWN -> KenjinxNative.inputSetButtonPressed(ev.id, this) + } + } + is Event.Direction -> { + when (ev.id) { + GamePadButtonInputId.DpadUp.ordinal -> { + if (ev.xAxis > 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadRight.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this) + } else if (ev.xAxis < 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadLeft.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this) + } else { + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this) + } + if (ev.yAxis < 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadUp.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this) + } else if (ev.yAxis > 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadDown.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this) + } else { + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this) + } + } + GamePadButtonInputId.LeftStick.ordinal -> { + val setting = QuickSettings(activity) + val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f) + val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f) + KenjinxNative.inputSetStickAxis(1, x, -y, this) + } + GamePadButtonInputId.RightStick.ordinal -> { + val setting = QuickSettings(activity) + val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f) + val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f) + KenjinxNative.inputSetStickAxis(2, x, -y, this) + } + } + } + } + } + } +} + +private fun generateConfig4(isLeft: Boolean, useSwitchLayout: Boolean): RadialGamePadConfig { + val distance = 0.3f + val buttonScale = 1f + + if (isLeft) { + return RadialGamePadConfig( + /* ringSegments = */ 12, + /* Primary (Stick) */ + // IMPORTANT: pressButtonId -> DUMMY_LEFT_STICK_PRESS_ID, so that double tap does not trigger L3 + PrimaryDialConfig.Stick( + GamePadButtonInputId.LeftStick.ordinal, + DUMMY_LEFT_STICK_PRESS_ID, + setOf(), + "LeftStick", + null + ), + listOf( + // D-Pad + SecondaryDialConfig.Cross( + /* sector */ 10, + /* size */ 3, + /* gap */ 2.1f, + distance, + CrossConfig( + GamePadButtonInputId.DpadUp.ordinal, + CrossConfig.Shape.STANDARD, + null, + setOf(), + CrossContentDescription(), + true, + null + ), + SecondaryDialConfig.RotationProcessor() + ), + + // Minus + SecondaryDialConfig.SingleButton( + /* sector */ 9, + buttonScale, + 0.3f, + ButtonConfig( + GamePadButtonInputId.Minus.ordinal, + "-", + true, + null, + "Minus", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // L-Bumper + SecondaryDialConfig.DoubleButton( + /* sector */ 2, + 1.2f, + ButtonConfig( + GamePadButtonInputId.LeftShoulder.ordinal, + "L", + true, + null, + "LeftBumper", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // ZL-Trigger + SecondaryDialConfig.DoubleButton( + /* sector */ 2, + 2f, + ButtonConfig( + GamePadButtonInputId.LeftTrigger.ordinal, + "ZL", + true, + null, + "LeftTrigger", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + ) + ) + } else { + return RadialGamePadConfig( + /* ringSegments = */ 12, + /* Primary (ABXY) */ + PrimaryDialConfig.PrimaryButtons( + if (useSwitchLayout) { + // Switch/Nintendo: A=Right, X=Top, Y=Left, B=Bottom + listOf( + ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null), // Right + ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Top + ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Left + ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null) // Bottom + ) + } else { + // Xbox-Stil: B=Right, Y=Top, X=Left, A=Bottom + listOf( + ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null), // Right + ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Top + ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Left + ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null) // Bottom + ) + }, + null, + 0f, + true, + null + ), + listOf( + // Right stick + // IMPORTANT: pressButtonId -> DUMMY_RIGHT_STICK_PRESS_ID, so that double tap does not trigger R3 + SecondaryDialConfig.Stick( + /* sector */ 6, + /* size */ 3, + /* gap */ 2.7f, + distance, + GamePadButtonInputId.RightStick.ordinal, + DUMMY_RIGHT_STICK_PRESS_ID, + null, + setOf(), + "RightStick", + SecondaryDialConfig.RotationProcessor() + ), + + // Plus + SecondaryDialConfig.SingleButton( + /* sector */ 9, + buttonScale, + 0.3f, + ButtonConfig( + GamePadButtonInputId.Plus.ordinal, + "+", + true, + null, + "Plus", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // R-Bumper + SecondaryDialConfig.DoubleButton( + /* sector */ 3, + 1.2f, + ButtonConfig( + GamePadButtonInputId.RightShoulder.ordinal, + "R", + true, + null, + "RightBumper", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // ZR-Trigger + SecondaryDialConfig.DoubleButton( + /* sector */ 3, + 2f, + ButtonConfig( + GamePadButtonInputId.RightTrigger.ordinal, + "ZR", + true, + null, + "RightTrigger", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // R3 separat + SecondaryDialConfig.SingleButton( + /* sector */ 3, + buttonScale, + 0f, + ButtonConfig( + GamePadButtonInputId.RightStickButton.ordinal, + "R3", + true, + null, + "RightStickButton", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // L3 separat + SecondaryDialConfig.SingleButton( + /* sector */ 4, + buttonScale, + 0f, + ButtonConfig( + GamePadButtonInputId.LeftStickButton.ordinal, + "L3", + true, + null, + "LeftStickButton", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + ) + ) + } +} diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController5.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController5.kt new file mode 100644 index 000000000..2a78851e7 --- /dev/null +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController5.kt @@ -0,0 +1,396 @@ +package org.kenjinx.android + +import android.app.Activity +import android.content.Context +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.math.MathUtils +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import com.swordfish.radialgamepad.library.RadialGamePad +import com.swordfish.radialgamepad.library.config.ButtonConfig +import com.swordfish.radialgamepad.library.config.CrossConfig +import com.swordfish.radialgamepad.library.config.CrossContentDescription +import com.swordfish.radialgamepad.library.config.PrimaryDialConfig +import com.swordfish.radialgamepad.library.config.RadialGamePadConfig +import com.swordfish.radialgamepad.library.config.SecondaryDialConfig +import com.swordfish.radialgamepad.library.event.Event +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.launch +import org.kenjinx.android.viewmodels.MainViewModel +import org.kenjinx.android.viewmodels.QuickSettings + +private const val DUMMY_LEFT_STICK_PRESS_ID = 10001 +private const val DUMMY_RIGHT_STICK_PRESS_ID = 10002 + +/** + * GameController5 + * Layout 5 – aktuell identisch zum Default-Layout, als eigenständige Klasse. + */ +class GameController5(var activity: Activity) : IGameController { + + companion object { + private fun init(context: Context, controller: GameController5): View { + val inflater = LayoutInflater.from(context) + val parent = FrameLayout(context) + val view = inflater.inflate(R.layout.game_layout, parent, false) + view.findViewById(R.id.leftcontainer)!!.addView(controller.leftGamePad) + view.findViewById(R.id.rightcontainer)!!.addView(controller.rightGamePad) + return view + } + + @Composable + fun Compose(viewModel: MainViewModel) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + val controller = GameController5(viewModel.activity) + val c = init(context, controller) + + viewModel.activity.lifecycleScope.launch { + val events = merge( + controller.leftGamePad.events(), + controller.rightGamePad.events() + ) + events.safeCollect { controller.handleEvent(it) } + } + + controller.controllerView = c + viewModel.setGameController(controller) + controller.setVisible(QuickSettings(viewModel.activity).useVirtualController) + c + } + ) + } + } + + private var controllerView: View? = null + var leftGamePad: RadialGamePad + var rightGamePad: RadialGamePad + var controllerId: Int = -1 + override val isVisible: Boolean + get() = controllerView?.isVisible ?: false + + init { + val useSwitchLayout = QuickSettings(activity).useSwitchLayout + leftGamePad = RadialGamePad(generateConfig5(true, useSwitchLayout), 16f, activity) + rightGamePad = RadialGamePad(generateConfig5(false, useSwitchLayout), 16f, activity) + + leftGamePad.primaryDialMaxSizeDp = 200f + rightGamePad.primaryDialMaxSizeDp = 200f + + leftGamePad.gravityX = -1f + leftGamePad.gravityY = 1f + rightGamePad.gravityX = 1f + rightGamePad.gravityY = 1f + } + + override fun setVisible(isVisible: Boolean) { + controllerView?.apply { + this.isVisible = isVisible + if (isVisible) connect() + } + } + + override fun connect() { + if (controllerId == -1) + controllerId = KenjinxNative.inputConnectGamepad(0) + } + + private fun handleEvent(ev: Event) { + if (controllerId == -1) + controllerId = KenjinxNative.inputConnectGamepad(0) + + controllerId.apply { + when (ev) { + is Event.Button -> { + if (ev.id == DUMMY_LEFT_STICK_PRESS_ID || ev.id == DUMMY_RIGHT_STICK_PRESS_ID) return + when (ev.action) { + KeyEvent.ACTION_UP -> KenjinxNative.inputSetButtonReleased(ev.id, this) + KeyEvent.ACTION_DOWN -> KenjinxNative.inputSetButtonPressed(ev.id, this) + } + } + is Event.Direction -> { + when (ev.id) { + GamePadButtonInputId.DpadUp.ordinal -> { + if (ev.xAxis > 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadRight.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this) + } else if (ev.xAxis < 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadLeft.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this) + } else { + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this) + } + if (ev.yAxis < 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadUp.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this) + } else if (ev.yAxis > 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadDown.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this) + } else { + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this) + } + } + GamePadButtonInputId.LeftStick.ordinal -> { + val setting = QuickSettings(activity) + val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f) + val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f) + KenjinxNative.inputSetStickAxis(1, x, -y, this) + } + GamePadButtonInputId.RightStick.ordinal -> { + val setting = QuickSettings(activity) + val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f) + val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f) + KenjinxNative.inputSetStickAxis(2, x, -y, this) + } + } + } + } + } + } +} + +private fun generateConfig5(isLeft: Boolean, useSwitchLayout: Boolean): RadialGamePadConfig { + val distance = 0.3f + val buttonScale = 1f + + if (isLeft) { + return RadialGamePadConfig( + /* ringSegments = */ 12, + /* Primary (Stick) */ + // IMPORTANT: pressButtonId -> DUMMY_LEFT_STICK_PRESS_ID, so that double tap does not trigger L3 + PrimaryDialConfig.Stick( + GamePadButtonInputId.LeftStick.ordinal, + DUMMY_LEFT_STICK_PRESS_ID, + setOf(), + "LeftStick", + null + ), + listOf( + // D-Pad + SecondaryDialConfig.Cross( + /* sector */ 10, + /* size */ 3, + /* gap */ 2.1f, + distance, + CrossConfig( + GamePadButtonInputId.DpadUp.ordinal, + CrossConfig.Shape.STANDARD, + null, + setOf(), + CrossContentDescription(), + true, + null + ), + SecondaryDialConfig.RotationProcessor() + ), + + // Minus + SecondaryDialConfig.SingleButton( + /* sector */ 9, + buttonScale, + 0.3f, + ButtonConfig( + GamePadButtonInputId.Minus.ordinal, + "-", + true, + null, + "Minus", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // L-Bumper + SecondaryDialConfig.SingleButton( + /* sector */ 3, + 1.5f, + 1.0f, + ButtonConfig( + GamePadButtonInputId.LeftShoulder.ordinal, + "L", + true, + null, + "LeftBumper", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // ZL-Trigger + SecondaryDialConfig.SingleButton( + /* sector */ 2, + 1.5f, + 1.1f, + ButtonConfig( + GamePadButtonInputId.LeftTrigger.ordinal, + "ZL", + true, + null, + "LeftTrigger", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + ) + ) + } else { + return RadialGamePadConfig( + /* ringSegments = */ 12, + /* Primary (ABXY) */ + PrimaryDialConfig.PrimaryButtons( + if (useSwitchLayout) { + // Switch/Nintendo: A=Right, X=Top, Y=Left, B=Bottom + listOf( + ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null), // Right + ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Top + ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Left + ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null) // Bottom + ) + } else { + // Xbox-Stil: B=Right, Y=Top, X=Left, A=Bottom + listOf( + ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null), // Right + ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Top + ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Left + ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null) // Bottom + ) + }, + null, + 0f, + true, + null + ), + listOf( + // Right stick + // IMPORTANT: pressButtonId -> DUMMY_RIGHT_STICK_PRESS_ID, so that double tap does not trigger R3 + SecondaryDialConfig.Stick( + /* sector */ 6, + /* size */ 3, + /* gap */ 2.7f, + distance, + GamePadButtonInputId.RightStick.ordinal, + DUMMY_RIGHT_STICK_PRESS_ID, + null, + setOf(), + "RightStick", + SecondaryDialConfig.RotationProcessor() + ), + + // Plus + SecondaryDialConfig.SingleButton( + /* sector */ 9, + buttonScale, + 0.3f, + ButtonConfig( + GamePadButtonInputId.Plus.ordinal, + "+", + true, + null, + "Plus", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // R-Bumper + SecondaryDialConfig.SingleButton( + /* sector */ 3, + 1.5f, + 1.0f, + ButtonConfig( + GamePadButtonInputId.RightShoulder.ordinal, + "R", + true, + null, + "RightBumper", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // ZR-Trigger + SecondaryDialConfig.SingleButton( + /* sector */ 4, + 1.5f, + 1.1f, + ButtonConfig( + GamePadButtonInputId.RightTrigger.ordinal, + "ZR", + true, + null, + "RightTrigger", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // R3 separat + SecondaryDialConfig.SingleButton( + /* sector */ 3, + 1.1f, + 0f, + ButtonConfig( + GamePadButtonInputId.RightStickButton.ordinal, + "R3", + true, + null, + "RightStickButton", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // L3 separat + SecondaryDialConfig.SingleButton( + /* sector */ 4, + 1.1f, + 0f, + ButtonConfig( + GamePadButtonInputId.LeftStickButton.ordinal, + "L3", + true, + null, + "LeftStickButton", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + ) + ) + } +} diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController6.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController6.kt new file mode 100644 index 000000000..fd5054592 --- /dev/null +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController6.kt @@ -0,0 +1,393 @@ +package org.kenjinx.android + +import android.app.Activity +import android.content.Context +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.math.MathUtils +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import com.swordfish.radialgamepad.library.RadialGamePad +import com.swordfish.radialgamepad.library.config.ButtonConfig +import com.swordfish.radialgamepad.library.config.CrossConfig +import com.swordfish.radialgamepad.library.config.CrossContentDescription +import com.swordfish.radialgamepad.library.config.PrimaryDialConfig +import com.swordfish.radialgamepad.library.config.RadialGamePadConfig +import com.swordfish.radialgamepad.library.config.SecondaryDialConfig +import com.swordfish.radialgamepad.library.event.Event +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.launch +import org.kenjinx.android.viewmodels.MainViewModel +import org.kenjinx.android.viewmodels.QuickSettings + +private const val DUMMY_LEFT_STICK_PRESS_ID = 10001 +private const val DUMMY_RIGHT_STICK_PRESS_ID = 10002 + +/** + * GameController6 + * Layout 6 – aktuell identisch zum Default-Layout, als eigenständige Klasse. + */ +class GameController6(var activity: Activity) : IGameController { + + companion object { + private fun init(context: Context, controller: GameController6): View { + val inflater = LayoutInflater.from(context) + val parent = FrameLayout(context) + val view = inflater.inflate(R.layout.game_layout, parent, false) + view.findViewById(R.id.leftcontainer)!!.addView(controller.leftGamePad) + view.findViewById(R.id.rightcontainer)!!.addView(controller.rightGamePad) + return view + } + + @Composable + fun Compose(viewModel: MainViewModel) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + val controller = GameController6(viewModel.activity) + val c = init(context, controller) + + viewModel.activity.lifecycleScope.launch { + val events = merge( + controller.leftGamePad.events(), + controller.rightGamePad.events() + ) + events.safeCollect { controller.handleEvent(it) } + } + + controller.controllerView = c + viewModel.setGameController(controller) + controller.setVisible(QuickSettings(viewModel.activity).useVirtualController) + c + } + ) + } + } + + private var controllerView: View? = null + var leftGamePad: RadialGamePad + var rightGamePad: RadialGamePad + var controllerId: Int = -1 + override val isVisible: Boolean + get() = controllerView?.isVisible ?: false + + init { + val useSwitchLayout = QuickSettings(activity).useSwitchLayout + leftGamePad = RadialGamePad(generateConfig6(true, useSwitchLayout), 16f, activity) + rightGamePad = RadialGamePad(generateConfig6(false, useSwitchLayout), 16f, activity) + + leftGamePad.primaryDialMaxSizeDp = 200f + rightGamePad.primaryDialMaxSizeDp = 200f + + leftGamePad.gravityX = -1f + leftGamePad.gravityY = 1f + rightGamePad.gravityX = 1f + rightGamePad.gravityY = 1f + } + + override fun setVisible(isVisible: Boolean) { + controllerView?.apply { + this.isVisible = isVisible + if (isVisible) connect() + } + } + + override fun connect() { + if (controllerId == -1) + controllerId = KenjinxNative.inputConnectGamepad(0) + } + + private fun handleEvent(ev: Event) { + if (controllerId == -1) + controllerId = KenjinxNative.inputConnectGamepad(0) + + controllerId.apply { + when (ev) { + is Event.Button -> { + if (ev.id == DUMMY_LEFT_STICK_PRESS_ID || ev.id == DUMMY_RIGHT_STICK_PRESS_ID) return + when (ev.action) { + KeyEvent.ACTION_UP -> KenjinxNative.inputSetButtonReleased(ev.id, this) + KeyEvent.ACTION_DOWN -> KenjinxNative.inputSetButtonPressed(ev.id, this) + } + } + is Event.Direction -> { + when (ev.id) { + GamePadButtonInputId.DpadUp.ordinal -> { + if (ev.xAxis > 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadRight.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this) + } else if (ev.xAxis < 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadLeft.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this) + } else { + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this) + } + if (ev.yAxis < 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadUp.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this) + } else if (ev.yAxis > 0) { + KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadDown.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this) + } else { + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this) + KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this) + } + } + GamePadButtonInputId.LeftStick.ordinal -> { + val setting = QuickSettings(activity) + val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f) + val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f) + KenjinxNative.inputSetStickAxis(1, x, -y, this) + } + GamePadButtonInputId.RightStick.ordinal -> { + val setting = QuickSettings(activity) + val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f) + val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f) + KenjinxNative.inputSetStickAxis(2, x, -y, this) + } + } + } + } + } + } +} + +private fun generateConfig6(isLeft: Boolean, useSwitchLayout: Boolean): GamePadConfig { + val distance = 0.3f + val buttonScale = 1f + + if (isLeft) { + return GamePadConfig( + /* ringSegments = */ 12, + /* Primary (Stick) */ + // IMPORTANT: pressButtonId -> DUMMY_LEFT_STICK_PRESS_ID, so that double tap does not trigger L3 + PrimaryDialConfig.Stick( + GamePadButtonInputId.LeftStick.ordinal, + DUMMY_LEFT_STICK_PRESS_ID, + setOf(), + "LeftStick", + null + ), + listOf( + // D-Pad + SecondaryDialConfig.Cross( + /* sector */ 10, + /* size */ 3, + /* gap */ 2.5f, + distance, + CrossConfig( + GamePadButtonInputId.DpadUp.ordinal, + CrossConfig.Shape.STANDARD, + null, + setOf(), + CrossContentDescription(), + true, + null + ), + SecondaryDialConfig.RotationProcessor() + ), + + // Minus + SecondaryDialConfig.SingleButton( + /* sector */ 1, + buttonScale, + 2f, + ButtonConfig( + GamePadButtonInputId.Minus.ordinal, + "-", + true, + null, + "Minus", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // L-Bumper + SecondaryDialConfig.DoubleButton( + /* sector */ 2, + distance, + ButtonConfig( + GamePadButtonInputId.LeftShoulder.ordinal, + "L", + true, + null, + "LeftBumper", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // ZL-Trigger + SecondaryDialConfig.SingleButton( + /* sector */ 9, + buttonScale, + distance, + ButtonConfig( + GamePadButtonInputId.LeftTrigger.ordinal, + "ZL", + true, + null, + "LeftTrigger", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + ) + ) + } else { + return GamePadConfig( + /* ringSegments = */ 12, + /* Primary (ABXY) */ + PrimaryDialConfig.PrimaryButtons( + if (useSwitchLayout) { + // Switch/Nintendo: A=Right, X=Top, Y=Left, B=Bottom + listOf( + ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null), // Right + ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Top + ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Left + ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null) // Bottom + ) + } else { + // Xbox-Stil: B=Right, Y=Top, X=Left, A=Bottom + listOf( + ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null), // Right + ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Top + ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Left + ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null) // Bottom + ) + }, + null, + 0f, + true, + null + ), + listOf( + // Right stick (unchanged) + // IMPORTANT: pressButtonId -> DUMMY_RIGHT_STICK_PRESS_ID, so that double tap does not trigger R3 + SecondaryDialConfig.Stick( + /* sector */ 6, + /* size */ 3, + /* gap */ 2.5f, + 0.7f, + GamePadButtonInputId.RightStick.ordinal, + DUMMY_RIGHT_STICK_PRESS_ID, + null, + setOf(), + "RightStick", + SecondaryDialConfig.RotationProcessor() + ), + + // Plus + SecondaryDialConfig.SingleButton( + /* sector */ 5, + buttonScale, + 2f, + ButtonConfig( + GamePadButtonInputId.Plus.ordinal, + "+", + true, + null, + "Plus", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // R-Bumper + SecondaryDialConfig.DoubleButton( + /* sector */ 3, + distance, + ButtonConfig( + GamePadButtonInputId.RightShoulder.ordinal, + "R", + true, + null, + "RightBumper", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // ZR-Trigger + SecondaryDialConfig.SingleButton( + /* sector */ 9, + buttonScale, + distance, + ButtonConfig( + GamePadButtonInputId.RightTrigger.ordinal, + "ZR", + true, + null, + "RightTrigger", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + + // NEW (remains): R3 as a separate button + SecondaryDialConfig.SingleButton( + /* sector */ 5, + buttonScale, + 0.0f, + ButtonConfig( + GamePadButtonInputId.RightStickButton.ordinal, + "R3", + true, + null, + "RightStickButton", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + // NEW (remains): L3 as a separate button + SecondaryDialConfig.SingleButton( + /* sector */ 6, + buttonScale, + 0.0f, + ButtonConfig( + GamePadButtonInputId.LeftStickButton.ordinal, + "L3", + true, + null, + "LeftStickButton", + setOf(), + true, + null + ), + null, + SecondaryDialConfig.RotationProcessor() + ), + ) + ) + } +} diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/IGameController.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/IGameController.kt new file mode 100644 index 000000000..9bc245a74 --- /dev/null +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/IGameController.kt @@ -0,0 +1,7 @@ +package org.kenjinx.android + +interface IGameController { + val isVisible: Boolean + fun setVisible(isVisible: Boolean) + fun connect() +} diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/viewmodels/MainViewModel.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/viewmodels/MainViewModel.kt index d7bcb18fa..143ae4d8b 100644 --- a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/viewmodels/MainViewModel.kt +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/viewmodels/MainViewModel.kt @@ -8,7 +8,7 @@ import androidx.preference.PreferenceManager import com.anggrayudi.storage.extension.launchOnUiThread import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Semaphore -import org.kenjinx.android.GameController +import org.kenjinx.android.IGameController import org.kenjinx.android.GameHost import org.kenjinx.android.Logging import org.kenjinx.android.MainActivity @@ -30,7 +30,7 @@ class MainViewModel(val activity: MainActivity) { var physicalControllerManager: PhysicalControllerManager? = null var motionSensorManager: MotionSensorManager? = null var gameModel: GameModel? = null - var controller: GameController? = null + var controller: IGameController? = null var performanceManager: PerformanceManager? = null var selected: GameModel? = null val loadGameModel: MutableState = mutableStateOf(null) @@ -422,7 +422,7 @@ class MainViewModel(val activity: MainActivity) { frequenciesState?.let { PerformanceMonitor.getFrequencies(it) } } - fun setGameController(controller: GameController) { + fun setGameController(controller: IGameController) { this.controller = controller } diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/viewmodels/QuickSettings.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/viewmodels/QuickSettings.kt index 762407ecb..e06a1c000 100644 --- a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/viewmodels/QuickSettings.kt +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/viewmodels/QuickSettings.kt @@ -24,6 +24,13 @@ class QuickSettings(val activity: Activity) { BottomMiddle, BottomLeft, BottomRight, TopMiddle, TopLeft, TopRight } + // --- Virtual Controller Preset + enum class VirtualControllerPreset { + Default, Layout2, Layout3, Layout4, Layout5, Layout6 + } + + var virtualControllerPreset: VirtualControllerPreset + var orientationPreference: OrientationPreference // --- Overlay Settings @@ -94,6 +101,9 @@ class QuickSettings(val activity: Activity) { resScale = sharedPref.getFloat("resScale", 1f) maxAnisotropy = sharedPref.getFloat("maxAnisotropy", 0f) useVirtualController = sharedPref.getBoolean("useVirtualController", true) + virtualControllerPreset = VirtualControllerPreset.entries[ + sharedPref.getInt("virtualControllerPreset", VirtualControllerPreset.Default.ordinal) + ] isGrid = sharedPref.getBoolean("isGrid", true) useSwitchLayout = sharedPref.getBoolean("useSwitchLayout", true) enableMotion = sharedPref.getBoolean("enableMotion", true) @@ -151,6 +161,7 @@ class QuickSettings(val activity: Activity) { putBoolean("enableTraceLogs", enableTraceLogs) putBoolean("enableDebugLogs", enableDebugLogs) putBoolean("enableGraphicsLogs", enableGraphicsLogs) + putInt("virtualControllerPreset", virtualControllerPreset.ordinal) } } diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/views/GameViews.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/views/GameViews.kt index 8ae382828..6e50a46be 100644 --- a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/views/GameViews.kt +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/views/GameViews.kt @@ -38,6 +38,11 @@ import androidx.compose.ui.draw.alpha import compose.icons.CssGgIcons import compose.icons.cssggicons.ToolbarBottom import org.kenjinx.android.GameController +import org.kenjinx.android.GameController2 +import org.kenjinx.android.GameController3 +import org.kenjinx.android.GameController4 +import org.kenjinx.android.GameController5 +import org.kenjinx.android.GameController6 import org.kenjinx.android.GameHost import org.kenjinx.android.Icons import org.kenjinx.android.MainActivity @@ -48,6 +53,10 @@ import org.kenjinx.android.viewmodels.VSyncMode import org.kenjinx.android.widgets.SimpleAlertDialog import java.util.Locale import kotlin.math.roundToInt +import android.net.Uri +import android.widget.Toast +import org.kenjinx.android.viewmodels.QuickSettings.VirtualControllerPreset + class GameViews { companion object { @@ -155,7 +164,17 @@ class GameViews { } if (!showLoading.value) { - GameController.Compose(mainViewModel) + // Aktuelles Preset aus QuickSettings holen (bei jeder Recomposition neu – so greift auch ein Wechsel nach dem Speichern) + val preset = QuickSettings(mainViewModel.activity).virtualControllerPreset + + when (preset) { + VirtualControllerPreset.Default -> GameController.Compose(mainViewModel) + VirtualControllerPreset.Layout2 -> GameController2.Compose(mainViewModel) + VirtualControllerPreset.Layout3 -> GameController3.Compose(mainViewModel) + VirtualControllerPreset.Layout4 -> GameController4.Compose(mainViewModel) + VirtualControllerPreset.Layout5 -> GameController5.Compose(mainViewModel) + VirtualControllerPreset.Layout6 -> GameController6.Compose(mainViewModel) + } // --- Button at any corner/edge + transparency Row( diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/views/SettingViews.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/views/SettingViews.kt index 2d0f4f565..03039349b 100644 --- a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/views/SettingViews.kt +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/views/SettingViews.kt @@ -89,6 +89,8 @@ import org.kenjinx.android.viewmodels.QuickSettings.OverlayMenuPosition // ← N import org.kenjinx.android.SystemLanguage import org.kenjinx.android.RegionCode +import org.kenjinx.android.viewmodels.QuickSettings.VirtualControllerPreset + class SettingViews { companion object { const val EXPANSTION_TRANSITION_DURATION = 450 @@ -129,6 +131,9 @@ class SettingViews { val isGrid = remember { mutableStateOf(true) } val useSwitchLayout = remember { mutableStateOf(true) } val enableMotion = remember { mutableStateOf(true) } + val vcPreset = remember { + mutableStateOf(QuickSettings(mainViewModel.activity).virtualControllerPreset) + } val enablePerformanceMode = remember { mutableStateOf(true) } val controllerStickSensitivity = remember { mutableFloatStateOf(1.0f) } val enableStubLogs = remember { mutableStateOf(true) } @@ -1224,6 +1229,17 @@ class SettingViews { Column(modifier = Modifier.fillMaxWidth()) { useVirtualController.SwitchSelector(label = "Use Virtual Controller") useSwitchLayout.SwitchSelector(label = "Use Switch Controller Layout") + if (useVirtualController.value) { + VirtualControllerPresetDropdown( + selectedPreset = vcPreset.value, + onPresetSelected = { preset -> + vcPreset.value = preset + val qs = QuickSettings(mainViewModel.activity) + qs.virtualControllerPreset = preset + qs.save() // sofort persistieren, wie bei Overlay-Settings + } + ) + } val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } @@ -1503,8 +1519,6 @@ class SettingViews { ) } - // ---- Existing dropdowns ---- - // ---- Dropdown for orientation ---- @Composable fun OrientationDropdown( @@ -1532,7 +1546,38 @@ class SettingViews { ) } - // ---- Existing dropdowns ---- + @Composable + fun VirtualControllerPresetDropdown( + selectedPreset: VirtualControllerPreset, + onPresetSelected: (VirtualControllerPreset) -> Unit + ) { + val options = listOf( + VirtualControllerPreset.Default, + VirtualControllerPreset.Layout2, + VirtualControllerPreset.Layout3, + VirtualControllerPreset.Layout4, + VirtualControllerPreset.Layout5, + VirtualControllerPreset.Layout6 + ) + + DropdownSelector( + label = "Controller Layout", + selectedValue = selectedPreset, + options = options, + getDisplayText = { opt -> + when (opt) { + VirtualControllerPreset.Default -> "Default" + VirtualControllerPreset.Layout2 -> "Layout 2" + VirtualControllerPreset.Layout3 -> "Layout 3" + VirtualControllerPreset.Layout4 -> "Layout 4" + VirtualControllerPreset.Layout5 -> "Layout 5" + VirtualControllerPreset.Layout6 -> "Layout 6" + } + }, + onOptionSelected = onPresetSelected + ) + } + @Composable fun MemoryModeDropdown(