diff options
| author | 2024-02-16 21:19:17 -0500 | |
|---|---|---|
| committer | 2024-02-17 12:32:33 -0500 | |
| commit | 50ecad547ea7e88301583f17c9f1eea2cc75b0af (patch) | |
| tree | 21e6a6669ea19d05b389b097c0d411654aba4fbf /src | |
| parent | hid_core: Prevent crash if we try to iterate through empty color devices list (diff) | |
| download | yuzu-50ecad547ea7e88301583f17c9f1eea2cc75b0af.tar.gz yuzu-50ecad547ea7e88301583f17c9f1eea2cc75b0af.tar.xz yuzu-50ecad547ea7e88301583f17c9f1eea2cc75b0af.zip | |
android: Input mapping
Diffstat (limited to 'src')
83 files changed, 5706 insertions, 989 deletions
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index 7890b30ca..b037fc055 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml | |||
| @@ -14,6 +14,7 @@ SPDX-License-Identifier: GPL-3.0-or-later | |||
| 14 | <uses-permission android:name="android.permission.INTERNET" /> | 14 | <uses-permission android:name="android.permission.INTERNET" /> |
| 15 | <uses-permission android:name="android.permission.NFC" /> | 15 | <uses-permission android:name="android.permission.NFC" /> |
| 16 | <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> | 16 | <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> |
| 17 | <uses-permission android:name="android.permission.VIBRATE" /> | ||
| 17 | 18 | ||
| 18 | <application | 19 | <application |
| 19 | android:name="org.yuzu.yuzu_emu.YuzuApplication" | 20 | android:name="org.yuzu.yuzu_emu.YuzuApplication" |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index 6ebb46af7..fd229c855 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt | |||
| @@ -30,34 +30,6 @@ import org.yuzu.yuzu_emu.model.GameVerificationResult | |||
| 30 | * with the native side of the Yuzu code. | 30 | * with the native side of the Yuzu code. |
| 31 | */ | 31 | */ |
| 32 | object NativeLibrary { | 32 | object NativeLibrary { |
| 33 | /** | ||
| 34 | * Default controller id for each device | ||
| 35 | */ | ||
| 36 | const val Player1Device = 0 | ||
| 37 | const val Player2Device = 1 | ||
| 38 | const val Player3Device = 2 | ||
| 39 | const val Player4Device = 3 | ||
| 40 | const val Player5Device = 4 | ||
| 41 | const val Player6Device = 5 | ||
| 42 | const val Player7Device = 6 | ||
| 43 | const val Player8Device = 7 | ||
| 44 | const val ConsoleDevice = 8 | ||
| 45 | |||
| 46 | /** | ||
| 47 | * Controller type for each device | ||
| 48 | */ | ||
| 49 | const val ProController = 3 | ||
| 50 | const val Handheld = 4 | ||
| 51 | const val JoyconDual = 5 | ||
| 52 | const val JoyconLeft = 6 | ||
| 53 | const val JoyconRight = 7 | ||
| 54 | const val GameCube = 8 | ||
| 55 | const val Pokeball = 9 | ||
| 56 | const val NES = 10 | ||
| 57 | const val SNES = 11 | ||
| 58 | const val N64 = 12 | ||
| 59 | const val SegaGenesis = 13 | ||
| 60 | |||
| 61 | @JvmField | 33 | @JvmField |
| 62 | var sEmulationActivity = WeakReference<EmulationActivity?>(null) | 34 | var sEmulationActivity = WeakReference<EmulationActivity?>(null) |
| 63 | 35 | ||
| @@ -127,112 +99,6 @@ object NativeLibrary { | |||
| 127 | FileUtil.getFilename(Uri.parse(path)) | 99 | FileUtil.getFilename(Uri.parse(path)) |
| 128 | } | 100 | } |
| 129 | 101 | ||
| 130 | /** | ||
| 131 | * Returns true if pro controller isn't available and handheld is | ||
| 132 | */ | ||
| 133 | external fun isHandheldOnly(): Boolean | ||
| 134 | |||
| 135 | /** | ||
| 136 | * Changes controller type for a specific device. | ||
| 137 | * | ||
| 138 | * @param Device The input descriptor of the gamepad. | ||
| 139 | * @param Type The NpadStyleIndex of the gamepad. | ||
| 140 | */ | ||
| 141 | external fun setDeviceType(Device: Int, Type: Int): Boolean | ||
| 142 | |||
| 143 | /** | ||
| 144 | * Handles event when a gamepad is connected. | ||
| 145 | * | ||
| 146 | * @param Device The input descriptor of the gamepad. | ||
| 147 | */ | ||
| 148 | external fun onGamePadConnectEvent(Device: Int): Boolean | ||
| 149 | |||
| 150 | /** | ||
| 151 | * Handles event when a gamepad is disconnected. | ||
| 152 | * | ||
| 153 | * @param Device The input descriptor of the gamepad. | ||
| 154 | */ | ||
| 155 | external fun onGamePadDisconnectEvent(Device: Int): Boolean | ||
| 156 | |||
| 157 | /** | ||
| 158 | * Handles button press events for a gamepad. | ||
| 159 | * | ||
| 160 | * @param Device The input descriptor of the gamepad. | ||
| 161 | * @param Button Key code identifying which button was pressed. | ||
| 162 | * @param Action Mask identifying which action is happening (button pressed down, or button released). | ||
| 163 | * @return If we handled the button press. | ||
| 164 | */ | ||
| 165 | external fun onGamePadButtonEvent(Device: Int, Button: Int, Action: Int): Boolean | ||
| 166 | |||
| 167 | /** | ||
| 168 | * Handles joystick movement events. | ||
| 169 | * | ||
| 170 | * @param Device The device ID of the gamepad. | ||
| 171 | * @param Axis The axis ID | ||
| 172 | * @param x_axis The value of the x-axis represented by the given ID. | ||
| 173 | * @param y_axis The value of the y-axis represented by the given ID. | ||
| 174 | */ | ||
| 175 | external fun onGamePadJoystickEvent( | ||
| 176 | Device: Int, | ||
| 177 | Axis: Int, | ||
| 178 | x_axis: Float, | ||
| 179 | y_axis: Float | ||
| 180 | ): Boolean | ||
| 181 | |||
| 182 | /** | ||
| 183 | * Handles motion events. | ||
| 184 | * | ||
| 185 | * @param delta_timestamp The finger id corresponding to this event | ||
| 186 | * @param gyro_x,gyro_y,gyro_z The value of the accelerometer sensor. | ||
| 187 | * @param accel_x,accel_y,accel_z The value of the y-axis | ||
| 188 | */ | ||
| 189 | external fun onGamePadMotionEvent( | ||
| 190 | Device: Int, | ||
| 191 | delta_timestamp: Long, | ||
| 192 | gyro_x: Float, | ||
| 193 | gyro_y: Float, | ||
| 194 | gyro_z: Float, | ||
| 195 | accel_x: Float, | ||
| 196 | accel_y: Float, | ||
| 197 | accel_z: Float | ||
| 198 | ): Boolean | ||
| 199 | |||
| 200 | /** | ||
| 201 | * Signals and load a nfc tag | ||
| 202 | * | ||
| 203 | * @param data Byte array containing all the data from a nfc tag | ||
| 204 | */ | ||
| 205 | external fun onReadNfcTag(data: ByteArray?): Boolean | ||
| 206 | |||
| 207 | /** | ||
| 208 | * Removes current loaded nfc tag | ||
| 209 | */ | ||
| 210 | external fun onRemoveNfcTag(): Boolean | ||
| 211 | |||
| 212 | /** | ||
| 213 | * Handles touch press events. | ||
| 214 | * | ||
| 215 | * @param finger_id The finger id corresponding to this event | ||
| 216 | * @param x_axis The value of the x-axis. | ||
| 217 | * @param y_axis The value of the y-axis. | ||
| 218 | */ | ||
| 219 | external fun onTouchPressed(finger_id: Int, x_axis: Float, y_axis: Float) | ||
| 220 | |||
| 221 | /** | ||
| 222 | * Handles touch movement. | ||
| 223 | * | ||
| 224 | * @param x_axis The value of the instantaneous x-axis. | ||
| 225 | * @param y_axis The value of the instantaneous y-axis. | ||
| 226 | */ | ||
| 227 | external fun onTouchMoved(finger_id: Int, x_axis: Float, y_axis: Float) | ||
| 228 | |||
| 229 | /** | ||
| 230 | * Handles touch release events. | ||
| 231 | * | ||
| 232 | * @param finger_id The finger id corresponding to this event | ||
| 233 | */ | ||
| 234 | external fun onTouchReleased(finger_id: Int) | ||
| 235 | |||
| 236 | external fun setAppDirectory(directory: String) | 102 | external fun setAppDirectory(directory: String) |
| 237 | 103 | ||
| 238 | /** | 104 | /** |
| @@ -629,46 +495,4 @@ object NativeLibrary { | |||
| 629 | * Checks if all necessary keys are present for decryption | 495 | * Checks if all necessary keys are present for decryption |
| 630 | */ | 496 | */ |
| 631 | external fun areKeysPresent(): Boolean | 497 | external fun areKeysPresent(): Boolean |
| 632 | |||
| 633 | /** | ||
| 634 | * Button type for use in onTouchEvent | ||
| 635 | */ | ||
| 636 | object ButtonType { | ||
| 637 | const val BUTTON_A = 0 | ||
| 638 | const val BUTTON_B = 1 | ||
| 639 | const val BUTTON_X = 2 | ||
| 640 | const val BUTTON_Y = 3 | ||
| 641 | const val STICK_L = 4 | ||
| 642 | const val STICK_R = 5 | ||
| 643 | const val TRIGGER_L = 6 | ||
| 644 | const val TRIGGER_R = 7 | ||
| 645 | const val TRIGGER_ZL = 8 | ||
| 646 | const val TRIGGER_ZR = 9 | ||
| 647 | const val BUTTON_PLUS = 10 | ||
| 648 | const val BUTTON_MINUS = 11 | ||
| 649 | const val DPAD_LEFT = 12 | ||
| 650 | const val DPAD_UP = 13 | ||
| 651 | const val DPAD_RIGHT = 14 | ||
| 652 | const val DPAD_DOWN = 15 | ||
| 653 | const val BUTTON_SL = 16 | ||
| 654 | const val BUTTON_SR = 17 | ||
| 655 | const val BUTTON_HOME = 18 | ||
| 656 | const val BUTTON_CAPTURE = 19 | ||
| 657 | } | ||
| 658 | |||
| 659 | /** | ||
| 660 | * Stick type for use in onTouchEvent | ||
| 661 | */ | ||
| 662 | object StickType { | ||
| 663 | const val STICK_L = 0 | ||
| 664 | const val STICK_R = 1 | ||
| 665 | } | ||
| 666 | |||
| 667 | /** | ||
| 668 | * Button states | ||
| 669 | */ | ||
| 670 | object ButtonState { | ||
| 671 | const val RELEASED = 0 | ||
| 672 | const val PRESSED = 1 | ||
| 673 | } | ||
| 674 | } | 498 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt index 76778c10a..72943f33e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt | |||
| @@ -7,6 +7,7 @@ import android.app.Application | |||
| 7 | import android.app.NotificationChannel | 7 | import android.app.NotificationChannel |
| 8 | import android.app.NotificationManager | 8 | import android.app.NotificationManager |
| 9 | import android.content.Context | 9 | import android.content.Context |
| 10 | import org.yuzu.yuzu_emu.features.input.NativeInput | ||
| 10 | import java.io.File | 11 | import java.io.File |
| 11 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization | 12 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization |
| 12 | import org.yuzu.yuzu_emu.utils.DocumentsTree | 13 | import org.yuzu.yuzu_emu.utils.DocumentsTree |
| @@ -37,6 +38,7 @@ class YuzuApplication : Application() { | |||
| 37 | documentsTree = DocumentsTree() | 38 | documentsTree = DocumentsTree() |
| 38 | DirectoryInitialization.start() | 39 | DirectoryInitialization.start() |
| 39 | GpuDriverHelper.initializeDriverParameters() | 40 | GpuDriverHelper.initializeDriverParameters() |
| 41 | NativeInput.reloadInputDevices() | ||
| 40 | NativeLibrary.logDeviceInfo() | 42 | NativeLibrary.logDeviceInfo() |
| 41 | Log.logDeviceInfo() | 43 | Log.logDeviceInfo() |
| 42 | 44 | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt index 7a8d03610..0b70fccec 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt | |||
| @@ -39,6 +39,7 @@ import org.yuzu.yuzu_emu.NativeLibrary | |||
| 39 | import org.yuzu.yuzu_emu.R | 39 | import org.yuzu.yuzu_emu.R |
| 40 | import org.yuzu.yuzu_emu.YuzuApplication | 40 | import org.yuzu.yuzu_emu.YuzuApplication |
| 41 | import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding | 41 | import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding |
| 42 | import org.yuzu.yuzu_emu.features.input.NativeInput | ||
| 42 | import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting | 43 | import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting |
| 43 | import org.yuzu.yuzu_emu.features.settings.model.IntSetting | 44 | import org.yuzu.yuzu_emu.features.settings.model.IntSetting |
| 44 | import org.yuzu.yuzu_emu.features.settings.model.Settings | 45 | import org.yuzu.yuzu_emu.features.settings.model.Settings |
| @@ -47,7 +48,9 @@ import org.yuzu.yuzu_emu.model.Game | |||
| 47 | import org.yuzu.yuzu_emu.utils.InputHandler | 48 | import org.yuzu.yuzu_emu.utils.InputHandler |
| 48 | import org.yuzu.yuzu_emu.utils.Log | 49 | import org.yuzu.yuzu_emu.utils.Log |
| 49 | import org.yuzu.yuzu_emu.utils.MemoryUtil | 50 | import org.yuzu.yuzu_emu.utils.MemoryUtil |
| 51 | import org.yuzu.yuzu_emu.utils.NativeConfig | ||
| 50 | import org.yuzu.yuzu_emu.utils.NfcReader | 52 | import org.yuzu.yuzu_emu.utils.NfcReader |
| 53 | import org.yuzu.yuzu_emu.utils.ParamPackage | ||
| 51 | import org.yuzu.yuzu_emu.utils.ThemeHelper | 54 | import org.yuzu.yuzu_emu.utils.ThemeHelper |
| 52 | import java.text.NumberFormat | 55 | import java.text.NumberFormat |
| 53 | import kotlin.math.roundToInt | 56 | import kotlin.math.roundToInt |
| @@ -63,8 +66,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||
| 63 | private var motionTimestamp: Long = 0 | 66 | private var motionTimestamp: Long = 0 |
| 64 | private var flipMotionOrientation: Boolean = false | 67 | private var flipMotionOrientation: Boolean = false |
| 65 | 68 | ||
| 66 | private var controllerIds = InputHandler.getGameControllerIds() | ||
| 67 | |||
| 68 | private val actionPause = "ACTION_EMULATOR_PAUSE" | 69 | private val actionPause = "ACTION_EMULATOR_PAUSE" |
| 69 | private val actionPlay = "ACTION_EMULATOR_PLAY" | 70 | private val actionPlay = "ACTION_EMULATOR_PLAY" |
| 70 | private val actionMute = "ACTION_EMULATOR_MUTE" | 71 | private val actionMute = "ACTION_EMULATOR_MUTE" |
| @@ -78,6 +79,27 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||
| 78 | 79 | ||
| 79 | super.onCreate(savedInstanceState) | 80 | super.onCreate(savedInstanceState) |
| 80 | 81 | ||
| 82 | InputHandler.updateControllerData() | ||
| 83 | val playerOne = NativeConfig.getInputSettings(true)[0] | ||
| 84 | if (!playerOne.hasMapping() && InputHandler.androidControllers.isNotEmpty()) { | ||
| 85 | var params: ParamPackage? = null | ||
| 86 | for (controller in InputHandler.registeredControllers) { | ||
| 87 | if (controller.get("port", -1) == 0) { | ||
| 88 | params = controller | ||
| 89 | break | ||
| 90 | } | ||
| 91 | } | ||
| 92 | |||
| 93 | if (params != null) { | ||
| 94 | NativeInput.updateMappingsWithDefault( | ||
| 95 | 0, | ||
| 96 | params, | ||
| 97 | params.get("display", getString(R.string.unknown)) | ||
| 98 | ) | ||
| 99 | NativeConfig.saveGlobalConfig() | ||
| 100 | } | ||
| 101 | } | ||
| 102 | |||
| 81 | binding = ActivityEmulationBinding.inflate(layoutInflater) | 103 | binding = ActivityEmulationBinding.inflate(layoutInflater) |
| 82 | setContentView(binding.root) | 104 | setContentView(binding.root) |
| 83 | 105 | ||
| @@ -95,8 +117,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||
| 95 | nfcReader = NfcReader(this) | 117 | nfcReader = NfcReader(this) |
| 96 | nfcReader.initialize() | 118 | nfcReader.initialize() |
| 97 | 119 | ||
| 98 | InputHandler.initialize() | ||
| 99 | |||
| 100 | val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) | 120 | val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) |
| 101 | if (!preferences.getBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, false)) { | 121 | if (!preferences.getBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, false)) { |
| 102 | if (MemoryUtil.isLessThan(MemoryUtil.REQUIRED_MEMORY, MemoryUtil.totalMemory)) { | 122 | if (MemoryUtil.isLessThan(MemoryUtil.REQUIRED_MEMORY, MemoryUtil.totalMemory)) { |
| @@ -147,7 +167,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||
| 147 | super.onResume() | 167 | super.onResume() |
| 148 | nfcReader.startScanning() | 168 | nfcReader.startScanning() |
| 149 | startMotionSensorListener() | 169 | startMotionSensorListener() |
| 150 | InputHandler.updateControllerIds() | 170 | InputHandler.updateControllerData() |
| 151 | 171 | ||
| 152 | buildPictureInPictureParams() | 172 | buildPictureInPictureParams() |
| 153 | } | 173 | } |
| @@ -172,6 +192,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||
| 172 | super.onNewIntent(intent) | 192 | super.onNewIntent(intent) |
| 173 | setIntent(intent) | 193 | setIntent(intent) |
| 174 | nfcReader.onNewIntent(intent) | 194 | nfcReader.onNewIntent(intent) |
| 195 | InputHandler.updateControllerData() | ||
| 175 | } | 196 | } |
| 176 | 197 | ||
| 177 | override fun dispatchKeyEvent(event: KeyEvent): Boolean { | 198 | override fun dispatchKeyEvent(event: KeyEvent): Boolean { |
| @@ -244,8 +265,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||
| 244 | } | 265 | } |
| 245 | val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000 | 266 | val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000 |
| 246 | motionTimestamp = event.timestamp | 267 | motionTimestamp = event.timestamp |
| 247 | NativeLibrary.onGamePadMotionEvent( | 268 | NativeInput.onDeviceMotionEvent( |
| 248 | NativeLibrary.Player1Device, | 269 | NativeInput.Player1Device, |
| 249 | deltaTimestamp, | 270 | deltaTimestamp, |
| 250 | gyro[0], | 271 | gyro[0], |
| 251 | gyro[1], | 272 | gyro[1], |
| @@ -254,8 +275,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||
| 254 | accel[1], | 275 | accel[1], |
| 255 | accel[2] | 276 | accel[2] |
| 256 | ) | 277 | ) |
| 257 | NativeLibrary.onGamePadMotionEvent( | 278 | NativeInput.onDeviceMotionEvent( |
| 258 | NativeLibrary.ConsoleDevice, | 279 | NativeInput.ConsoleDevice, |
| 259 | deltaTimestamp, | 280 | deltaTimestamp, |
| 260 | gyro[0], | 281 | gyro[0], |
| 261 | gyro[1], | 282 | gyro[1], |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/NativeInput.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/NativeInput.kt new file mode 100644 index 000000000..15d776311 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/NativeInput.kt | |||
| @@ -0,0 +1,416 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.input | ||
| 5 | |||
| 6 | import org.yuzu.yuzu_emu.features.input.model.NativeButton | ||
| 7 | import org.yuzu.yuzu_emu.features.input.model.NativeAnalog | ||
| 8 | import org.yuzu.yuzu_emu.features.input.model.InputType | ||
| 9 | import org.yuzu.yuzu_emu.features.input.model.ButtonName | ||
| 10 | import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex | ||
| 11 | import org.yuzu.yuzu_emu.utils.NativeConfig | ||
| 12 | import org.yuzu.yuzu_emu.utils.ParamPackage | ||
| 13 | import android.view.InputDevice | ||
| 14 | |||
| 15 | object NativeInput { | ||
| 16 | /** | ||
| 17 | * Default controller id for each device | ||
| 18 | */ | ||
| 19 | const val Player1Device = 0 | ||
| 20 | const val Player2Device = 1 | ||
| 21 | const val Player3Device = 2 | ||
| 22 | const val Player4Device = 3 | ||
| 23 | const val Player5Device = 4 | ||
| 24 | const val Player6Device = 5 | ||
| 25 | const val Player7Device = 6 | ||
| 26 | const val Player8Device = 7 | ||
| 27 | const val ConsoleDevice = 8 | ||
| 28 | |||
| 29 | /** | ||
| 30 | * Button states | ||
| 31 | */ | ||
| 32 | object ButtonState { | ||
| 33 | const val RELEASED = 0 | ||
| 34 | const val PRESSED = 1 | ||
| 35 | } | ||
| 36 | |||
| 37 | /** | ||
| 38 | * Returns true if pro controller isn't available and handheld is. | ||
| 39 | * Intended to check where the input overlay should direct its inputs. | ||
| 40 | */ | ||
| 41 | external fun isHandheldOnly(): Boolean | ||
| 42 | |||
| 43 | /** | ||
| 44 | * Handles button press events for a gamepad. | ||
| 45 | * @param guid 32 character hexadecimal string consisting of the controller's PID+VID. | ||
| 46 | * @param port Port determined by controller connection order. | ||
| 47 | * @param buttonId The Android Keycode corresponding to this event. | ||
| 48 | * @param action Mask identifying which action is happening (button pressed down, or button released). | ||
| 49 | */ | ||
| 50 | external fun onGamePadButtonEvent( | ||
| 51 | guid: String, | ||
| 52 | port: Int, | ||
| 53 | buttonId: Int, | ||
| 54 | action: Int | ||
| 55 | ) | ||
| 56 | |||
| 57 | /** | ||
| 58 | * Handles axis movement events. | ||
| 59 | * @param guid 32 character hexadecimal string consisting of the controller's PID+VID. | ||
| 60 | * @param port Port determined by controller connection order. | ||
| 61 | * @param axis The axis ID. | ||
| 62 | * @param value Value along the given axis. | ||
| 63 | */ | ||
| 64 | external fun onGamePadAxisEvent(guid: String, port: Int, axis: Int, value: Float) | ||
| 65 | |||
| 66 | /** | ||
| 67 | * Handles motion events. | ||
| 68 | * @param guid 32 character hexadecimal string consisting of the controller's PID+VID. | ||
| 69 | * @param port Port determined by controller connection order. | ||
| 70 | * @param deltaTimestamp The finger id corresponding to this event. | ||
| 71 | * @param xGyro The value of the x-axis for the gyroscope. | ||
| 72 | * @param yGyro The value of the y-axis for the gyroscope. | ||
| 73 | * @param zGyro The value of the z-axis for the gyroscope. | ||
| 74 | * @param xAccel The value of the x-axis for the accelerometer. | ||
| 75 | * @param yAccel The value of the y-axis for the accelerometer. | ||
| 76 | * @param zAccel The value of the z-axis for the accelerometer. | ||
| 77 | */ | ||
| 78 | external fun onGamePadMotionEvent( | ||
| 79 | guid: String, | ||
| 80 | port: Int, | ||
| 81 | deltaTimestamp: Long, | ||
| 82 | xGyro: Float, | ||
| 83 | yGyro: Float, | ||
| 84 | zGyro: Float, | ||
| 85 | xAccel: Float, | ||
| 86 | yAccel: Float, | ||
| 87 | zAccel: Float | ||
| 88 | ) | ||
| 89 | |||
| 90 | /** | ||
| 91 | * Signals and load a nfc tag | ||
| 92 | * @param data Byte array containing all the data from a nfc tag. | ||
| 93 | */ | ||
| 94 | external fun onReadNfcTag(data: ByteArray?) | ||
| 95 | |||
| 96 | /** | ||
| 97 | * Removes current loaded nfc tag. | ||
| 98 | */ | ||
| 99 | external fun onRemoveNfcTag() | ||
| 100 | |||
| 101 | /** | ||
| 102 | * Handles touch press events. | ||
| 103 | * @param fingerId The finger id corresponding to this event. | ||
| 104 | * @param xAxis The value of the x-axis on the touchscreen. | ||
| 105 | * @param yAxis The value of the y-axis on the touchscreen. | ||
| 106 | */ | ||
| 107 | external fun onTouchPressed(fingerId: Int, xAxis: Float, yAxis: Float) | ||
| 108 | |||
| 109 | /** | ||
| 110 | * Handles touch movement. | ||
| 111 | * @param fingerId The finger id corresponding to this event. | ||
| 112 | * @param xAxis The value of the x-axis on the touchscreen. | ||
| 113 | * @param yAxis The value of the y-axis on the touchscreen. | ||
| 114 | */ | ||
| 115 | external fun onTouchMoved(fingerId: Int, xAxis: Float, yAxis: Float) | ||
| 116 | |||
| 117 | /** | ||
| 118 | * Handles touch release events. | ||
| 119 | * @param fingerId The finger id corresponding to this event | ||
| 120 | */ | ||
| 121 | external fun onTouchReleased(fingerId: Int) | ||
| 122 | |||
| 123 | /** | ||
| 124 | * Sends a button input to the global virtual controllers. | ||
| 125 | * @param port Port determined by controller connection order. | ||
| 126 | * @param button The [NativeButton] corresponding to this event. | ||
| 127 | * @param action Mask identifying which action is happening (button pressed down, or button released). | ||
| 128 | */ | ||
| 129 | fun onOverlayButtonEvent(port: Int, button: NativeButton, action: Int) = | ||
| 130 | onOverlayButtonEventImpl(port, button.int, action) | ||
| 131 | |||
| 132 | private external fun onOverlayButtonEventImpl(port: Int, buttonId: Int, action: Int) | ||
| 133 | |||
| 134 | /** | ||
| 135 | * Sends a joystick input to the global virtual controllers. | ||
| 136 | * @param port Port determined by controller connection order. | ||
| 137 | * @param stick The [NativeAnalog] corresponding to this event. | ||
| 138 | * @param xAxis Value along the X axis. | ||
| 139 | * @param yAxis Value along the Y axis. | ||
| 140 | */ | ||
| 141 | fun onOverlayJoystickEvent(port: Int, stick: NativeAnalog, xAxis: Float, yAxis: Float) = | ||
| 142 | onOverlayJoystickEventImpl(port, stick.int, xAxis, yAxis) | ||
| 143 | |||
| 144 | private external fun onOverlayJoystickEventImpl( | ||
| 145 | port: Int, | ||
| 146 | stickId: Int, | ||
| 147 | xAxis: Float, | ||
| 148 | yAxis: Float | ||
| 149 | ) | ||
| 150 | |||
| 151 | /** | ||
| 152 | * Handles motion events for the global virtual controllers. | ||
| 153 | * @param port Port determined by controller connection order | ||
| 154 | * @param deltaTimestamp The finger id corresponding to this event. | ||
| 155 | * @param xGyro The value of the x-axis for the gyroscope. | ||
| 156 | * @param yGyro The value of the y-axis for the gyroscope. | ||
| 157 | * @param zGyro The value of the z-axis for the gyroscope. | ||
| 158 | * @param xAccel The value of the x-axis for the accelerometer. | ||
| 159 | * @param yAccel The value of the y-axis for the accelerometer. | ||
| 160 | * @param zAccel The value of the z-axis for the accelerometer. | ||
| 161 | */ | ||
| 162 | external fun onDeviceMotionEvent( | ||
| 163 | port: Int, | ||
| 164 | deltaTimestamp: Long, | ||
| 165 | xGyro: Float, | ||
| 166 | yGyro: Float, | ||
| 167 | zGyro: Float, | ||
| 168 | xAccel: Float, | ||
| 169 | yAccel: Float, | ||
| 170 | zAccel: Float | ||
| 171 | ) | ||
| 172 | |||
| 173 | /** | ||
| 174 | * Reloads all input devices from the currently loaded Settings::values.players into HID Core | ||
| 175 | */ | ||
| 176 | external fun reloadInputDevices() | ||
| 177 | |||
| 178 | /** | ||
| 179 | * Registers a controller to be used with mapping | ||
| 180 | * @param device An [InputDevice] or the input overlay wrapped with [YuzuInputDevice] | ||
| 181 | */ | ||
| 182 | external fun registerController(device: YuzuInputDevice) | ||
| 183 | |||
| 184 | /** | ||
| 185 | * Gets the names of input devices that have been registered with the input subsystem via [registerController] | ||
| 186 | */ | ||
| 187 | external fun getInputDevices(): Array<String> | ||
| 188 | |||
| 189 | /** | ||
| 190 | * Reads all input profiles from disk. Must be called before creating a profile picker. | ||
| 191 | */ | ||
| 192 | external fun loadInputProfiles() | ||
| 193 | |||
| 194 | /** | ||
| 195 | * Gets the names of each available input profile. | ||
| 196 | */ | ||
| 197 | external fun getInputProfileNames(): Array<String> | ||
| 198 | |||
| 199 | /** | ||
| 200 | * Checks if the user-provided name for an input profile is valid. | ||
| 201 | * @param name User-provided name for an input profile. | ||
| 202 | * @return Whether [name] is valid or not. | ||
| 203 | */ | ||
| 204 | external fun isProfileNameValid(name: String): Boolean | ||
| 205 | |||
| 206 | /** | ||
| 207 | * Creates a new input profile. | ||
| 208 | * @param name The new profile's name. | ||
| 209 | * @param playerIndex Index of the player that's currently being edited. Used to write the profile | ||
| 210 | * name to this player's config. | ||
| 211 | * @return Whether creating the profile was successful or not. | ||
| 212 | */ | ||
| 213 | external fun createProfile(name: String, playerIndex: Int): Boolean | ||
| 214 | |||
| 215 | /** | ||
| 216 | * Deletes an input profile. | ||
| 217 | * @param name Name of the profile to delete. | ||
| 218 | * @param playerIndex Index of the player that's currently being edited. Used to remove the profile | ||
| 219 | * name from this player's config if they have it loaded. | ||
| 220 | * @return Whether deleting this profile was successful or not. | ||
| 221 | */ | ||
| 222 | external fun deleteProfile(name: String, playerIndex: Int): Boolean | ||
| 223 | |||
| 224 | /** | ||
| 225 | * Loads an input profile. | ||
| 226 | * @param name Name of the input profile to load. | ||
| 227 | * @param playerIndex Index of the player that will have this profile loaded. | ||
| 228 | * @return Whether loading this profile was successful or not. | ||
| 229 | */ | ||
| 230 | external fun loadProfile(name: String, playerIndex: Int): Boolean | ||
| 231 | |||
| 232 | /** | ||
| 233 | * Saves an input profile. | ||
| 234 | * @param name Name of the profile to save. | ||
| 235 | * @param playerIndex Index of the player that's currently being edited. Used to write the profile | ||
| 236 | * name to this player's config. | ||
| 237 | * @return Whether saving the profile was successful or not. | ||
| 238 | */ | ||
| 239 | external fun saveProfile(name: String, playerIndex: Int): Boolean | ||
| 240 | |||
| 241 | /** | ||
| 242 | * Intended to be used immediately before a call to [NativeConfig.saveControlPlayerValues] | ||
| 243 | * Must be used while per-game config is loaded. | ||
| 244 | */ | ||
| 245 | external fun loadPerGameConfiguration( | ||
| 246 | playerIndex: Int, | ||
| 247 | selectedIndex: Int, | ||
| 248 | selectedProfileName: String | ||
| 249 | ) | ||
| 250 | |||
| 251 | /** | ||
| 252 | * Tells the input subsystem to start listening for inputs to map. | ||
| 253 | * @param type Type of input to map as shown by the int property in each [InputType]. | ||
| 254 | */ | ||
| 255 | external fun beginMapping(type: Int) | ||
| 256 | |||
| 257 | /** | ||
| 258 | * Gets an input's [ParamPackage] as a serialized string. Used for input verification before mapping. | ||
| 259 | * Must be run after [beginMapping] and before [stopMapping]. | ||
| 260 | */ | ||
| 261 | external fun getNextInput(): String | ||
| 262 | |||
| 263 | /** | ||
| 264 | * Tells the input subsystem to stop listening for inputs to map. | ||
| 265 | */ | ||
| 266 | external fun stopMapping() | ||
| 267 | |||
| 268 | /** | ||
| 269 | * Updates a controller's mappings with auto-mapping params. | ||
| 270 | * @param playerIndex Index of the player to auto-map. | ||
| 271 | * @param deviceParams [ParamPackage] representing the device to auto-map as received | ||
| 272 | * from [getInputDevices]. | ||
| 273 | * @param displayName Name of the device to auto-map as received from the "display" param in [deviceParams]. | ||
| 274 | * Intended to be a way to provide a default name for a controller if the "display" param is empty. | ||
| 275 | */ | ||
| 276 | fun updateMappingsWithDefault( | ||
| 277 | playerIndex: Int, | ||
| 278 | deviceParams: ParamPackage, | ||
| 279 | displayName: String | ||
| 280 | ) = updateMappingsWithDefaultImpl(playerIndex, deviceParams.serialize(), displayName) | ||
| 281 | |||
| 282 | private external fun updateMappingsWithDefaultImpl( | ||
| 283 | playerIndex: Int, | ||
| 284 | deviceParams: String, | ||
| 285 | displayName: String | ||
| 286 | ) | ||
| 287 | |||
| 288 | /** | ||
| 289 | * Gets the params for a specific button. | ||
| 290 | * @param playerIndex Index of the player to get params from. | ||
| 291 | * @param button The [NativeButton] to get params for. | ||
| 292 | * @return A [ParamPackage] representing a player's specific button. | ||
| 293 | */ | ||
| 294 | fun getButtonParam(playerIndex: Int, button: NativeButton): ParamPackage = | ||
| 295 | ParamPackage(getButtonParamImpl(playerIndex, button.int)) | ||
| 296 | |||
| 297 | private external fun getButtonParamImpl(playerIndex: Int, buttonId: Int): String | ||
| 298 | |||
| 299 | /** | ||
| 300 | * Sets the params for a specific button. | ||
| 301 | * @param playerIndex Index of the player to set params for. | ||
| 302 | * @param button The [NativeButton] to set params for. | ||
| 303 | * @param param A [ParamPackage] to set. | ||
| 304 | */ | ||
| 305 | fun setButtonParam(playerIndex: Int, button: NativeButton, param: ParamPackage) = | ||
| 306 | setButtonParamImpl(playerIndex, button.int, param.serialize()) | ||
| 307 | |||
| 308 | private external fun setButtonParamImpl(playerIndex: Int, buttonId: Int, param: String) | ||
| 309 | |||
| 310 | /** | ||
| 311 | * Gets the params for a specific stick. | ||
| 312 | * @param playerIndex Index of the player to get params from. | ||
| 313 | * @param stick The [NativeAnalog] to get params for. | ||
| 314 | * @return A [ParamPackage] representing a player's specific stick. | ||
| 315 | */ | ||
| 316 | fun getStickParam(playerIndex: Int, stick: NativeAnalog): ParamPackage = | ||
| 317 | ParamPackage(getStickParamImpl(playerIndex, stick.int)) | ||
| 318 | |||
| 319 | private external fun getStickParamImpl(playerIndex: Int, stickId: Int): String | ||
| 320 | |||
| 321 | /** | ||
| 322 | * Sets the params for a specific stick. | ||
| 323 | * @param playerIndex Index of the player to set params for. | ||
| 324 | * @param stick The [NativeAnalog] to set params for. | ||
| 325 | * @param param A [ParamPackage] to set. | ||
| 326 | */ | ||
| 327 | fun setStickParam(playerIndex: Int, stick: NativeAnalog, param: ParamPackage) = | ||
| 328 | setStickParamImpl(playerIndex, stick.int, param.serialize()) | ||
| 329 | |||
| 330 | private external fun setStickParamImpl(playerIndex: Int, stickId: Int, param: String) | ||
| 331 | |||
| 332 | /** | ||
| 333 | * Gets the int representation of a [ButtonName]. Tells you what to show as the mapped input for | ||
| 334 | * a button/analog/other. | ||
| 335 | * @param param A [ParamPackage] that represents a specific button's params. | ||
| 336 | * @return The [ButtonName] for [param]. | ||
| 337 | */ | ||
| 338 | fun getButtonName(param: ParamPackage): ButtonName = | ||
| 339 | ButtonName.from(getButtonNameImpl(param.serialize())) | ||
| 340 | |||
| 341 | private external fun getButtonNameImpl(param: String): Int | ||
| 342 | |||
| 343 | /** | ||
| 344 | * Gets each supported [NpadStyleIndex] for a given player. | ||
| 345 | * @param playerIndex Index of the player to get supported indexes for. | ||
| 346 | * @return List of each supported [NpadStyleIndex]. | ||
| 347 | */ | ||
| 348 | fun getSupportedStyleTags(playerIndex: Int): List<NpadStyleIndex> = | ||
| 349 | getSupportedStyleTagsImpl(playerIndex).map { NpadStyleIndex.from(it) } | ||
| 350 | |||
| 351 | private external fun getSupportedStyleTagsImpl(playerIndex: Int): IntArray | ||
| 352 | |||
| 353 | /** | ||
| 354 | * Gets the [NpadStyleIndex] for a given player. | ||
| 355 | * @param playerIndex Index of the player to get an [NpadStyleIndex] from. | ||
| 356 | * @return The [NpadStyleIndex] for a given player. | ||
| 357 | */ | ||
| 358 | fun getStyleIndex(playerIndex: Int): NpadStyleIndex = | ||
| 359 | NpadStyleIndex.from(getStyleIndexImpl(playerIndex)) | ||
| 360 | |||
| 361 | private external fun getStyleIndexImpl(playerIndex: Int): Int | ||
| 362 | |||
| 363 | /** | ||
| 364 | * Sets the [NpadStyleIndex] for a given player. | ||
| 365 | * @param playerIndex Index of the player to change. | ||
| 366 | * @param style The new style to set. | ||
| 367 | */ | ||
| 368 | fun setStyleIndex(playerIndex: Int, style: NpadStyleIndex) = | ||
| 369 | setStyleIndexImpl(playerIndex, style.int) | ||
| 370 | |||
| 371 | private external fun setStyleIndexImpl(playerIndex: Int, styleIndex: Int) | ||
| 372 | |||
| 373 | /** | ||
| 374 | * Checks if a device is a controller. | ||
| 375 | * @param params [ParamPackage] for an input device retrieved from [getInputDevices] | ||
| 376 | * @return Whether the device is a controller or not. | ||
| 377 | */ | ||
| 378 | fun isController(params: ParamPackage): Boolean = isControllerImpl(params.serialize()) | ||
| 379 | |||
| 380 | private external fun isControllerImpl(params: String): Boolean | ||
| 381 | |||
| 382 | /** | ||
| 383 | * Checks if a controller is connected | ||
| 384 | * @param playerIndex Index of the player to check. | ||
| 385 | * @return Whether the player is connected or not. | ||
| 386 | */ | ||
| 387 | external fun getIsConnected(playerIndex: Int): Boolean | ||
| 388 | |||
| 389 | /** | ||
| 390 | * Connects/disconnects a controller and ensures that connection order stays in-tact. | ||
| 391 | * @param playerIndex Index of the player to connect/disconnect. | ||
| 392 | * @param connected Whether to connect or disconnect this controller. | ||
| 393 | */ | ||
| 394 | fun connectControllers(playerIndex: Int, connected: Boolean = true) { | ||
| 395 | val connectedControllers = mutableListOf<Boolean>().apply { | ||
| 396 | if (connected) { | ||
| 397 | for (i in 0 until 8) { | ||
| 398 | add(i <= playerIndex) | ||
| 399 | } | ||
| 400 | } else { | ||
| 401 | for (i in 0 until 8) { | ||
| 402 | add(i < playerIndex) | ||
| 403 | } | ||
| 404 | } | ||
| 405 | } | ||
| 406 | connectControllersImpl(connectedControllers.toBooleanArray()) | ||
| 407 | } | ||
| 408 | |||
| 409 | private external fun connectControllersImpl(connected: BooleanArray) | ||
| 410 | |||
| 411 | /** | ||
| 412 | * Resets all of the button and analog mappings for a player. | ||
| 413 | * @param playerIndex Index of the player that will have its mappings reset. | ||
| 414 | */ | ||
| 415 | external fun resetControllerMappings(playerIndex: Int) | ||
| 416 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuInputDevice.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuInputDevice.kt new file mode 100644 index 000000000..15cc38c7f --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuInputDevice.kt | |||
| @@ -0,0 +1,93 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.input | ||
| 5 | |||
| 6 | import android.view.InputDevice | ||
| 7 | import androidx.annotation.Keep | ||
| 8 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 9 | import org.yuzu.yuzu_emu.R | ||
| 10 | import org.yuzu.yuzu_emu.utils.InputHandler.getGUID | ||
| 11 | |||
| 12 | @Keep | ||
| 13 | interface YuzuInputDevice { | ||
| 14 | fun getName(): String | ||
| 15 | |||
| 16 | fun getGUID(): String | ||
| 17 | |||
| 18 | fun getPort(): Int | ||
| 19 | |||
| 20 | fun getSupportsVibration(): Boolean | ||
| 21 | |||
| 22 | fun vibrate(intensity: Float) | ||
| 23 | |||
| 24 | fun getAxes(): Array<Int> = arrayOf() | ||
| 25 | fun hasKeys(keys: IntArray): BooleanArray = BooleanArray(0) | ||
| 26 | } | ||
| 27 | |||
| 28 | class YuzuPhysicalDevice( | ||
| 29 | private val device: InputDevice, | ||
| 30 | private val port: Int, | ||
| 31 | useSystemVibrator: Boolean | ||
| 32 | ) : YuzuInputDevice { | ||
| 33 | private val vibrator = if (useSystemVibrator) { | ||
| 34 | YuzuVibrator.getSystemVibrator() | ||
| 35 | } else { | ||
| 36 | YuzuVibrator.getControllerVibrator(device) | ||
| 37 | } | ||
| 38 | |||
| 39 | override fun getName(): String { | ||
| 40 | return device.name | ||
| 41 | } | ||
| 42 | |||
| 43 | override fun getGUID(): String { | ||
| 44 | return device.getGUID() | ||
| 45 | } | ||
| 46 | |||
| 47 | override fun getPort(): Int { | ||
| 48 | return port | ||
| 49 | } | ||
| 50 | |||
| 51 | override fun getSupportsVibration(): Boolean { | ||
| 52 | return vibrator.supportsVibration() | ||
| 53 | } | ||
| 54 | |||
| 55 | override fun vibrate(intensity: Float) { | ||
| 56 | vibrator.vibrate(intensity) | ||
| 57 | } | ||
| 58 | |||
| 59 | override fun getAxes(): Array<Int> = device.motionRanges.map { it.axis }.toTypedArray() | ||
| 60 | override fun hasKeys(keys: IntArray): BooleanArray = device.hasKeys(*keys) | ||
| 61 | } | ||
| 62 | |||
| 63 | class YuzuInputOverlayDevice( | ||
| 64 | private val vibration: Boolean, | ||
| 65 | private val port: Int | ||
| 66 | ) : YuzuInputDevice { | ||
| 67 | private val vibrator = YuzuVibrator.getSystemVibrator() | ||
| 68 | |||
| 69 | override fun getName(): String { | ||
| 70 | return YuzuApplication.appContext.getString(R.string.input_overlay) | ||
| 71 | } | ||
| 72 | |||
| 73 | override fun getGUID(): String { | ||
| 74 | return "00000000000000000000000000000000" | ||
| 75 | } | ||
| 76 | |||
| 77 | override fun getPort(): Int { | ||
| 78 | return port | ||
| 79 | } | ||
| 80 | |||
| 81 | override fun getSupportsVibration(): Boolean { | ||
| 82 | if (vibration) { | ||
| 83 | return vibrator.supportsVibration() | ||
| 84 | } | ||
| 85 | return false | ||
| 86 | } | ||
| 87 | |||
| 88 | override fun vibrate(intensity: Float) { | ||
| 89 | if (vibration) { | ||
| 90 | vibrator.vibrate(intensity) | ||
| 91 | } | ||
| 92 | } | ||
| 93 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuVibrator.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuVibrator.kt new file mode 100644 index 000000000..aac49ecae --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuVibrator.kt | |||
| @@ -0,0 +1,76 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.input | ||
| 5 | |||
| 6 | import android.content.Context | ||
| 7 | import android.os.Build | ||
| 8 | import android.os.CombinedVibration | ||
| 9 | import android.os.VibrationEffect | ||
| 10 | import android.os.Vibrator | ||
| 11 | import android.os.VibratorManager | ||
| 12 | import android.view.InputDevice | ||
| 13 | import androidx.annotation.Keep | ||
| 14 | import androidx.annotation.RequiresApi | ||
| 15 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 16 | |||
| 17 | @Keep | ||
| 18 | @Suppress("DEPRECATION") | ||
| 19 | interface YuzuVibrator { | ||
| 20 | fun supportsVibration(): Boolean | ||
| 21 | |||
| 22 | fun vibrate(intensity: Float) | ||
| 23 | |||
| 24 | companion object { | ||
| 25 | fun getControllerVibrator(device: InputDevice): YuzuVibrator = | ||
| 26 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||
| 27 | YuzuVibratorManager(device.vibratorManager) | ||
| 28 | } else { | ||
| 29 | YuzuVibratorManagerCompat(device.vibrator) | ||
| 30 | } | ||
| 31 | |||
| 32 | fun getSystemVibrator(): YuzuVibrator = | ||
| 33 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||
| 34 | val vibratorManager = YuzuApplication.appContext | ||
| 35 | .getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager | ||
| 36 | YuzuVibratorManager(vibratorManager) | ||
| 37 | } else { | ||
| 38 | val vibrator = YuzuApplication.appContext | ||
| 39 | .getSystemService(Context.VIBRATOR_SERVICE) as Vibrator | ||
| 40 | YuzuVibratorManagerCompat(vibrator) | ||
| 41 | } | ||
| 42 | |||
| 43 | fun getVibrationEffect(intensity: Float): VibrationEffect? { | ||
| 44 | if (intensity > 0f) { | ||
| 45 | return VibrationEffect.createOneShot( | ||
| 46 | 50, | ||
| 47 | (255.0 * intensity).toInt().coerceIn(1, 255) | ||
| 48 | ) | ||
| 49 | } | ||
| 50 | return null | ||
| 51 | } | ||
| 52 | } | ||
| 53 | } | ||
| 54 | |||
| 55 | @RequiresApi(Build.VERSION_CODES.S) | ||
| 56 | class YuzuVibratorManager(private val vibratorManager: VibratorManager) : YuzuVibrator { | ||
| 57 | override fun supportsVibration(): Boolean { | ||
| 58 | return vibratorManager.vibratorIds.isNotEmpty() | ||
| 59 | } | ||
| 60 | |||
| 61 | override fun vibrate(intensity: Float) { | ||
| 62 | val vibration = YuzuVibrator.getVibrationEffect(intensity) ?: return | ||
| 63 | vibratorManager.vibrate(CombinedVibration.createParallel(vibration)) | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | class YuzuVibratorManagerCompat(private val vibrator: Vibrator) : YuzuVibrator { | ||
| 68 | override fun supportsVibration(): Boolean { | ||
| 69 | return vibrator.hasVibrator() | ||
| 70 | } | ||
| 71 | |||
| 72 | override fun vibrate(intensity: Float) { | ||
| 73 | val vibration = YuzuVibrator.getVibrationEffect(intensity) ?: return | ||
| 74 | vibrator.vibrate(vibration) | ||
| 75 | } | ||
| 76 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/AnalogDirection.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/AnalogDirection.kt new file mode 100644 index 000000000..0a5fab2ae --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/AnalogDirection.kt | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.input.model | ||
| 5 | |||
| 6 | enum class AnalogDirection(val int: Int, val param: String) { | ||
| 7 | Up(0, "up"), | ||
| 8 | Down(1, "down"), | ||
| 9 | Left(2, "left"), | ||
| 10 | Right(3, "right") | ||
| 11 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/ButtonName.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/ButtonName.kt new file mode 100644 index 000000000..b8846ecad --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/ButtonName.kt | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.input.model | ||
| 5 | |||
| 6 | // Loosely matches the enum in common/input.h | ||
| 7 | enum class ButtonName(val int: Int) { | ||
| 8 | Invalid(1), | ||
| 9 | |||
| 10 | // This will display the engine name instead of the button name | ||
| 11 | Engine(2), | ||
| 12 | |||
| 13 | // This will display the button by value instead of the button name | ||
| 14 | Value(3); | ||
| 15 | |||
| 16 | companion object { | ||
| 17 | fun from(int: Int): ButtonName = entries.firstOrNull { it.int == int } ?: Invalid | ||
| 18 | } | ||
| 19 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/InputType.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/InputType.kt new file mode 100644 index 000000000..f725231cb --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/InputType.kt | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.input.model | ||
| 5 | |||
| 6 | // Must match the corresponding enum in input_common/main.h | ||
| 7 | enum class InputType(val int: Int) { | ||
| 8 | None(0), | ||
| 9 | Button(1), | ||
| 10 | Stick(2), | ||
| 11 | Motion(3), | ||
| 12 | Touch(4) | ||
| 13 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeAnalog.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeAnalog.kt new file mode 100644 index 000000000..c3b7a785d --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeAnalog.kt | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.input.model | ||
| 5 | |||
| 6 | // Must match enum in src/common/settings_input.h | ||
| 7 | enum class NativeAnalog(val int: Int) { | ||
| 8 | LStick(0), | ||
| 9 | RStick(1); | ||
| 10 | |||
| 11 | companion object { | ||
| 12 | fun from(int: Int): NativeAnalog = entries.firstOrNull { it.int == int } ?: LStick | ||
| 13 | } | ||
| 14 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeButton.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeButton.kt new file mode 100644 index 000000000..c5ccd7115 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeButton.kt | |||
| @@ -0,0 +1,38 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.input.model | ||
| 5 | |||
| 6 | // Must match enum in src/common/settings_input.h | ||
| 7 | enum class NativeButton(val int: Int) { | ||
| 8 | A(0), | ||
| 9 | B(1), | ||
| 10 | X(2), | ||
| 11 | Y(3), | ||
| 12 | LStick(4), | ||
| 13 | RStick(5), | ||
| 14 | L(6), | ||
| 15 | R(7), | ||
| 16 | ZL(8), | ||
| 17 | ZR(9), | ||
| 18 | Plus(10), | ||
| 19 | Minus(11), | ||
| 20 | |||
| 21 | DLeft(12), | ||
| 22 | DUp(13), | ||
| 23 | DRight(14), | ||
| 24 | DDown(15), | ||
| 25 | |||
| 26 | SLLeft(16), | ||
| 27 | SRLeft(17), | ||
| 28 | |||
| 29 | Home(18), | ||
| 30 | Capture(19), | ||
| 31 | |||
| 32 | SLRight(20), | ||
| 33 | SRRight(21); | ||
| 34 | |||
| 35 | companion object { | ||
| 36 | fun from(int: Int): NativeButton = entries.firstOrNull { it.int == int } ?: A | ||
| 37 | } | ||
| 38 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeTrigger.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeTrigger.kt new file mode 100644 index 000000000..625f352b4 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeTrigger.kt | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.input.model | ||
| 5 | |||
| 6 | // Must match enum in src/common/settings_input.h | ||
| 7 | enum class NativeTrigger(val int: Int) { | ||
| 8 | LTrigger(0), | ||
| 9 | RTrigger(1) | ||
| 10 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NpadStyleIndex.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NpadStyleIndex.kt new file mode 100644 index 000000000..e2a3d7aff --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NpadStyleIndex.kt | |||
| @@ -0,0 +1,30 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.input.model | ||
| 5 | |||
| 6 | import androidx.annotation.StringRes | ||
| 7 | import org.yuzu.yuzu_emu.R | ||
| 8 | |||
| 9 | // Must match enum in src/core/hid/hid_types.h | ||
| 10 | enum class NpadStyleIndex(val int: Int, @StringRes val nameId: Int = 0) { | ||
| 11 | None(0), | ||
| 12 | Fullkey(3, R.string.pro_controller), | ||
| 13 | Handheld(4, R.string.handheld), | ||
| 14 | HandheldNES(4), | ||
| 15 | JoyconDual(5, R.string.dual_joycons), | ||
| 16 | JoyconLeft(6, R.string.left_joycon), | ||
| 17 | JoyconRight(7, R.string.right_joycon), | ||
| 18 | GameCube(8, R.string.gamecube_controller), | ||
| 19 | Pokeball(9), | ||
| 20 | NES(10), | ||
| 21 | SNES(12), | ||
| 22 | N64(13), | ||
| 23 | SegaGenesis(14), | ||
| 24 | SystemExt(32), | ||
| 25 | System(33); | ||
| 26 | |||
| 27 | companion object { | ||
| 28 | fun from(int: Int): NpadStyleIndex = entries.firstOrNull { it.int == int } ?: None | ||
| 29 | } | ||
| 30 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/PlayerInput.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/PlayerInput.kt new file mode 100644 index 000000000..d35de80c4 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/PlayerInput.kt | |||
| @@ -0,0 +1,83 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.input.model | ||
| 5 | |||
| 6 | import androidx.annotation.Keep | ||
| 7 | |||
| 8 | @Keep | ||
| 9 | data class PlayerInput( | ||
| 10 | var connected: Boolean, | ||
| 11 | var buttons: Array<String>, | ||
| 12 | var analogs: Array<String>, | ||
| 13 | var motions: Array<String>, | ||
| 14 | |||
| 15 | var vibrationEnabled: Boolean, | ||
| 16 | var vibrationStrength: Int, | ||
| 17 | |||
| 18 | var bodyColorLeft: Long, | ||
| 19 | var bodyColorRight: Long, | ||
| 20 | var buttonColorLeft: Long, | ||
| 21 | var buttonColorRight: Long, | ||
| 22 | var profileName: String, | ||
| 23 | |||
| 24 | var useSystemVibrator: Boolean | ||
| 25 | ) { | ||
| 26 | // It's recommended to use the generated equals() and hashCode() methods | ||
| 27 | // when using arrays in a data class | ||
| 28 | override fun equals(other: Any?): Boolean { | ||
| 29 | if (this === other) return true | ||
| 30 | if (javaClass != other?.javaClass) return false | ||
| 31 | |||
| 32 | other as PlayerInput | ||
| 33 | |||
| 34 | if (connected != other.connected) return false | ||
| 35 | if (!buttons.contentEquals(other.buttons)) return false | ||
| 36 | if (!analogs.contentEquals(other.analogs)) return false | ||
| 37 | if (!motions.contentEquals(other.motions)) return false | ||
| 38 | if (vibrationEnabled != other.vibrationEnabled) return false | ||
| 39 | if (vibrationStrength != other.vibrationStrength) return false | ||
| 40 | if (bodyColorLeft != other.bodyColorLeft) return false | ||
| 41 | if (bodyColorRight != other.bodyColorRight) return false | ||
| 42 | if (buttonColorLeft != other.buttonColorLeft) return false | ||
| 43 | if (buttonColorRight != other.buttonColorRight) return false | ||
| 44 | if (profileName != other.profileName) return false | ||
| 45 | return useSystemVibrator == other.useSystemVibrator | ||
| 46 | } | ||
| 47 | |||
| 48 | override fun hashCode(): Int { | ||
| 49 | var result = connected.hashCode() | ||
| 50 | result = 31 * result + buttons.contentHashCode() | ||
| 51 | result = 31 * result + analogs.contentHashCode() | ||
| 52 | result = 31 * result + motions.contentHashCode() | ||
| 53 | result = 31 * result + vibrationEnabled.hashCode() | ||
| 54 | result = 31 * result + vibrationStrength | ||
| 55 | result = 31 * result + bodyColorLeft.hashCode() | ||
| 56 | result = 31 * result + bodyColorRight.hashCode() | ||
| 57 | result = 31 * result + buttonColorLeft.hashCode() | ||
| 58 | result = 31 * result + buttonColorRight.hashCode() | ||
| 59 | result = 31 * result + profileName.hashCode() | ||
| 60 | result = 31 * result + useSystemVibrator.hashCode() | ||
| 61 | return result | ||
| 62 | } | ||
| 63 | |||
| 64 | fun hasMapping(): Boolean { | ||
| 65 | var hasMapping = false | ||
| 66 | buttons.forEach { | ||
| 67 | if (it != "[empty]") { | ||
| 68 | hasMapping = true | ||
| 69 | } | ||
| 70 | } | ||
| 71 | analogs.forEach { | ||
| 72 | if (it != "[empty]") { | ||
| 73 | hasMapping = true | ||
| 74 | } | ||
| 75 | } | ||
| 76 | motions.forEach { | ||
| 77 | if (it != "[empty]") { | ||
| 78 | hasMapping = true | ||
| 79 | } | ||
| 80 | } | ||
| 81 | return hasMapping | ||
| 82 | } | ||
| 83 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt index 862c6c483..4f6b93bd2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt | |||
| @@ -4,17 +4,30 @@ | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.model | 4 | package org.yuzu.yuzu_emu.features.settings.model |
| 5 | 5 | ||
| 6 | import org.yuzu.yuzu_emu.R | 6 | import org.yuzu.yuzu_emu.R |
| 7 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 7 | 8 | ||
| 8 | object Settings { | 9 | object Settings { |
| 9 | enum class MenuTag(val titleId: Int) { | 10 | enum class MenuTag(val titleId: Int = 0) { |
| 10 | SECTION_ROOT(R.string.advanced_settings), | 11 | SECTION_ROOT(R.string.advanced_settings), |
| 11 | SECTION_SYSTEM(R.string.preferences_system), | 12 | SECTION_SYSTEM(R.string.preferences_system), |
| 12 | SECTION_RENDERER(R.string.preferences_graphics), | 13 | SECTION_RENDERER(R.string.preferences_graphics), |
| 13 | SECTION_AUDIO(R.string.preferences_audio), | 14 | SECTION_AUDIO(R.string.preferences_audio), |
| 15 | SECTION_INPUT(R.string.preferences_controls), | ||
| 16 | SECTION_INPUT_PLAYER_ONE, | ||
| 17 | SECTION_INPUT_PLAYER_TWO, | ||
| 18 | SECTION_INPUT_PLAYER_THREE, | ||
| 19 | SECTION_INPUT_PLAYER_FOUR, | ||
| 20 | SECTION_INPUT_PLAYER_FIVE, | ||
| 21 | SECTION_INPUT_PLAYER_SIX, | ||
| 22 | SECTION_INPUT_PLAYER_SEVEN, | ||
| 23 | SECTION_INPUT_PLAYER_EIGHT, | ||
| 14 | SECTION_THEME(R.string.preferences_theme), | 24 | SECTION_THEME(R.string.preferences_theme), |
| 15 | SECTION_DEBUG(R.string.preferences_debug); | 25 | SECTION_DEBUG(R.string.preferences_debug); |
| 16 | } | 26 | } |
| 17 | 27 | ||
| 28 | fun getPlayerString(player: Int): String = | ||
| 29 | YuzuApplication.appContext.getString(R.string.preferences_player, player) | ||
| 30 | |||
| 18 | const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch" | 31 | const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch" |
| 19 | const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown" | 32 | const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown" |
| 20 | 33 | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/AnalogInputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/AnalogInputSetting.kt new file mode 100644 index 000000000..a2996725e --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/AnalogInputSetting.kt | |||
| @@ -0,0 +1,31 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.model.view | ||
| 5 | |||
| 6 | import androidx.annotation.StringRes | ||
| 7 | import org.yuzu.yuzu_emu.features.input.NativeInput | ||
| 8 | import org.yuzu.yuzu_emu.features.input.model.AnalogDirection | ||
| 9 | import org.yuzu.yuzu_emu.features.input.model.InputType | ||
| 10 | import org.yuzu.yuzu_emu.features.input.model.NativeAnalog | ||
| 11 | import org.yuzu.yuzu_emu.utils.ParamPackage | ||
| 12 | |||
| 13 | class AnalogInputSetting( | ||
| 14 | override val playerIndex: Int, | ||
| 15 | val nativeAnalog: NativeAnalog, | ||
| 16 | val analogDirection: AnalogDirection, | ||
| 17 | @StringRes titleId: Int = 0, | ||
| 18 | titleString: String = "" | ||
| 19 | ) : InputSetting(titleId, titleString) { | ||
| 20 | override val type = TYPE_INPUT | ||
| 21 | override val inputType = InputType.Stick | ||
| 22 | |||
| 23 | override fun getSelectedValue(): String { | ||
| 24 | val params = NativeInput.getStickParam(playerIndex, nativeAnalog) | ||
| 25 | val analog = analogToText(params, analogDirection.param) | ||
| 26 | return getDisplayString(params, analog) | ||
| 27 | } | ||
| 28 | |||
| 29 | override fun setSelectedValue(param: ParamPackage) = | ||
| 30 | NativeInput.setStickParam(playerIndex, nativeAnalog, param) | ||
| 31 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ButtonInputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ButtonInputSetting.kt new file mode 100644 index 000000000..786d09a7a --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ButtonInputSetting.kt | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.model.view | ||
| 5 | |||
| 6 | import androidx.annotation.StringRes | ||
| 7 | import org.yuzu.yuzu_emu.utils.ParamPackage | ||
| 8 | import org.yuzu.yuzu_emu.features.input.NativeInput | ||
| 9 | import org.yuzu.yuzu_emu.features.input.model.InputType | ||
| 10 | import org.yuzu.yuzu_emu.features.input.model.NativeButton | ||
| 11 | |||
| 12 | class ButtonInputSetting( | ||
| 13 | override val playerIndex: Int, | ||
| 14 | val nativeButton: NativeButton, | ||
| 15 | @StringRes titleId: Int = 0, | ||
| 16 | titleString: String = "" | ||
| 17 | ) : InputSetting(titleId, titleString) { | ||
| 18 | override val type = TYPE_INPUT | ||
| 19 | override val inputType = InputType.Button | ||
| 20 | |||
| 21 | override fun getSelectedValue(): String { | ||
| 22 | val params = NativeInput.getButtonParam(playerIndex, nativeButton) | ||
| 23 | val button = buttonToText(params) | ||
| 24 | return getDisplayString(params, button) | ||
| 25 | } | ||
| 26 | |||
| 27 | override fun setSelectedValue(param: ParamPackage) = | ||
| 28 | NativeInput.setButtonParam(playerIndex, nativeButton, param) | ||
| 29 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputProfileSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputProfileSetting.kt new file mode 100644 index 000000000..c46de08c5 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputProfileSetting.kt | |||
| @@ -0,0 +1,32 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.model.view | ||
| 5 | |||
| 6 | import org.yuzu.yuzu_emu.R | ||
| 7 | import org.yuzu.yuzu_emu.features.input.NativeInput | ||
| 8 | import org.yuzu.yuzu_emu.utils.NativeConfig | ||
| 9 | |||
| 10 | class InputProfileSetting(private val playerIndex: Int) : | ||
| 11 | SettingsItem(emptySetting, R.string.profile, "", 0, "") { | ||
| 12 | override val type = TYPE_INPUT_PROFILE | ||
| 13 | |||
| 14 | fun getCurrentProfile(): String = | ||
| 15 | NativeConfig.getInputSettings(true)[playerIndex].profileName | ||
| 16 | |||
| 17 | fun getProfileNames(): Array<String> = NativeInput.getInputProfileNames() | ||
| 18 | |||
| 19 | fun isProfileNameValid(name: String): Boolean = NativeInput.isProfileNameValid(name) | ||
| 20 | |||
| 21 | fun createProfile(name: String): Boolean = NativeInput.createProfile(name, playerIndex) | ||
| 22 | |||
| 23 | fun deleteProfile(name: String): Boolean = NativeInput.deleteProfile(name, playerIndex) | ||
| 24 | |||
| 25 | fun loadProfile(name: String): Boolean { | ||
| 26 | val result = NativeInput.loadProfile(name, playerIndex) | ||
| 27 | NativeInput.reloadInputDevices() | ||
| 28 | return result | ||
| 29 | } | ||
| 30 | |||
| 31 | fun saveProfile(name: String): Boolean = NativeInput.saveProfile(name, playerIndex) | ||
| 32 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputSetting.kt new file mode 100644 index 000000000..2d118bff3 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputSetting.kt | |||
| @@ -0,0 +1,134 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.model.view | ||
| 5 | |||
| 6 | import androidx.annotation.StringRes | ||
| 7 | import org.yuzu.yuzu_emu.R | ||
| 8 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 9 | import org.yuzu.yuzu_emu.features.input.NativeInput | ||
| 10 | import org.yuzu.yuzu_emu.features.input.model.ButtonName | ||
| 11 | import org.yuzu.yuzu_emu.features.input.model.InputType | ||
| 12 | import org.yuzu.yuzu_emu.utils.ParamPackage | ||
| 13 | |||
| 14 | sealed class InputSetting( | ||
| 15 | @StringRes titleId: Int, | ||
| 16 | titleString: String | ||
| 17 | ) : SettingsItem(emptySetting, titleId, titleString, 0, "") { | ||
| 18 | override val type = TYPE_INPUT | ||
| 19 | abstract val inputType: InputType | ||
| 20 | abstract val playerIndex: Int | ||
| 21 | |||
| 22 | protected val context get() = YuzuApplication.appContext | ||
| 23 | |||
| 24 | abstract fun getSelectedValue(): String | ||
| 25 | |||
| 26 | abstract fun setSelectedValue(param: ParamPackage) | ||
| 27 | |||
| 28 | protected fun getDisplayString(params: ParamPackage, control: String): String { | ||
| 29 | val deviceName = params.get("display", "") | ||
| 30 | deviceName.ifEmpty { | ||
| 31 | return context.getString(R.string.not_set) | ||
| 32 | } | ||
| 33 | return "$deviceName: $control" | ||
| 34 | } | ||
| 35 | |||
| 36 | private fun getDirectionName(direction: String): String = | ||
| 37 | when (direction) { | ||
| 38 | "up" -> context.getString(R.string.up) | ||
| 39 | "down" -> context.getString(R.string.down) | ||
| 40 | "left" -> context.getString(R.string.left) | ||
| 41 | "right" -> context.getString(R.string.right) | ||
| 42 | else -> direction | ||
| 43 | } | ||
| 44 | |||
| 45 | protected fun buttonToText(param: ParamPackage): String { | ||
| 46 | if (!param.has("engine")) { | ||
| 47 | return context.getString(R.string.not_set) | ||
| 48 | } | ||
| 49 | |||
| 50 | val toggle = if (param.get("toggle", false)) "~" else "" | ||
| 51 | val inverted = if (param.get("inverted", false)) "!" else "" | ||
| 52 | val invert = if (param.get("invert", "+") == "-") "-" else "" | ||
| 53 | val turbo = if (param.get("turbo", false)) "$" else "" | ||
| 54 | val commonButtonName = NativeInput.getButtonName(param) | ||
| 55 | |||
| 56 | if (commonButtonName == ButtonName.Invalid) { | ||
| 57 | return context.getString(R.string.invalid) | ||
| 58 | } | ||
| 59 | |||
| 60 | if (commonButtonName == ButtonName.Engine) { | ||
| 61 | return param.get("engine", "") | ||
| 62 | } | ||
| 63 | |||
| 64 | if (commonButtonName == ButtonName.Value) { | ||
| 65 | if (param.has("hat")) { | ||
| 66 | val hat = getDirectionName(param.get("direction", "")) | ||
| 67 | return context.getString(R.string.qualified_hat, turbo, toggle, inverted, hat) | ||
| 68 | } | ||
| 69 | if (param.has("axis")) { | ||
| 70 | val axis = param.get("axis", "") | ||
| 71 | return context.getString( | ||
| 72 | R.string.qualified_button_stick_axis, | ||
| 73 | toggle, | ||
| 74 | inverted, | ||
| 75 | invert, | ||
| 76 | axis | ||
| 77 | ) | ||
| 78 | } | ||
| 79 | if (param.has("button")) { | ||
| 80 | val button = param.get("button", "") | ||
| 81 | return context.getString(R.string.qualified_button, turbo, toggle, inverted, button) | ||
| 82 | } | ||
| 83 | } | ||
| 84 | |||
| 85 | return context.getString(R.string.unknown) | ||
| 86 | } | ||
| 87 | |||
| 88 | protected fun analogToText(param: ParamPackage, direction: String): String { | ||
| 89 | if (!param.has("engine")) { | ||
| 90 | return context.getString(R.string.not_set) | ||
| 91 | } | ||
| 92 | |||
| 93 | if (param.get("engine", "") == "analog_from_button") { | ||
| 94 | return buttonToText(ParamPackage(param.get(direction, ""))) | ||
| 95 | } | ||
| 96 | |||
| 97 | if (!param.has("axis_x") || !param.has("axis_y")) { | ||
| 98 | return context.getString(R.string.unknown) | ||
| 99 | } | ||
| 100 | |||
| 101 | val xAxis = param.get("axis_x", "") | ||
| 102 | val yAxis = param.get("axis_y", "") | ||
| 103 | val xInvert = param.get("invert_x", "+") == "-" | ||
| 104 | val yInvert = param.get("invert_y", "+") == "-" | ||
| 105 | |||
| 106 | if (direction == "modifier") { | ||
| 107 | return context.getString(R.string.unused) | ||
| 108 | } | ||
| 109 | |||
| 110 | when (direction) { | ||
| 111 | "up" -> { | ||
| 112 | val yInvertString = if (yInvert) "+" else "-" | ||
| 113 | return context.getString(R.string.qualified_axis, yAxis, yInvertString) | ||
| 114 | } | ||
| 115 | |||
| 116 | "down" -> { | ||
| 117 | val yInvertString = if (yInvert) "-" else "+" | ||
| 118 | return context.getString(R.string.qualified_axis, yAxis, yInvertString) | ||
| 119 | } | ||
| 120 | |||
| 121 | "left" -> { | ||
| 122 | val xInvertString = if (xInvert) "+" else "-" | ||
| 123 | return context.getString(R.string.qualified_axis, xAxis, xInvertString) | ||
| 124 | } | ||
| 125 | |||
| 126 | "right" -> { | ||
| 127 | val xInvertString = if (xInvert) "-" else "+" | ||
| 128 | return context.getString(R.string.qualified_axis, xAxis, xInvertString) | ||
| 129 | } | ||
| 130 | } | ||
| 131 | |||
| 132 | return context.getString(R.string.unknown) | ||
| 133 | } | ||
| 134 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/IntSingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/IntSingleChoiceSetting.kt new file mode 100644 index 000000000..e024c793a --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/IntSingleChoiceSetting.kt | |||
| @@ -0,0 +1,38 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.model.view | ||
| 5 | |||
| 6 | import androidx.annotation.StringRes | ||
| 7 | import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting | ||
| 8 | |||
| 9 | class IntSingleChoiceSetting( | ||
| 10 | private val intSetting: AbstractIntSetting, | ||
| 11 | @StringRes titleId: Int = 0, | ||
| 12 | titleString: String = "", | ||
| 13 | @StringRes descriptionId: Int = 0, | ||
| 14 | descriptionString: String = "", | ||
| 15 | val choices: Array<String>, | ||
| 16 | val values: Array<Int> | ||
| 17 | ) : SettingsItem(intSetting, titleId, titleString, descriptionId, descriptionString) { | ||
| 18 | override val type = TYPE_INT_SINGLE_CHOICE | ||
| 19 | |||
| 20 | fun getValueAt(index: Int): Int = | ||
| 21 | if (values.indices.contains(index)) values[index] else -1 | ||
| 22 | |||
| 23 | fun getChoiceAt(index: Int): String = | ||
| 24 | if (choices.indices.contains(index)) choices[index] else "" | ||
| 25 | |||
| 26 | fun getSelectedValue(needsGlobal: Boolean = false) = intSetting.getInt(needsGlobal) | ||
| 27 | fun setSelectedValue(value: Int) = intSetting.setInt(value) | ||
| 28 | |||
| 29 | val selectedValueIndex: Int | ||
| 30 | get() { | ||
| 31 | for (i in values.indices) { | ||
| 32 | if (values[i] == getSelectedValue()) { | ||
| 33 | return i | ||
| 34 | } | ||
| 35 | } | ||
| 36 | return -1 | ||
| 37 | } | ||
| 38 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ModifierInputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ModifierInputSetting.kt new file mode 100644 index 000000000..a1db3cc87 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ModifierInputSetting.kt | |||
| @@ -0,0 +1,31 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.model.view | ||
| 5 | |||
| 6 | import androidx.annotation.StringRes | ||
| 7 | import org.yuzu.yuzu_emu.features.input.NativeInput | ||
| 8 | import org.yuzu.yuzu_emu.features.input.model.InputType | ||
| 9 | import org.yuzu.yuzu_emu.features.input.model.NativeAnalog | ||
| 10 | import org.yuzu.yuzu_emu.utils.ParamPackage | ||
| 11 | |||
| 12 | class ModifierInputSetting( | ||
| 13 | override val playerIndex: Int, | ||
| 14 | val nativeAnalog: NativeAnalog, | ||
| 15 | @StringRes titleId: Int = 0, | ||
| 16 | titleString: String = "" | ||
| 17 | ) : InputSetting(titleId, titleString) { | ||
| 18 | override val inputType = InputType.Button | ||
| 19 | |||
| 20 | override fun getSelectedValue(): String { | ||
| 21 | val analogParam = NativeInput.getStickParam(playerIndex, nativeAnalog) | ||
| 22 | val modifierParam = ParamPackage(analogParam.get("modifier", "")) | ||
| 23 | return buttonToText(modifierParam) | ||
| 24 | } | ||
| 25 | |||
| 26 | override fun setSelectedValue(param: ParamPackage) { | ||
| 27 | val newParam = NativeInput.getStickParam(playerIndex, nativeAnalog) | ||
| 28 | newParam.set("modifier", param.serialize()) | ||
| 29 | NativeInput.setStickParam(playerIndex, nativeAnalog, newParam) | ||
| 30 | } | ||
| 31 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt index 1005a2b7d..06f607424 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt | |||
| @@ -7,11 +7,11 @@ import androidx.annotation.DrawableRes | |||
| 7 | import androidx.annotation.StringRes | 7 | import androidx.annotation.StringRes |
| 8 | 8 | ||
| 9 | class RunnableSetting( | 9 | class RunnableSetting( |
| 10 | val isRuntimeRunnable: Boolean, | ||
| 11 | @StringRes titleId: Int = 0, | 10 | @StringRes titleId: Int = 0, |
| 12 | titleString: String = "", | 11 | titleString: String = "", |
| 13 | @StringRes descriptionId: Int = 0, | 12 | @StringRes descriptionId: Int = 0, |
| 14 | descriptionString: String = "", | 13 | descriptionString: String = "", |
| 14 | val isRunnable: Boolean, | ||
| 15 | @DrawableRes val iconId: Int = 0, | 15 | @DrawableRes val iconId: Int = 0, |
| 16 | val runnable: () -> Unit | 16 | val runnable: () -> Unit |
| 17 | ) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) { | 17 | ) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) { |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt new file mode 100644 index 000000000..16a1d0504 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt | |||
| @@ -0,0 +1,300 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.ui | ||
| 5 | |||
| 6 | import android.app.Dialog | ||
| 7 | import android.graphics.drawable.Animatable2 | ||
| 8 | import android.graphics.drawable.AnimatedVectorDrawable | ||
| 9 | import android.graphics.drawable.Drawable | ||
| 10 | import android.os.Bundle | ||
| 11 | import android.view.InputDevice | ||
| 12 | import android.view.KeyEvent | ||
| 13 | import android.view.LayoutInflater | ||
| 14 | import android.view.MotionEvent | ||
| 15 | import android.view.View | ||
| 16 | import android.view.ViewGroup | ||
| 17 | import androidx.fragment.app.DialogFragment | ||
| 18 | import androidx.fragment.app.activityViewModels | ||
| 19 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||
| 20 | import org.yuzu.yuzu_emu.R | ||
| 21 | import org.yuzu.yuzu_emu.databinding.DialogMappingBinding | ||
| 22 | import org.yuzu.yuzu_emu.features.input.NativeInput | ||
| 23 | import org.yuzu.yuzu_emu.features.input.model.NativeAnalog | ||
| 24 | import org.yuzu.yuzu_emu.features.input.model.NativeButton | ||
| 25 | import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting | ||
| 26 | import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting | ||
| 27 | import org.yuzu.yuzu_emu.features.settings.model.view.InputSetting | ||
| 28 | import org.yuzu.yuzu_emu.features.settings.model.view.ModifierInputSetting | ||
| 29 | import org.yuzu.yuzu_emu.utils.InputHandler | ||
| 30 | import org.yuzu.yuzu_emu.utils.ParamPackage | ||
| 31 | |||
| 32 | class InputDialogFragment : DialogFragment() { | ||
| 33 | private var inputAccepted = false | ||
| 34 | |||
| 35 | private var position: Int = 0 | ||
| 36 | |||
| 37 | private lateinit var inputSetting: InputSetting | ||
| 38 | |||
| 39 | private lateinit var binding: DialogMappingBinding | ||
| 40 | |||
| 41 | private val settingsViewModel: SettingsViewModel by activityViewModels() | ||
| 42 | |||
| 43 | override fun onCreate(savedInstanceState: Bundle?) { | ||
| 44 | super.onCreate(savedInstanceState) | ||
| 45 | if (settingsViewModel.clickedItem == null) dismiss() | ||
| 46 | |||
| 47 | position = requireArguments().getInt(POSITION) | ||
| 48 | |||
| 49 | InputHandler.updateControllerData() | ||
| 50 | } | ||
| 51 | |||
| 52 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { | ||
| 53 | inputSetting = settingsViewModel.clickedItem as InputSetting | ||
| 54 | binding = DialogMappingBinding.inflate(layoutInflater) | ||
| 55 | |||
| 56 | val builder = MaterialAlertDialogBuilder(requireContext()) | ||
| 57 | .setPositiveButton(android.R.string.cancel) { _, _ -> | ||
| 58 | NativeInput.stopMapping() | ||
| 59 | dismiss() | ||
| 60 | } | ||
| 61 | .setView(binding.root) | ||
| 62 | |||
| 63 | val playButtonMapAnimation = { twoDirections: Boolean -> | ||
| 64 | val stickAnimation: AnimatedVectorDrawable | ||
| 65 | val buttonAnimation: AnimatedVectorDrawable | ||
| 66 | binding.imageStickAnimation.apply { | ||
| 67 | val anim = if (twoDirections) { | ||
| 68 | R.drawable.stick_two_direction_anim | ||
| 69 | } else { | ||
| 70 | R.drawable.stick_one_direction_anim | ||
| 71 | } | ||
| 72 | setBackgroundResource(anim) | ||
| 73 | stickAnimation = background as AnimatedVectorDrawable | ||
| 74 | } | ||
| 75 | binding.imageButtonAnimation.apply { | ||
| 76 | setBackgroundResource(R.drawable.button_anim) | ||
| 77 | buttonAnimation = background as AnimatedVectorDrawable | ||
| 78 | } | ||
| 79 | stickAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() { | ||
| 80 | override fun onAnimationEnd(drawable: Drawable?) { | ||
| 81 | buttonAnimation.start() | ||
| 82 | } | ||
| 83 | }) | ||
| 84 | buttonAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() { | ||
| 85 | override fun onAnimationEnd(drawable: Drawable?) { | ||
| 86 | stickAnimation.start() | ||
| 87 | } | ||
| 88 | }) | ||
| 89 | stickAnimation.start() | ||
| 90 | } | ||
| 91 | |||
| 92 | when (val setting = inputSetting) { | ||
| 93 | is AnalogInputSetting -> { | ||
| 94 | when (setting.nativeAnalog) { | ||
| 95 | NativeAnalog.LStick -> builder.setTitle( | ||
| 96 | getString(R.string.map_control, getString(R.string.left_stick)) | ||
| 97 | ) | ||
| 98 | |||
| 99 | NativeAnalog.RStick -> builder.setTitle( | ||
| 100 | getString(R.string.map_control, getString(R.string.right_stick)) | ||
| 101 | ) | ||
| 102 | } | ||
| 103 | |||
| 104 | builder.setMessage(R.string.stick_map_description) | ||
| 105 | |||
| 106 | playButtonMapAnimation.invoke(true) | ||
| 107 | } | ||
| 108 | |||
| 109 | is ModifierInputSetting -> { | ||
| 110 | builder.setTitle(getString(R.string.map_control, setting.title)) | ||
| 111 | .setMessage(R.string.button_map_description) | ||
| 112 | playButtonMapAnimation.invoke(false) | ||
| 113 | } | ||
| 114 | |||
| 115 | is ButtonInputSetting -> { | ||
| 116 | if (setting.nativeButton == NativeButton.DUp || | ||
| 117 | setting.nativeButton == NativeButton.DDown || | ||
| 118 | setting.nativeButton == NativeButton.DLeft || | ||
| 119 | setting.nativeButton == NativeButton.DRight | ||
| 120 | ) { | ||
| 121 | builder.setTitle(getString(R.string.map_dpad_direction, setting.title)) | ||
| 122 | } else { | ||
| 123 | builder.setTitle(getString(R.string.map_control, setting.title)) | ||
| 124 | } | ||
| 125 | builder.setMessage(R.string.button_map_description) | ||
| 126 | playButtonMapAnimation.invoke(false) | ||
| 127 | } | ||
| 128 | } | ||
| 129 | |||
| 130 | return builder.create() | ||
| 131 | } | ||
| 132 | |||
| 133 | override fun onCreateView( | ||
| 134 | inflater: LayoutInflater, | ||
| 135 | container: ViewGroup?, | ||
| 136 | savedInstanceState: Bundle? | ||
| 137 | ): View { | ||
| 138 | return binding.root | ||
| 139 | } | ||
| 140 | |||
| 141 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| 142 | super.onViewCreated(view, savedInstanceState) | ||
| 143 | view.requestFocus() | ||
| 144 | view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() } | ||
| 145 | dialog?.setOnKeyListener { _, _, keyEvent -> onKeyEvent(keyEvent) } | ||
| 146 | binding.root.setOnGenericMotionListener { _, motionEvent -> onMotionEvent(motionEvent) } | ||
| 147 | NativeInput.beginMapping(inputSetting.inputType.int) | ||
| 148 | } | ||
| 149 | |||
| 150 | private fun onKeyEvent(event: KeyEvent): Boolean { | ||
| 151 | if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK && | ||
| 152 | event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD | ||
| 153 | ) { | ||
| 154 | return false | ||
| 155 | } | ||
| 156 | |||
| 157 | val action = when (event.action) { | ||
| 158 | KeyEvent.ACTION_DOWN -> NativeInput.ButtonState.PRESSED | ||
| 159 | KeyEvent.ACTION_UP -> NativeInput.ButtonState.RELEASED | ||
| 160 | else -> return false | ||
| 161 | } | ||
| 162 | val controllerData = | ||
| 163 | InputHandler.androidControllers[event.device.controllerNumber] ?: return false | ||
| 164 | NativeInput.onGamePadButtonEvent( | ||
| 165 | controllerData.getGUID(), | ||
| 166 | controllerData.getPort(), | ||
| 167 | event.keyCode, | ||
| 168 | action | ||
| 169 | ) | ||
| 170 | onInputReceived(event.device) | ||
| 171 | return true | ||
| 172 | } | ||
| 173 | |||
| 174 | private fun onMotionEvent(event: MotionEvent): Boolean { | ||
| 175 | if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK && | ||
| 176 | event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD | ||
| 177 | ) { | ||
| 178 | return false | ||
| 179 | } | ||
| 180 | |||
| 181 | // Temp workaround for DPads that give both axis and button input. The input system can't | ||
| 182 | // take in a specific axis direction for a binding so you lose half of the directions for a DPad. | ||
| 183 | |||
| 184 | val controllerData = | ||
| 185 | InputHandler.androidControllers[event.device.controllerNumber] ?: return false | ||
| 186 | event.device.motionRanges.forEach { | ||
| 187 | NativeInput.onGamePadAxisEvent( | ||
| 188 | controllerData.getGUID(), | ||
| 189 | controllerData.getPort(), | ||
| 190 | it.axis, | ||
| 191 | event.getAxisValue(it.axis) | ||
| 192 | ) | ||
| 193 | onInputReceived(event.device) | ||
| 194 | } | ||
| 195 | return true | ||
| 196 | } | ||
| 197 | |||
| 198 | private fun onInputReceived(device: InputDevice) { | ||
| 199 | val params = ParamPackage(NativeInput.getNextInput()) | ||
| 200 | if (params.has("engine") && isInputAcceptable(params) && !inputAccepted) { | ||
| 201 | inputAccepted = true | ||
| 202 | setResult(params, device) | ||
| 203 | } | ||
| 204 | } | ||
| 205 | |||
| 206 | private fun setResult(params: ParamPackage, device: InputDevice) { | ||
| 207 | NativeInput.stopMapping() | ||
| 208 | params.set("display", "${device.name} ${params.get("port", 0)}") | ||
| 209 | when (val item = settingsViewModel.clickedItem as InputSetting) { | ||
| 210 | is ModifierInputSetting, | ||
| 211 | is ButtonInputSetting -> { | ||
| 212 | // Invert DPad up and left bindings by default | ||
| 213 | val tempSetting = inputSetting as? ButtonInputSetting | ||
| 214 | if (tempSetting != null) { | ||
| 215 | if (tempSetting.nativeButton == NativeButton.DUp || | ||
| 216 | tempSetting.nativeButton == NativeButton.DLeft && | ||
| 217 | params.has("axis") | ||
| 218 | ) { | ||
| 219 | params.set("invert", "-") | ||
| 220 | } | ||
| 221 | } | ||
| 222 | |||
| 223 | item.setSelectedValue(params) | ||
| 224 | settingsViewModel.setAdapterItemChanged(position) | ||
| 225 | } | ||
| 226 | |||
| 227 | is AnalogInputSetting -> { | ||
| 228 | var analogParam = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog) | ||
| 229 | analogParam = adjustAnalogParam(params, analogParam, item.analogDirection.param) | ||
| 230 | |||
| 231 | // Invert Y-Axis by default | ||
| 232 | analogParam.set("invert_y", "-") | ||
| 233 | |||
| 234 | item.setSelectedValue(analogParam) | ||
| 235 | settingsViewModel.setReloadListAndNotifyDataset(true) | ||
| 236 | } | ||
| 237 | } | ||
| 238 | dismiss() | ||
| 239 | } | ||
| 240 | |||
| 241 | private fun adjustAnalogParam( | ||
| 242 | inputParam: ParamPackage, | ||
| 243 | analogParam: ParamPackage, | ||
| 244 | buttonName: String | ||
| 245 | ): ParamPackage { | ||
| 246 | // The poller returned a complete axis, so set all the buttons | ||
| 247 | if (inputParam.has("axis_x") && inputParam.has("axis_y")) { | ||
| 248 | return inputParam | ||
| 249 | } | ||
| 250 | |||
| 251 | // Check if the current configuration has either no engine or an axis binding. | ||
| 252 | // Clears out the old binding and adds one with analog_from_button. | ||
| 253 | if (!analogParam.has("engine") || analogParam.has("axis_x") || analogParam.has("axis_y")) { | ||
| 254 | analogParam.clear() | ||
| 255 | analogParam.set("engine", "analog_from_button") | ||
| 256 | } | ||
| 257 | analogParam.set(buttonName, inputParam.serialize()) | ||
| 258 | return analogParam | ||
| 259 | } | ||
| 260 | |||
| 261 | private fun isInputAcceptable(params: ParamPackage): Boolean { | ||
| 262 | if (InputHandler.registeredControllers.size == 1) { | ||
| 263 | return true | ||
| 264 | } | ||
| 265 | |||
| 266 | if (params.has("motion")) { | ||
| 267 | return true | ||
| 268 | } | ||
| 269 | |||
| 270 | val currentDevice = settingsViewModel.getCurrentDeviceParams(params) | ||
| 271 | if (currentDevice.get("engine", "any") == "any") { | ||
| 272 | return true | ||
| 273 | } | ||
| 274 | |||
| 275 | val guidMatch = params.get("guid", "") == currentDevice.get("guid", "") || | ||
| 276 | params.get("guid", "") == currentDevice.get("guid2", "") | ||
| 277 | return params.get("engine", "") == currentDevice.get("engine", "") && | ||
| 278 | guidMatch && | ||
| 279 | params.get("port", 0) == currentDevice.get("port", 0) | ||
| 280 | } | ||
| 281 | |||
| 282 | companion object { | ||
| 283 | const val TAG = "InputDialogFragment" | ||
| 284 | |||
| 285 | const val POSITION = "Position" | ||
| 286 | |||
| 287 | fun newInstance( | ||
| 288 | inputMappingViewModel: SettingsViewModel, | ||
| 289 | setting: InputSetting, | ||
| 290 | position: Int | ||
| 291 | ): InputDialogFragment { | ||
| 292 | inputMappingViewModel.clickedItem = setting | ||
| 293 | val args = Bundle() | ||
| 294 | args.putInt(POSITION, position) | ||
| 295 | val fragment = InputDialogFragment() | ||
| 296 | fragment.arguments = args | ||
| 297 | return fragment | ||
| 298 | } | ||
| 299 | } | ||
| 300 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileAdapter.kt new file mode 100644 index 000000000..5656e9d8d --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileAdapter.kt | |||
| @@ -0,0 +1,68 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.ui | ||
| 5 | |||
| 6 | import android.view.LayoutInflater | ||
| 7 | import android.view.View | ||
| 8 | import android.view.ViewGroup | ||
| 9 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 10 | import org.yuzu.yuzu_emu.adapters.AbstractListAdapter | ||
| 11 | import org.yuzu.yuzu_emu.databinding.ListItemInputProfileBinding | ||
| 12 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder | ||
| 13 | import org.yuzu.yuzu_emu.R | ||
| 14 | |||
| 15 | class InputProfileAdapter(options: List<ProfileItem>) : | ||
| 16 | AbstractListAdapter<ProfileItem, AbstractViewHolder<ProfileItem>>(options) { | ||
| 17 | override fun onCreateViewHolder( | ||
| 18 | parent: ViewGroup, | ||
| 19 | viewType: Int | ||
| 20 | ): AbstractViewHolder<ProfileItem> { | ||
| 21 | ListItemInputProfileBinding.inflate(LayoutInflater.from(parent.context), parent, false) | ||
| 22 | .also { return InputProfileViewHolder(it) } | ||
| 23 | } | ||
| 24 | |||
| 25 | inner class InputProfileViewHolder(val binding: ListItemInputProfileBinding) : | ||
| 26 | AbstractViewHolder<ProfileItem>(binding) { | ||
| 27 | override fun bind(model: ProfileItem) { | ||
| 28 | when (model) { | ||
| 29 | is ExistingProfileItem -> { | ||
| 30 | binding.title.text = model.name | ||
| 31 | binding.buttonNew.visibility = View.GONE | ||
| 32 | binding.buttonDelete.visibility = View.VISIBLE | ||
| 33 | binding.buttonDelete.setOnClickListener { model.deleteProfile.invoke() } | ||
| 34 | binding.buttonSave.visibility = View.VISIBLE | ||
| 35 | binding.buttonSave.setOnClickListener { model.saveProfile.invoke() } | ||
| 36 | binding.buttonLoad.visibility = View.VISIBLE | ||
| 37 | binding.buttonLoad.setOnClickListener { model.loadProfile.invoke() } | ||
| 38 | } | ||
| 39 | |||
| 40 | is NewProfileItem -> { | ||
| 41 | binding.title.text = model.name | ||
| 42 | binding.buttonNew.visibility = View.VISIBLE | ||
| 43 | binding.buttonNew.setOnClickListener { model.createNewProfile.invoke() } | ||
| 44 | binding.buttonSave.visibility = View.GONE | ||
| 45 | binding.buttonDelete.visibility = View.GONE | ||
| 46 | binding.buttonLoad.visibility = View.GONE | ||
| 47 | } | ||
| 48 | } | ||
| 49 | } | ||
| 50 | } | ||
| 51 | } | ||
| 52 | |||
| 53 | sealed interface ProfileItem { | ||
| 54 | val name: String | ||
| 55 | } | ||
| 56 | |||
| 57 | data class NewProfileItem( | ||
| 58 | val createNewProfile: () -> Unit | ||
| 59 | ) : ProfileItem { | ||
| 60 | override val name: String = YuzuApplication.appContext.getString(R.string.create_new_profile) | ||
| 61 | } | ||
| 62 | |||
| 63 | data class ExistingProfileItem( | ||
| 64 | override val name: String, | ||
| 65 | val deleteProfile: () -> Unit, | ||
| 66 | val saveProfile: () -> Unit, | ||
| 67 | val loadProfile: () -> Unit | ||
| 68 | ) : ProfileItem | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt new file mode 100644 index 000000000..9b24d41c1 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt | |||
| @@ -0,0 +1,155 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.ui | ||
| 5 | |||
| 6 | import android.app.Dialog | ||
| 7 | import android.os.Bundle | ||
| 8 | import android.view.LayoutInflater | ||
| 9 | import android.view.View | ||
| 10 | import android.view.ViewGroup | ||
| 11 | import android.widget.Toast | ||
| 12 | import androidx.fragment.app.DialogFragment | ||
| 13 | import androidx.fragment.app.activityViewModels | ||
| 14 | import androidx.lifecycle.Lifecycle | ||
| 15 | import androidx.lifecycle.lifecycleScope | ||
| 16 | import androidx.lifecycle.repeatOnLifecycle | ||
| 17 | import androidx.recyclerview.widget.LinearLayoutManager | ||
| 18 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||
| 19 | import kotlinx.coroutines.launch | ||
| 20 | import org.yuzu.yuzu_emu.R | ||
| 21 | import org.yuzu.yuzu_emu.databinding.DialogInputProfilesBinding | ||
| 22 | import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting | ||
| 23 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment | ||
| 24 | |||
| 25 | class InputProfileDialogFragment : DialogFragment() { | ||
| 26 | private var position = 0 | ||
| 27 | |||
| 28 | private val settingsViewModel: SettingsViewModel by activityViewModels() | ||
| 29 | |||
| 30 | private lateinit var binding: DialogInputProfilesBinding | ||
| 31 | |||
| 32 | private lateinit var setting: InputProfileSetting | ||
| 33 | |||
| 34 | override fun onCreate(savedInstanceState: Bundle?) { | ||
| 35 | super.onCreate(savedInstanceState) | ||
| 36 | position = requireArguments().getInt(POSITION) | ||
| 37 | } | ||
| 38 | |||
| 39 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { | ||
| 40 | binding = DialogInputProfilesBinding.inflate(layoutInflater) | ||
| 41 | |||
| 42 | setting = settingsViewModel.clickedItem as InputProfileSetting | ||
| 43 | val options = mutableListOf<ProfileItem>().apply { | ||
| 44 | add( | ||
| 45 | NewProfileItem( | ||
| 46 | createNewProfile = { | ||
| 47 | NewInputProfileDialogFragment.newInstance( | ||
| 48 | settingsViewModel, | ||
| 49 | setting, | ||
| 50 | position | ||
| 51 | ).show(parentFragmentManager, NewInputProfileDialogFragment.TAG) | ||
| 52 | dismiss() | ||
| 53 | } | ||
| 54 | ) | ||
| 55 | ) | ||
| 56 | |||
| 57 | val onActionDismiss = { | ||
| 58 | settingsViewModel.setReloadListAndNotifyDataset(true) | ||
| 59 | dismiss() | ||
| 60 | } | ||
| 61 | setting.getProfileNames().forEach { | ||
| 62 | add( | ||
| 63 | ExistingProfileItem( | ||
| 64 | it, | ||
| 65 | deleteProfile = { | ||
| 66 | settingsViewModel.setShouldShowDeleteProfileDialog(it) | ||
| 67 | }, | ||
| 68 | saveProfile = { | ||
| 69 | if (!setting.saveProfile(it)) { | ||
| 70 | Toast.makeText( | ||
| 71 | requireContext(), | ||
| 72 | R.string.failed_to_save_profile, | ||
| 73 | Toast.LENGTH_SHORT | ||
| 74 | ).show() | ||
| 75 | } | ||
| 76 | onActionDismiss.invoke() | ||
| 77 | }, | ||
| 78 | loadProfile = { | ||
| 79 | if (!setting.loadProfile(it)) { | ||
| 80 | Toast.makeText( | ||
| 81 | requireContext(), | ||
| 82 | R.string.failed_to_load_profile, | ||
| 83 | Toast.LENGTH_SHORT | ||
| 84 | ).show() | ||
| 85 | } | ||
| 86 | onActionDismiss.invoke() | ||
| 87 | } | ||
| 88 | ) | ||
| 89 | ) | ||
| 90 | } | ||
| 91 | } | ||
| 92 | binding.listProfiles.apply { | ||
| 93 | layoutManager = LinearLayoutManager(requireContext()) | ||
| 94 | adapter = InputProfileAdapter(options) | ||
| 95 | } | ||
| 96 | |||
| 97 | return MaterialAlertDialogBuilder(requireContext()) | ||
| 98 | .setView(binding.root) | ||
| 99 | .create() | ||
| 100 | } | ||
| 101 | |||
| 102 | override fun onCreateView( | ||
| 103 | inflater: LayoutInflater, | ||
| 104 | container: ViewGroup?, | ||
| 105 | savedInstanceState: Bundle? | ||
| 106 | ): View { | ||
| 107 | return binding.root | ||
| 108 | } | ||
| 109 | |||
| 110 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| 111 | super.onViewCreated(view, savedInstanceState) | ||
| 112 | |||
| 113 | viewLifecycleOwner.lifecycleScope.launch { | ||
| 114 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 115 | settingsViewModel.shouldShowDeleteProfileDialog.collect { | ||
| 116 | if (it.isNotEmpty()) { | ||
| 117 | MessageDialogFragment.newInstance( | ||
| 118 | activity = requireActivity(), | ||
| 119 | titleId = R.string.delete_input_profile, | ||
| 120 | descriptionId = R.string.delete_input_profile_description, | ||
| 121 | positiveAction = { | ||
| 122 | setting.deleteProfile(it) | ||
| 123 | settingsViewModel.setReloadListAndNotifyDataset(true) | ||
| 124 | }, | ||
| 125 | negativeAction = {}, | ||
| 126 | negativeButtonTitleId = android.R.string.cancel | ||
| 127 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 128 | settingsViewModel.setShouldShowDeleteProfileDialog("") | ||
| 129 | dismiss() | ||
| 130 | } | ||
| 131 | } | ||
| 132 | } | ||
| 133 | } | ||
| 134 | } | ||
| 135 | |||
| 136 | companion object { | ||
| 137 | const val TAG = "InputProfileDialogFragment" | ||
| 138 | |||
| 139 | const val POSITION = "Position" | ||
| 140 | |||
| 141 | fun newInstance( | ||
| 142 | settingsViewModel: SettingsViewModel, | ||
| 143 | profileSetting: InputProfileSetting, | ||
| 144 | position: Int | ||
| 145 | ): InputProfileDialogFragment { | ||
| 146 | settingsViewModel.clickedItem = profileSetting | ||
| 147 | |||
| 148 | val args = Bundle() | ||
| 149 | args.putInt(POSITION, position) | ||
| 150 | val fragment = InputProfileDialogFragment() | ||
| 151 | fragment.arguments = args | ||
| 152 | return fragment | ||
| 153 | } | ||
| 154 | } | ||
| 155 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/NewInputProfileDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/NewInputProfileDialogFragment.kt new file mode 100644 index 000000000..6e52bea80 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/NewInputProfileDialogFragment.kt | |||
| @@ -0,0 +1,79 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.ui | ||
| 5 | |||
| 6 | import android.app.Dialog | ||
| 7 | import android.os.Bundle | ||
| 8 | import android.widget.Toast | ||
| 9 | import androidx.fragment.app.DialogFragment | ||
| 10 | import androidx.fragment.app.activityViewModels | ||
| 11 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||
| 12 | import org.yuzu.yuzu_emu.databinding.DialogEditTextBinding | ||
| 13 | import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting | ||
| 14 | import org.yuzu.yuzu_emu.R | ||
| 15 | |||
| 16 | class NewInputProfileDialogFragment : DialogFragment() { | ||
| 17 | private var position = 0 | ||
| 18 | |||
| 19 | private val settingsViewModel: SettingsViewModel by activityViewModels() | ||
| 20 | |||
| 21 | private lateinit var binding: DialogEditTextBinding | ||
| 22 | |||
| 23 | override fun onCreate(savedInstanceState: Bundle?) { | ||
| 24 | super.onCreate(savedInstanceState) | ||
| 25 | position = requireArguments().getInt(POSITION) | ||
| 26 | } | ||
| 27 | |||
| 28 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { | ||
| 29 | binding = DialogEditTextBinding.inflate(layoutInflater) | ||
| 30 | |||
| 31 | val setting = settingsViewModel.clickedItem as InputProfileSetting | ||
| 32 | return MaterialAlertDialogBuilder(requireContext()) | ||
| 33 | .setTitle(R.string.enter_profile_name) | ||
| 34 | .setPositiveButton(android.R.string.ok) { _, _ -> | ||
| 35 | val profileName = binding.editText.text.toString() | ||
| 36 | if (!setting.isProfileNameValid(profileName)) { | ||
| 37 | Toast.makeText( | ||
| 38 | requireContext(), | ||
| 39 | R.string.invalid_profile_name, | ||
| 40 | Toast.LENGTH_SHORT | ||
| 41 | ).show() | ||
| 42 | return@setPositiveButton | ||
| 43 | } | ||
| 44 | |||
| 45 | if (!setting.createProfile(profileName)) { | ||
| 46 | Toast.makeText( | ||
| 47 | requireContext(), | ||
| 48 | R.string.profile_name_already_exists, | ||
| 49 | Toast.LENGTH_SHORT | ||
| 50 | ).show() | ||
| 51 | } else { | ||
| 52 | settingsViewModel.setAdapterItemChanged(position) | ||
| 53 | } | ||
| 54 | } | ||
| 55 | .setNegativeButton(android.R.string.cancel, null) | ||
| 56 | .setView(binding.root) | ||
| 57 | .show() | ||
| 58 | } | ||
| 59 | |||
| 60 | companion object { | ||
| 61 | const val TAG = "NewInputProfileDialogFragment" | ||
| 62 | |||
| 63 | const val POSITION = "Position" | ||
| 64 | |||
| 65 | fun newInstance( | ||
| 66 | settingsViewModel: SettingsViewModel, | ||
| 67 | profileSetting: InputProfileSetting, | ||
| 68 | position: Int | ||
| 69 | ): NewInputProfileDialogFragment { | ||
| 70 | settingsViewModel.clickedItem = profileSetting | ||
| 71 | |||
| 72 | val args = Bundle() | ||
| 73 | args.putInt(POSITION, position) | ||
| 74 | val fragment = NewInputProfileDialogFragment() | ||
| 75 | fragment.arguments = args | ||
| 76 | return fragment | ||
| 77 | } | ||
| 78 | } | ||
| 79 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt index 6f072241a..681a18b3b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt | |||
| @@ -25,9 +25,9 @@ import org.yuzu.yuzu_emu.NativeLibrary | |||
| 25 | import java.io.IOException | 25 | import java.io.IOException |
| 26 | import org.yuzu.yuzu_emu.R | 26 | import org.yuzu.yuzu_emu.R |
| 27 | import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding | 27 | import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding |
| 28 | import org.yuzu.yuzu_emu.features.input.NativeInput | ||
| 28 | import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile | 29 | import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile |
| 29 | import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment | 30 | import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment |
| 30 | import org.yuzu.yuzu_emu.model.SettingsViewModel | ||
| 31 | import org.yuzu.yuzu_emu.utils.* | 31 | import org.yuzu.yuzu_emu.utils.* |
| 32 | 32 | ||
| 33 | class SettingsActivity : AppCompatActivity() { | 33 | class SettingsActivity : AppCompatActivity() { |
| @@ -137,6 +137,7 @@ class SettingsActivity : AppCompatActivity() { | |||
| 137 | super.onStop() | 137 | super.onStop() |
| 138 | Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...") | 138 | Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...") |
| 139 | if (isFinishing) { | 139 | if (isFinishing) { |
| 140 | NativeInput.reloadInputDevices() | ||
| 140 | NativeLibrary.applySettings() | 141 | NativeLibrary.applySettings() |
| 141 | if (args.game == null) { | 142 | if (args.game == null) { |
| 142 | NativeConfig.saveGlobalConfig() | 143 | NativeConfig.saveGlobalConfig() |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt index be9b3031b..45c8faa10 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt | |||
| @@ -8,12 +8,11 @@ import android.icu.util.Calendar | |||
| 8 | import android.icu.util.TimeZone | 8 | import android.icu.util.TimeZone |
| 9 | import android.text.format.DateFormat | 9 | import android.text.format.DateFormat |
| 10 | import android.view.LayoutInflater | 10 | import android.view.LayoutInflater |
| 11 | import android.view.View | ||
| 11 | import android.view.ViewGroup | 12 | import android.view.ViewGroup |
| 13 | import android.widget.PopupMenu | ||
| 12 | import androidx.fragment.app.Fragment | 14 | import androidx.fragment.app.Fragment |
| 13 | import androidx.lifecycle.Lifecycle | ||
| 14 | import androidx.lifecycle.ViewModelProvider | 15 | import androidx.lifecycle.ViewModelProvider |
| 15 | import androidx.lifecycle.lifecycleScope | ||
| 16 | import androidx.lifecycle.repeatOnLifecycle | ||
| 17 | import androidx.navigation.findNavController | 16 | import androidx.navigation.findNavController |
| 18 | import androidx.recyclerview.widget.AsyncDifferConfig | 17 | import androidx.recyclerview.widget.AsyncDifferConfig |
| 19 | import androidx.recyclerview.widget.DiffUtil | 18 | import androidx.recyclerview.widget.DiffUtil |
| @@ -21,16 +20,18 @@ import androidx.recyclerview.widget.ListAdapter | |||
| 21 | import com.google.android.material.datepicker.MaterialDatePicker | 20 | import com.google.android.material.datepicker.MaterialDatePicker |
| 22 | import com.google.android.material.timepicker.MaterialTimePicker | 21 | import com.google.android.material.timepicker.MaterialTimePicker |
| 23 | import com.google.android.material.timepicker.TimeFormat | 22 | import com.google.android.material.timepicker.TimeFormat |
| 24 | import kotlinx.coroutines.launch | ||
| 25 | import org.yuzu.yuzu_emu.R | 23 | import org.yuzu.yuzu_emu.R |
| 26 | import org.yuzu.yuzu_emu.SettingsNavigationDirections | 24 | import org.yuzu.yuzu_emu.SettingsNavigationDirections |
| 27 | import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding | 25 | import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding |
| 26 | import org.yuzu.yuzu_emu.databinding.ListItemSettingInputBinding | ||
| 28 | import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding | 27 | import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding |
| 29 | import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding | 28 | import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding |
| 29 | import org.yuzu.yuzu_emu.features.input.NativeInput | ||
| 30 | import org.yuzu.yuzu_emu.features.input.model.AnalogDirection | ||
| 31 | import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting | ||
| 30 | import org.yuzu.yuzu_emu.features.settings.model.view.* | 32 | import org.yuzu.yuzu_emu.features.settings.model.view.* |
| 31 | import org.yuzu.yuzu_emu.features.settings.ui.viewholder.* | 33 | import org.yuzu.yuzu_emu.features.settings.ui.viewholder.* |
| 32 | import org.yuzu.yuzu_emu.fragments.SettingsDialogFragment | 34 | import org.yuzu.yuzu_emu.utils.ParamPackage |
| 33 | import org.yuzu.yuzu_emu.model.SettingsViewModel | ||
| 34 | 35 | ||
| 35 | class SettingsAdapter( | 36 | class SettingsAdapter( |
| 36 | private val fragment: Fragment, | 37 | private val fragment: Fragment, |
| @@ -41,19 +42,6 @@ class SettingsAdapter( | |||
| 41 | private val settingsViewModel: SettingsViewModel | 42 | private val settingsViewModel: SettingsViewModel |
| 42 | get() = ViewModelProvider(fragment.requireActivity())[SettingsViewModel::class.java] | 43 | get() = ViewModelProvider(fragment.requireActivity())[SettingsViewModel::class.java] |
| 43 | 44 | ||
| 44 | init { | ||
| 45 | fragment.viewLifecycleOwner.lifecycleScope.launch { | ||
| 46 | fragment.repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
| 47 | settingsViewModel.adapterItemChanged.collect { | ||
| 48 | if (it != -1) { | ||
| 49 | notifyItemChanged(it) | ||
| 50 | settingsViewModel.setAdapterItemChanged(-1) | ||
| 51 | } | ||
| 52 | } | ||
| 53 | } | ||
| 54 | } | ||
| 55 | } | ||
| 56 | |||
| 57 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder { | 45 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder { |
| 58 | val inflater = LayoutInflater.from(parent.context) | 46 | val inflater = LayoutInflater.from(parent.context) |
| 59 | return when (viewType) { | 47 | return when (viewType) { |
| @@ -85,8 +73,19 @@ class SettingsAdapter( | |||
| 85 | RunnableViewHolder(ListItemSettingBinding.inflate(inflater), this) | 73 | RunnableViewHolder(ListItemSettingBinding.inflate(inflater), this) |
| 86 | } | 74 | } |
| 87 | 75 | ||
| 76 | SettingsItem.TYPE_INPUT -> { | ||
| 77 | InputViewHolder(ListItemSettingInputBinding.inflate(inflater), this) | ||
| 78 | } | ||
| 79 | |||
| 80 | SettingsItem.TYPE_INT_SINGLE_CHOICE -> { | ||
| 81 | SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this) | ||
| 82 | } | ||
| 83 | |||
| 84 | SettingsItem.TYPE_INPUT_PROFILE -> { | ||
| 85 | InputProfileViewHolder(ListItemSettingBinding.inflate(inflater), this) | ||
| 86 | } | ||
| 87 | |||
| 88 | else -> { | 88 | else -> { |
| 89 | // TODO: Create an error view since we can't return null now | ||
| 90 | HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this) | 89 | HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this) |
| 91 | } | 90 | } |
| 92 | } | 91 | } |
| @@ -126,6 +125,15 @@ class SettingsAdapter( | |||
| 126 | ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) | 125 | ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) |
| 127 | } | 126 | } |
| 128 | 127 | ||
| 128 | fun onIntSingleChoiceClick(item: IntSingleChoiceSetting, position: Int) { | ||
| 129 | SettingsDialogFragment.newInstance( | ||
| 130 | settingsViewModel, | ||
| 131 | item, | ||
| 132 | SettingsItem.TYPE_INT_SINGLE_CHOICE, | ||
| 133 | position | ||
| 134 | ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) | ||
| 135 | } | ||
| 136 | |||
| 129 | fun onDateTimeClick(item: DateTimeSetting, position: Int) { | 137 | fun onDateTimeClick(item: DateTimeSetting, position: Int) { |
| 130 | val storedTime = item.getValue() * 1000 | 138 | val storedTime = item.getValue() * 1000 |
| 131 | 139 | ||
| @@ -185,6 +193,205 @@ class SettingsAdapter( | |||
| 185 | fragment.view?.findNavController()?.navigate(action) | 193 | fragment.view?.findNavController()?.navigate(action) |
| 186 | } | 194 | } |
| 187 | 195 | ||
| 196 | fun onInputProfileClick(item: InputProfileSetting, position: Int) { | ||
| 197 | InputProfileDialogFragment.newInstance( | ||
| 198 | settingsViewModel, | ||
| 199 | item, | ||
| 200 | position | ||
| 201 | ).show(fragment.childFragmentManager, InputProfileDialogFragment.TAG) | ||
| 202 | } | ||
| 203 | |||
| 204 | fun onInputClick(item: InputSetting, position: Int) { | ||
| 205 | InputDialogFragment.newInstance( | ||
| 206 | settingsViewModel, | ||
| 207 | item, | ||
| 208 | position | ||
| 209 | ).show(fragment.childFragmentManager, InputDialogFragment.TAG) | ||
| 210 | } | ||
| 211 | |||
| 212 | fun onInputOptionsClick(anchor: View, item: InputSetting, position: Int) { | ||
| 213 | val popup = PopupMenu(context, anchor) | ||
| 214 | popup.menuInflater.inflate(R.menu.menu_input_options, popup.menu) | ||
| 215 | |||
| 216 | popup.menu.apply { | ||
| 217 | val invertAxis = findItem(R.id.invert_axis) | ||
| 218 | val invertButton = findItem(R.id.invert_button) | ||
| 219 | val toggleButton = findItem(R.id.toggle_button) | ||
| 220 | val turboButton = findItem(R.id.turbo_button) | ||
| 221 | val setThreshold = findItem(R.id.set_threshold) | ||
| 222 | val toggleAxis = findItem(R.id.toggle_axis) | ||
| 223 | when (item) { | ||
| 224 | is AnalogInputSetting -> { | ||
| 225 | val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog) | ||
| 226 | |||
| 227 | invertAxis.isVisible = true | ||
| 228 | invertAxis.isCheckable = true | ||
| 229 | invertAxis.isChecked = when (item.analogDirection) { | ||
| 230 | AnalogDirection.Left, AnalogDirection.Right -> { | ||
| 231 | params.get("invert_x", "+") == "-" | ||
| 232 | } | ||
| 233 | |||
| 234 | AnalogDirection.Up, AnalogDirection.Down -> { | ||
| 235 | params.get("invert_y", "+") == "-" | ||
| 236 | } | ||
| 237 | } | ||
| 238 | invertAxis.setOnMenuItemClickListener { | ||
| 239 | if (item.analogDirection == AnalogDirection.Left || | ||
| 240 | item.analogDirection == AnalogDirection.Right | ||
| 241 | ) { | ||
| 242 | val invertValue = params.get("invert_x", "+") == "-" | ||
| 243 | val invertString = if (invertValue) "+" else "-" | ||
| 244 | params.set("invert_x", invertString) | ||
| 245 | } else if ( | ||
| 246 | item.analogDirection == AnalogDirection.Up || | ||
| 247 | item.analogDirection == AnalogDirection.Down | ||
| 248 | ) { | ||
| 249 | val invertValue = params.get("invert_y", "+") == "-" | ||
| 250 | val invertString = if (invertValue) "+" else "-" | ||
| 251 | params.set("invert_y", invertString) | ||
| 252 | } | ||
| 253 | true | ||
| 254 | } | ||
| 255 | |||
| 256 | popup.setOnDismissListener { | ||
| 257 | NativeInput.setStickParam(item.playerIndex, item.nativeAnalog, params) | ||
| 258 | settingsViewModel.setDatasetChanged(true) | ||
| 259 | } | ||
| 260 | } | ||
| 261 | |||
| 262 | is ButtonInputSetting -> { | ||
| 263 | val params = NativeInput.getButtonParam(item.playerIndex, item.nativeButton) | ||
| 264 | if (params.has("code") || params.has("button") || params.has("hat")) { | ||
| 265 | val buttonInvert = params.get("inverted", false) | ||
| 266 | invertButton.isVisible = true | ||
| 267 | invertButton.isCheckable = true | ||
| 268 | invertButton.isChecked = buttonInvert | ||
| 269 | invertButton.setOnMenuItemClickListener { | ||
| 270 | params.set("inverted", !buttonInvert) | ||
| 271 | true | ||
| 272 | } | ||
| 273 | |||
| 274 | val toggle = params.get("toggle", false) | ||
| 275 | toggleButton.isVisible = true | ||
| 276 | toggleButton.isCheckable = true | ||
| 277 | toggleButton.isChecked = toggle | ||
| 278 | toggleButton.setOnMenuItemClickListener { | ||
| 279 | params.set("toggle", !toggle) | ||
| 280 | true | ||
| 281 | } | ||
| 282 | |||
| 283 | val turbo = params.get("turbo", false) | ||
| 284 | turboButton.isVisible = true | ||
| 285 | turboButton.isCheckable = true | ||
| 286 | turboButton.isChecked = turbo | ||
| 287 | turboButton.setOnMenuItemClickListener { | ||
| 288 | params.set("turbo", !turbo) | ||
| 289 | true | ||
| 290 | } | ||
| 291 | } else if (params.has("axis")) { | ||
| 292 | val axisInvert = params.get("invert", "+") == "-" | ||
| 293 | invertAxis.isVisible = true | ||
| 294 | invertAxis.isCheckable = true | ||
| 295 | invertAxis.isChecked = axisInvert | ||
| 296 | invertAxis.setOnMenuItemClickListener { | ||
| 297 | params.set("invert", if (!axisInvert) "-" else "+") | ||
| 298 | true | ||
| 299 | } | ||
| 300 | |||
| 301 | val buttonInvert = params.get("inverted", false) | ||
| 302 | invertButton.isVisible = true | ||
| 303 | invertButton.isCheckable = true | ||
| 304 | invertButton.isChecked = buttonInvert | ||
| 305 | invertButton.setOnMenuItemClickListener { | ||
| 306 | params.set("inverted", !buttonInvert) | ||
| 307 | true | ||
| 308 | } | ||
| 309 | |||
| 310 | setThreshold.isVisible = true | ||
| 311 | val thresholdSetting = object : AbstractIntSetting { | ||
| 312 | override val key = "" | ||
| 313 | |||
| 314 | override fun getInt(needsGlobal: Boolean): Int = | ||
| 315 | (params.get("threshold", 0.5f) * 100).toInt() | ||
| 316 | |||
| 317 | override fun setInt(value: Int) { | ||
| 318 | params.set("threshold", value.toFloat() / 100) | ||
| 319 | NativeInput.setButtonParam( | ||
| 320 | item.playerIndex, | ||
| 321 | item.nativeButton, | ||
| 322 | params | ||
| 323 | ) | ||
| 324 | } | ||
| 325 | |||
| 326 | override val defaultValue = 50 | ||
| 327 | |||
| 328 | override fun getValueAsString(needsGlobal: Boolean): String = | ||
| 329 | getInt(needsGlobal).toString() | ||
| 330 | |||
| 331 | override fun reset() = setInt(defaultValue) | ||
| 332 | } | ||
| 333 | setThreshold.setOnMenuItemClickListener { | ||
| 334 | onSliderClick( | ||
| 335 | SliderSetting(thresholdSetting, R.string.set_threshold), | ||
| 336 | position | ||
| 337 | ) | ||
| 338 | true | ||
| 339 | } | ||
| 340 | |||
| 341 | val axisToggle = params.get("toggle", false) | ||
| 342 | toggleAxis.isVisible = true | ||
| 343 | toggleAxis.isCheckable = true | ||
| 344 | toggleAxis.isChecked = axisToggle | ||
| 345 | toggleAxis.setOnMenuItemClickListener { | ||
| 346 | params.set("toggle", !axisToggle) | ||
| 347 | true | ||
| 348 | } | ||
| 349 | } | ||
| 350 | |||
| 351 | popup.setOnDismissListener { | ||
| 352 | NativeInput.setButtonParam(item.playerIndex, item.nativeButton, params) | ||
| 353 | settingsViewModel.setAdapterItemChanged(position) | ||
| 354 | } | ||
| 355 | } | ||
| 356 | |||
| 357 | is ModifierInputSetting -> { | ||
| 358 | val stickParams = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog) | ||
| 359 | val modifierParams = ParamPackage(stickParams.get("modifier", "")) | ||
| 360 | |||
| 361 | val invert = modifierParams.get("inverted", false) | ||
| 362 | invertButton.isVisible = true | ||
| 363 | invertButton.isCheckable = true | ||
| 364 | invertButton.isChecked = invert | ||
| 365 | invertButton.setOnMenuItemClickListener { | ||
| 366 | modifierParams.set("inverted", !invert) | ||
| 367 | stickParams.set("modifier", modifierParams.serialize()) | ||
| 368 | true | ||
| 369 | } | ||
| 370 | |||
| 371 | val toggle = modifierParams.get("toggle", false) | ||
| 372 | toggleButton.isVisible = true | ||
| 373 | toggleButton.isCheckable = true | ||
| 374 | toggleButton.isChecked = toggle | ||
| 375 | toggleButton.setOnMenuItemClickListener { | ||
| 376 | modifierParams.set("toggle", !toggle) | ||
| 377 | stickParams.set("modifier", modifierParams.serialize()) | ||
| 378 | true | ||
| 379 | } | ||
| 380 | |||
| 381 | popup.setOnDismissListener { | ||
| 382 | NativeInput.setStickParam( | ||
| 383 | item.playerIndex, | ||
| 384 | item.nativeAnalog, | ||
| 385 | stickParams | ||
| 386 | ) | ||
| 387 | settingsViewModel.setAdapterItemChanged(position) | ||
| 388 | } | ||
| 389 | } | ||
| 390 | } | ||
| 391 | } | ||
| 392 | popup.show() | ||
| 393 | } | ||
| 394 | |||
| 188 | fun onLongClick(item: SettingsItem, position: Int): Boolean { | 395 | fun onLongClick(item: SettingsItem, position: Int): Boolean { |
| 189 | SettingsDialogFragment.newInstance( | 396 | SettingsDialogFragment.newInstance( |
| 190 | settingsViewModel, | 397 | settingsViewModel, |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt index 60e029f34..5d1ea5d29 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt | |||
| @@ -1,7 +1,7 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | 2 | // SPDX-License-Identifier: GPL-2.0-or-later |
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.fragments | 4 | package org.yuzu.yuzu_emu.features.settings.ui |
| 5 | 5 | ||
| 6 | import android.app.Dialog | 6 | import android.app.Dialog |
| 7 | import android.content.DialogInterface | 7 | import android.content.DialogInterface |
| @@ -19,11 +19,16 @@ import com.google.android.material.slider.Slider | |||
| 19 | import kotlinx.coroutines.launch | 19 | import kotlinx.coroutines.launch |
| 20 | import org.yuzu.yuzu_emu.R | 20 | import org.yuzu.yuzu_emu.R |
| 21 | import org.yuzu.yuzu_emu.databinding.DialogSliderBinding | 21 | import org.yuzu.yuzu_emu.databinding.DialogSliderBinding |
| 22 | import org.yuzu.yuzu_emu.features.input.NativeInput | ||
| 23 | import org.yuzu.yuzu_emu.features.input.model.AnalogDirection | ||
| 24 | import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting | ||
| 25 | import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting | ||
| 26 | import org.yuzu.yuzu_emu.features.settings.model.view.IntSingleChoiceSetting | ||
| 22 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | 27 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem |
| 23 | import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting | 28 | import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting |
| 24 | import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting | 29 | import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting |
| 25 | import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting | 30 | import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting |
| 26 | import org.yuzu.yuzu_emu.model.SettingsViewModel | 31 | import org.yuzu.yuzu_emu.utils.ParamPackage |
| 27 | 32 | ||
| 28 | class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener { | 33 | class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener { |
| 29 | private var type = 0 | 34 | private var type = 0 |
| @@ -50,8 +55,49 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener | |||
| 50 | MaterialAlertDialogBuilder(requireContext()) | 55 | MaterialAlertDialogBuilder(requireContext()) |
| 51 | .setMessage(R.string.reset_setting_confirmation) | 56 | .setMessage(R.string.reset_setting_confirmation) |
| 52 | .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> | 57 | .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> |
| 53 | settingsViewModel.clickedItem!!.setting.reset() | 58 | when (val item = settingsViewModel.clickedItem) { |
| 54 | settingsViewModel.setAdapterItemChanged(position) | 59 | is AnalogInputSetting -> { |
| 60 | val stickParam = NativeInput.getStickParam( | ||
| 61 | item.playerIndex, | ||
| 62 | item.nativeAnalog | ||
| 63 | ) | ||
| 64 | if (stickParam.get("engine", "") == "analog_from_button") { | ||
| 65 | when (item.analogDirection) { | ||
| 66 | AnalogDirection.Up -> stickParam.erase("up") | ||
| 67 | AnalogDirection.Down -> stickParam.erase("down") | ||
| 68 | AnalogDirection.Left -> stickParam.erase("left") | ||
| 69 | AnalogDirection.Right -> stickParam.erase("right") | ||
| 70 | } | ||
| 71 | NativeInput.setStickParam( | ||
| 72 | item.playerIndex, | ||
| 73 | item.nativeAnalog, | ||
| 74 | stickParam | ||
| 75 | ) | ||
| 76 | settingsViewModel.setAdapterItemChanged(position) | ||
| 77 | } else { | ||
| 78 | NativeInput.setStickParam( | ||
| 79 | item.playerIndex, | ||
| 80 | item.nativeAnalog, | ||
| 81 | ParamPackage() | ||
| 82 | ) | ||
| 83 | settingsViewModel.setDatasetChanged(true) | ||
| 84 | } | ||
| 85 | } | ||
| 86 | |||
| 87 | is ButtonInputSetting -> { | ||
| 88 | NativeInput.setButtonParam( | ||
| 89 | item.playerIndex, | ||
| 90 | item.nativeButton, | ||
| 91 | ParamPackage() | ||
| 92 | ) | ||
| 93 | settingsViewModel.setAdapterItemChanged(position) | ||
| 94 | } | ||
| 95 | |||
| 96 | else -> { | ||
| 97 | settingsViewModel.clickedItem!!.setting.reset() | ||
| 98 | settingsViewModel.setAdapterItemChanged(position) | ||
| 99 | } | ||
| 100 | } | ||
| 55 | } | 101 | } |
| 56 | .setNegativeButton(android.R.string.cancel, null) | 102 | .setNegativeButton(android.R.string.cancel, null) |
| 57 | .create() | 103 | .create() |
| @@ -61,7 +107,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener | |||
| 61 | val item = settingsViewModel.clickedItem as SingleChoiceSetting | 107 | val item = settingsViewModel.clickedItem as SingleChoiceSetting |
| 62 | val value = getSelectionForSingleChoiceValue(item) | 108 | val value = getSelectionForSingleChoiceValue(item) |
| 63 | MaterialAlertDialogBuilder(requireContext()) | 109 | MaterialAlertDialogBuilder(requireContext()) |
| 64 | .setTitle(item.nameId) | 110 | .setTitle(item.title) |
| 65 | .setSingleChoiceItems(item.choicesId, value, this) | 111 | .setSingleChoiceItems(item.choicesId, value, this) |
| 66 | .create() | 112 | .create() |
| 67 | } | 113 | } |
| @@ -81,7 +127,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener | |||
| 81 | } | 127 | } |
| 82 | 128 | ||
| 83 | MaterialAlertDialogBuilder(requireContext()) | 129 | MaterialAlertDialogBuilder(requireContext()) |
| 84 | .setTitle(item.nameId) | 130 | .setTitle(item.title) |
| 85 | .setView(sliderBinding.root) | 131 | .setView(sliderBinding.root) |
| 86 | .setPositiveButton(android.R.string.ok, this) | 132 | .setPositiveButton(android.R.string.ok, this) |
| 87 | .setNegativeButton(android.R.string.cancel, defaultCancelListener) | 133 | .setNegativeButton(android.R.string.cancel, defaultCancelListener) |
| @@ -91,8 +137,16 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener | |||
| 91 | SettingsItem.TYPE_STRING_SINGLE_CHOICE -> { | 137 | SettingsItem.TYPE_STRING_SINGLE_CHOICE -> { |
| 92 | val item = settingsViewModel.clickedItem as StringSingleChoiceSetting | 138 | val item = settingsViewModel.clickedItem as StringSingleChoiceSetting |
| 93 | MaterialAlertDialogBuilder(requireContext()) | 139 | MaterialAlertDialogBuilder(requireContext()) |
| 94 | .setTitle(item.nameId) | 140 | .setTitle(item.title) |
| 95 | .setSingleChoiceItems(item.choices, item.selectValueIndex, this) | 141 | .setSingleChoiceItems(item.choices, item.selectedValueIndex, this) |
| 142 | .create() | ||
| 143 | } | ||
| 144 | |||
| 145 | SettingsItem.TYPE_INT_SINGLE_CHOICE -> { | ||
| 146 | val item = settingsViewModel.clickedItem as IntSingleChoiceSetting | ||
| 147 | MaterialAlertDialogBuilder(requireContext()) | ||
| 148 | .setTitle(item.title) | ||
| 149 | .setSingleChoiceItems(item.choices, item.selectedValueIndex, this) | ||
| 96 | .create() | 150 | .create() |
| 97 | } | 151 | } |
| 98 | 152 | ||
| @@ -145,6 +199,12 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener | |||
| 145 | scSetting.setSelectedValue(value) | 199 | scSetting.setSelectedValue(value) |
| 146 | } | 200 | } |
| 147 | 201 | ||
| 202 | is IntSingleChoiceSetting -> { | ||
| 203 | val scSetting = settingsViewModel.clickedItem as IntSingleChoiceSetting | ||
| 204 | val value = scSetting.getValueAt(which) | ||
| 205 | scSetting.setSelectedValue(value) | ||
| 206 | } | ||
| 207 | |||
| 148 | is SliderSetting -> { | 208 | is SliderSetting -> { |
| 149 | val sliderSetting = settingsViewModel.clickedItem as SliderSetting | 209 | val sliderSetting = settingsViewModel.clickedItem as SliderSetting |
| 150 | sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value) | 210 | sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value) |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt index 6f6e7be10..0cf944b43 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt | |||
| @@ -24,8 +24,9 @@ import kotlinx.coroutines.flow.collectLatest | |||
| 24 | import kotlinx.coroutines.launch | 24 | import kotlinx.coroutines.launch |
| 25 | import org.yuzu.yuzu_emu.R | 25 | import org.yuzu.yuzu_emu.R |
| 26 | import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding | 26 | import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding |
| 27 | import org.yuzu.yuzu_emu.features.input.NativeInput | ||
| 27 | import org.yuzu.yuzu_emu.features.settings.model.Settings | 28 | import org.yuzu.yuzu_emu.features.settings.model.Settings |
| 28 | import org.yuzu.yuzu_emu.model.SettingsViewModel | 29 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment |
| 29 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins | 30 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins |
| 30 | 31 | ||
| 31 | class SettingsFragment : Fragment() { | 32 | class SettingsFragment : Fragment() { |
| @@ -45,6 +46,12 @@ class SettingsFragment : Fragment() { | |||
| 45 | returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) | 46 | returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) |
| 46 | reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) | 47 | reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) |
| 47 | exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) | 48 | exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) |
| 49 | |||
| 50 | val playerIndex = getPlayerIndex() | ||
| 51 | if (playerIndex != -1) { | ||
| 52 | NativeInput.loadInputProfiles() | ||
| 53 | NativeInput.reloadInputDevices() | ||
| 54 | } | ||
| 48 | } | 55 | } |
| 49 | 56 | ||
| 50 | override fun onCreateView( | 57 | override fun onCreateView( |
| @@ -57,8 +64,9 @@ class SettingsFragment : Fragment() { | |||
| 57 | } | 64 | } |
| 58 | 65 | ||
| 59 | // This is using the correct scope, lint is just acting up | 66 | // This is using the correct scope, lint is just acting up |
| 60 | @SuppressLint("UnsafeRepeatOnLifecycleDetector") | 67 | @SuppressLint("UnsafeRepeatOnLifecycleDetector", "NotifyDataSetChanged") |
| 61 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | 68 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| 69 | super.onViewCreated(view, savedInstanceState) | ||
| 62 | settingsAdapter = SettingsAdapter(this, requireContext()) | 70 | settingsAdapter = SettingsAdapter(this, requireContext()) |
| 63 | presenter = SettingsFragmentPresenter( | 71 | presenter = SettingsFragmentPresenter( |
| 64 | settingsViewModel, | 72 | settingsViewModel, |
| @@ -71,7 +79,17 @@ class SettingsFragment : Fragment() { | |||
| 71 | ) { | 79 | ) { |
| 72 | args.game!!.title | 80 | args.game!!.title |
| 73 | } else { | 81 | } else { |
| 74 | getString(args.menuTag.titleId) | 82 | when (args.menuTag) { |
| 83 | Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> Settings.getPlayerString(1) | ||
| 84 | Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> Settings.getPlayerString(2) | ||
| 85 | Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> Settings.getPlayerString(3) | ||
| 86 | Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> Settings.getPlayerString(4) | ||
| 87 | Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> Settings.getPlayerString(5) | ||
| 88 | Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> Settings.getPlayerString(6) | ||
| 89 | Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> Settings.getPlayerString(7) | ||
| 90 | Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> Settings.getPlayerString(8) | ||
| 91 | else -> getString(args.menuTag.titleId) | ||
| 92 | } | ||
| 75 | } | 93 | } |
| 76 | binding.listSettings.apply { | 94 | binding.listSettings.apply { |
| 77 | adapter = settingsAdapter | 95 | adapter = settingsAdapter |
| @@ -93,6 +111,55 @@ class SettingsFragment : Fragment() { | |||
| 93 | } | 111 | } |
| 94 | } | 112 | } |
| 95 | } | 113 | } |
| 114 | launch { | ||
| 115 | repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
| 116 | settingsViewModel.adapterItemChanged.collect { | ||
| 117 | if (it != -1) { | ||
| 118 | settingsAdapter?.notifyItemChanged(it) | ||
| 119 | settingsViewModel.setAdapterItemChanged(-1) | ||
| 120 | } | ||
| 121 | } | ||
| 122 | } | ||
| 123 | } | ||
| 124 | launch { | ||
| 125 | repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
| 126 | settingsViewModel.datasetChanged.collect { | ||
| 127 | if (it) { | ||
| 128 | settingsAdapter?.notifyDataSetChanged() | ||
| 129 | settingsViewModel.setDatasetChanged(false) | ||
| 130 | } | ||
| 131 | } | ||
| 132 | } | ||
| 133 | } | ||
| 134 | launch { | ||
| 135 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 136 | settingsViewModel.reloadListAndNotifyDataset.collectLatest { | ||
| 137 | if (it) { | ||
| 138 | settingsViewModel.setReloadListAndNotifyDataset(false) | ||
| 139 | presenter.loadSettingsList(true) | ||
| 140 | } | ||
| 141 | } | ||
| 142 | } | ||
| 143 | } | ||
| 144 | launch { | ||
| 145 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 146 | settingsViewModel.shouldShowResetInputDialog.collectLatest { | ||
| 147 | if (it) { | ||
| 148 | MessageDialogFragment.newInstance( | ||
| 149 | activity = requireActivity(), | ||
| 150 | titleId = R.string.reset_mapping, | ||
| 151 | descriptionId = R.string.reset_mapping_description, | ||
| 152 | positiveAction = { | ||
| 153 | NativeInput.resetControllerMappings(getPlayerIndex()) | ||
| 154 | settingsViewModel.setReloadListAndNotifyDataset(true) | ||
| 155 | }, | ||
| 156 | negativeAction = {} | ||
| 157 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 158 | settingsViewModel.setShouldShowResetInputDialog(false) | ||
| 159 | } | ||
| 160 | } | ||
| 161 | } | ||
| 162 | } | ||
| 96 | } | 163 | } |
| 97 | 164 | ||
| 98 | if (args.menuTag == Settings.MenuTag.SECTION_ROOT) { | 165 | if (args.menuTag == Settings.MenuTag.SECTION_ROOT) { |
| @@ -115,6 +182,19 @@ class SettingsFragment : Fragment() { | |||
| 115 | setInsets() | 182 | setInsets() |
| 116 | } | 183 | } |
| 117 | 184 | ||
| 185 | private fun getPlayerIndex(): Int = | ||
| 186 | when (args.menuTag) { | ||
| 187 | Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> 0 | ||
| 188 | Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> 1 | ||
| 189 | Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> 2 | ||
| 190 | Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> 3 | ||
| 191 | Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> 4 | ||
| 192 | Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> 5 | ||
| 193 | Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> 6 | ||
| 194 | Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> 7 | ||
| 195 | else -> -1 | ||
| 196 | } | ||
| 197 | |||
| 118 | private fun setInsets() { | 198 | private fun setInsets() { |
| 119 | ViewCompat.setOnApplyWindowInsetsListener( | 199 | ViewCompat.setOnApplyWindowInsetsListener( |
| 120 | binding.root | 200 | binding.root |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 5d495a7ca..e491c29a2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt | |||
| @@ -3,11 +3,17 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.features.settings.ui | 4 | package org.yuzu.yuzu_emu.features.settings.ui |
| 5 | 5 | ||
| 6 | import android.annotation.SuppressLint | ||
| 6 | import android.os.Build | 7 | import android.os.Build |
| 7 | import android.widget.Toast | 8 | import android.widget.Toast |
| 8 | import org.yuzu.yuzu_emu.NativeLibrary | 9 | import org.yuzu.yuzu_emu.NativeLibrary |
| 9 | import org.yuzu.yuzu_emu.R | 10 | import org.yuzu.yuzu_emu.R |
| 10 | import org.yuzu.yuzu_emu.YuzuApplication | 11 | import org.yuzu.yuzu_emu.YuzuApplication |
| 12 | import org.yuzu.yuzu_emu.features.input.NativeInput | ||
| 13 | import org.yuzu.yuzu_emu.features.input.model.AnalogDirection | ||
| 14 | import org.yuzu.yuzu_emu.features.input.model.NativeAnalog | ||
| 15 | import org.yuzu.yuzu_emu.features.input.model.NativeButton | ||
| 16 | import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex | ||
| 11 | import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting | 17 | import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting |
| 12 | import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting | 18 | import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting |
| 13 | import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting | 19 | import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting |
| @@ -15,18 +21,21 @@ import org.yuzu.yuzu_emu.features.settings.model.ByteSetting | |||
| 15 | import org.yuzu.yuzu_emu.features.settings.model.IntSetting | 21 | import org.yuzu.yuzu_emu.features.settings.model.IntSetting |
| 16 | import org.yuzu.yuzu_emu.features.settings.model.LongSetting | 22 | import org.yuzu.yuzu_emu.features.settings.model.LongSetting |
| 17 | import org.yuzu.yuzu_emu.features.settings.model.Settings | 23 | import org.yuzu.yuzu_emu.features.settings.model.Settings |
| 24 | import org.yuzu.yuzu_emu.features.settings.model.Settings.MenuTag | ||
| 18 | import org.yuzu.yuzu_emu.features.settings.model.ShortSetting | 25 | import org.yuzu.yuzu_emu.features.settings.model.ShortSetting |
| 19 | import org.yuzu.yuzu_emu.features.settings.model.view.* | 26 | import org.yuzu.yuzu_emu.features.settings.model.view.* |
| 20 | import org.yuzu.yuzu_emu.model.SettingsViewModel | 27 | import org.yuzu.yuzu_emu.utils.InputHandler |
| 21 | import org.yuzu.yuzu_emu.utils.NativeConfig | 28 | import org.yuzu.yuzu_emu.utils.NativeConfig |
| 22 | 29 | ||
| 23 | class SettingsFragmentPresenter( | 30 | class SettingsFragmentPresenter( |
| 24 | private val settingsViewModel: SettingsViewModel, | 31 | private val settingsViewModel: SettingsViewModel, |
| 25 | private val adapter: SettingsAdapter, | 32 | private val adapter: SettingsAdapter, |
| 26 | private var menuTag: Settings.MenuTag | 33 | private var menuTag: MenuTag |
| 27 | ) { | 34 | ) { |
| 28 | private var settingsList = ArrayList<SettingsItem>() | 35 | private var settingsList = ArrayList<SettingsItem>() |
| 29 | 36 | ||
| 37 | private val context get() = YuzuApplication.appContext | ||
| 38 | |||
| 30 | // Extension for altering settings list based on each setting's properties | 39 | // Extension for altering settings list based on each setting's properties |
| 31 | fun ArrayList<SettingsItem>.add(key: String) { | 40 | fun ArrayList<SettingsItem>.add(key: String) { |
| 32 | val item = SettingsItem.settingsItems[key]!! | 41 | val item = SettingsItem.settingsItems[key]!! |
| @@ -53,31 +62,48 @@ class SettingsFragmentPresenter( | |||
| 53 | add(item) | 62 | add(item) |
| 54 | } | 63 | } |
| 55 | 64 | ||
| 65 | // Allows you to show/hide abstract settings based on the paired setting key | ||
| 66 | fun ArrayList<SettingsItem>.addAbstract(item: SettingsItem) { | ||
| 67 | val pairedSettingKey = item.setting.pairedSettingKey | ||
| 68 | if (pairedSettingKey.isNotEmpty()) { | ||
| 69 | val pairedSettingsItem = | ||
| 70 | this.firstOrNull { it.setting.key == pairedSettingKey } ?: return | ||
| 71 | val pairedSetting = pairedSettingsItem.setting as AbstractBooleanSetting | ||
| 72 | if (!pairedSetting.getBoolean(!NativeConfig.isPerGameConfigLoaded())) return | ||
| 73 | } | ||
| 74 | add(item) | ||
| 75 | } | ||
| 76 | |||
| 56 | fun onViewCreated() { | 77 | fun onViewCreated() { |
| 57 | loadSettingsList() | 78 | loadSettingsList() |
| 58 | } | 79 | } |
| 59 | 80 | ||
| 60 | fun loadSettingsList() { | 81 | @SuppressLint("NotifyDataSetChanged") |
| 82 | fun loadSettingsList(notifyDataSetChanged: Boolean = false) { | ||
| 61 | val sl = ArrayList<SettingsItem>() | 83 | val sl = ArrayList<SettingsItem>() |
| 62 | when (menuTag) { | 84 | when (menuTag) { |
| 63 | Settings.MenuTag.SECTION_ROOT -> addConfigSettings(sl) | 85 | MenuTag.SECTION_ROOT -> addConfigSettings(sl) |
| 64 | Settings.MenuTag.SECTION_SYSTEM -> addSystemSettings(sl) | 86 | MenuTag.SECTION_SYSTEM -> addSystemSettings(sl) |
| 65 | Settings.MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl) | 87 | MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl) |
| 66 | Settings.MenuTag.SECTION_AUDIO -> addAudioSettings(sl) | 88 | MenuTag.SECTION_AUDIO -> addAudioSettings(sl) |
| 67 | Settings.MenuTag.SECTION_THEME -> addThemeSettings(sl) | 89 | MenuTag.SECTION_INPUT -> addInputSettings(sl) |
| 68 | Settings.MenuTag.SECTION_DEBUG -> addDebugSettings(sl) | 90 | MenuTag.SECTION_INPUT_PLAYER_ONE -> addInputPlayer(sl, 0) |
| 69 | else -> { | 91 | MenuTag.SECTION_INPUT_PLAYER_TWO -> addInputPlayer(sl, 1) |
| 70 | val context = YuzuApplication.appContext | 92 | MenuTag.SECTION_INPUT_PLAYER_THREE -> addInputPlayer(sl, 2) |
| 71 | Toast.makeText( | 93 | MenuTag.SECTION_INPUT_PLAYER_FOUR -> addInputPlayer(sl, 3) |
| 72 | context, | 94 | MenuTag.SECTION_INPUT_PLAYER_FIVE -> addInputPlayer(sl, 4) |
| 73 | context.getString(R.string.unimplemented_menu), | 95 | MenuTag.SECTION_INPUT_PLAYER_SIX -> addInputPlayer(sl, 5) |
| 74 | Toast.LENGTH_SHORT | 96 | MenuTag.SECTION_INPUT_PLAYER_SEVEN -> addInputPlayer(sl, 6) |
| 75 | ).show() | 97 | MenuTag.SECTION_INPUT_PLAYER_EIGHT -> addInputPlayer(sl, 7) |
| 76 | return | 98 | MenuTag.SECTION_THEME -> addThemeSettings(sl) |
| 77 | } | 99 | MenuTag.SECTION_DEBUG -> addDebugSettings(sl) |
| 78 | } | 100 | } |
| 79 | settingsList = sl | 101 | settingsList = sl |
| 80 | adapter.submitList(settingsList) | 102 | adapter.submitList(settingsList) { |
| 103 | if (notifyDataSetChanged) { | ||
| 104 | adapter.notifyDataSetChanged() | ||
| 105 | } | ||
| 106 | } | ||
| 81 | } | 107 | } |
| 82 | 108 | ||
| 83 | private fun addConfigSettings(sl: ArrayList<SettingsItem>) { | 109 | private fun addConfigSettings(sl: ArrayList<SettingsItem>) { |
| @@ -118,6 +144,7 @@ class SettingsFragmentPresenter( | |||
| 118 | RunnableSetting( | 144 | RunnableSetting( |
| 119 | titleId = R.string.reset_to_default, | 145 | titleId = R.string.reset_to_default, |
| 120 | descriptionId = R.string.reset_to_default_description, | 146 | descriptionId = R.string.reset_to_default_description, |
| 147 | isRunnable = !NativeLibrary.isRunning(), | ||
| 121 | iconId = R.drawable.ic_restore | 148 | iconId = R.drawable.ic_restore |
| 122 | ) { settingsViewModel.setShouldShowResetSettingsDialog(true) } | 149 | ) { settingsViewModel.setShouldShowResetSettingsDialog(true) } |
| 123 | ) | 150 | ) |
| @@ -163,6 +190,671 @@ class SettingsFragmentPresenter( | |||
| 163 | } | 190 | } |
| 164 | } | 191 | } |
| 165 | 192 | ||
| 193 | private fun addInputSettings(sl: ArrayList<SettingsItem>) { | ||
| 194 | settingsViewModel.currentDevice = 0 | ||
| 195 | |||
| 196 | if (NativeConfig.isPerGameConfigLoaded()) { | ||
| 197 | NativeInput.loadInputProfiles() | ||
| 198 | val profiles = NativeInput.getInputProfileNames().toMutableList() | ||
| 199 | profiles.add(0, "") | ||
| 200 | val prettyProfiles = profiles.toTypedArray() | ||
| 201 | prettyProfiles[0] = | ||
| 202 | context.getString(R.string.use_global_input_configuration) | ||
| 203 | sl.apply { | ||
| 204 | for (i in 0 until 8) { | ||
| 205 | add( | ||
| 206 | IntSingleChoiceSetting( | ||
| 207 | getPerGameProfileSetting(profiles, i), | ||
| 208 | titleString = getPlayerProfileString(i + 1), | ||
| 209 | choices = prettyProfiles, | ||
| 210 | values = IntArray(profiles.size) { it }.toTypedArray() | ||
| 211 | ) | ||
| 212 | ) | ||
| 213 | } | ||
| 214 | } | ||
| 215 | return | ||
| 216 | } | ||
| 217 | |||
| 218 | val getConnectedIcon: (Int) -> Int = { playerIndex: Int -> | ||
| 219 | if (NativeInput.getIsConnected(playerIndex)) { | ||
| 220 | R.drawable.ic_controller | ||
| 221 | } else { | ||
| 222 | R.drawable.ic_controller_disconnected | ||
| 223 | } | ||
| 224 | } | ||
| 225 | |||
| 226 | val inputSettings = NativeConfig.getInputSettings(true) | ||
| 227 | sl.apply { | ||
| 228 | add( | ||
| 229 | SubmenuSetting( | ||
| 230 | titleString = Settings.getPlayerString(1), | ||
| 231 | descriptionString = inputSettings[0].profileName, | ||
| 232 | menuKey = MenuTag.SECTION_INPUT_PLAYER_ONE, | ||
| 233 | iconId = getConnectedIcon(0) | ||
| 234 | ) | ||
| 235 | ) | ||
| 236 | add( | ||
| 237 | SubmenuSetting( | ||
| 238 | titleString = Settings.getPlayerString(2), | ||
| 239 | descriptionString = inputSettings[1].profileName, | ||
| 240 | menuKey = MenuTag.SECTION_INPUT_PLAYER_TWO, | ||
| 241 | iconId = getConnectedIcon(1) | ||
| 242 | ) | ||
| 243 | ) | ||
| 244 | add( | ||
| 245 | SubmenuSetting( | ||
| 246 | titleString = Settings.getPlayerString(3), | ||
| 247 | descriptionString = inputSettings[2].profileName, | ||
| 248 | menuKey = MenuTag.SECTION_INPUT_PLAYER_THREE, | ||
| 249 | iconId = getConnectedIcon(2) | ||
| 250 | ) | ||
| 251 | ) | ||
| 252 | add( | ||
| 253 | SubmenuSetting( | ||
| 254 | titleString = Settings.getPlayerString(4), | ||
| 255 | descriptionString = inputSettings[3].profileName, | ||
| 256 | menuKey = MenuTag.SECTION_INPUT_PLAYER_FOUR, | ||
| 257 | iconId = getConnectedIcon(3) | ||
| 258 | ) | ||
| 259 | ) | ||
| 260 | add( | ||
| 261 | SubmenuSetting( | ||
| 262 | titleString = Settings.getPlayerString(5), | ||
| 263 | descriptionString = inputSettings[4].profileName, | ||
| 264 | menuKey = MenuTag.SECTION_INPUT_PLAYER_FIVE, | ||
| 265 | iconId = getConnectedIcon(4) | ||
| 266 | ) | ||
| 267 | ) | ||
| 268 | add( | ||
| 269 | SubmenuSetting( | ||
| 270 | titleString = Settings.getPlayerString(6), | ||
| 271 | descriptionString = inputSettings[5].profileName, | ||
| 272 | menuKey = MenuTag.SECTION_INPUT_PLAYER_SIX, | ||
| 273 | iconId = getConnectedIcon(5) | ||
| 274 | ) | ||
| 275 | ) | ||
| 276 | add( | ||
| 277 | SubmenuSetting( | ||
| 278 | titleString = Settings.getPlayerString(7), | ||
| 279 | descriptionString = inputSettings[6].profileName, | ||
| 280 | menuKey = MenuTag.SECTION_INPUT_PLAYER_SEVEN, | ||
| 281 | iconId = getConnectedIcon(6) | ||
| 282 | ) | ||
| 283 | ) | ||
| 284 | add( | ||
| 285 | SubmenuSetting( | ||
| 286 | titleString = Settings.getPlayerString(8), | ||
| 287 | descriptionString = inputSettings[7].profileName, | ||
| 288 | menuKey = MenuTag.SECTION_INPUT_PLAYER_EIGHT, | ||
| 289 | iconId = getConnectedIcon(7) | ||
| 290 | ) | ||
| 291 | ) | ||
| 292 | } | ||
| 293 | } | ||
| 294 | |||
| 295 | private fun getPlayerProfileString(player: Int): String = | ||
| 296 | context.getString(R.string.player_num_profile, player) | ||
| 297 | |||
| 298 | private fun getPerGameProfileSetting( | ||
| 299 | profiles: List<String>, | ||
| 300 | playerIndex: Int | ||
| 301 | ): AbstractIntSetting { | ||
| 302 | return object : AbstractIntSetting { | ||
| 303 | private val players | ||
| 304 | get() = NativeConfig.getInputSettings(false) | ||
| 305 | |||
| 306 | override val key = "" | ||
| 307 | |||
| 308 | override fun getInt(needsGlobal: Boolean): Int { | ||
| 309 | val currentProfile = players[playerIndex].profileName | ||
| 310 | profiles.forEachIndexed { i, profile -> | ||
| 311 | if (profile == currentProfile) { | ||
| 312 | return i | ||
| 313 | } | ||
| 314 | } | ||
| 315 | return 0 | ||
| 316 | } | ||
| 317 | |||
| 318 | override fun setInt(value: Int) { | ||
| 319 | NativeInput.loadPerGameConfiguration(playerIndex, value, profiles[value]) | ||
| 320 | NativeInput.connectControllers(playerIndex) | ||
| 321 | NativeConfig.saveControlPlayerValues() | ||
| 322 | } | ||
| 323 | |||
| 324 | override val defaultValue = 0 | ||
| 325 | |||
| 326 | override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString() | ||
| 327 | |||
| 328 | override fun reset() = setInt(defaultValue) | ||
| 329 | |||
| 330 | override var global = true | ||
| 331 | |||
| 332 | override val isRuntimeModifiable = true | ||
| 333 | |||
| 334 | override val isSaveable = true | ||
| 335 | } | ||
| 336 | } | ||
| 337 | |||
| 338 | private fun addInputPlayer(sl: ArrayList<SettingsItem>, playerIndex: Int) { | ||
| 339 | sl.apply { | ||
| 340 | val connectedSetting = object : AbstractBooleanSetting { | ||
| 341 | override val key = "connected" | ||
| 342 | |||
| 343 | override fun getBoolean(needsGlobal: Boolean): Boolean = | ||
| 344 | NativeInput.getIsConnected(playerIndex) | ||
| 345 | |||
| 346 | override fun setBoolean(value: Boolean) = | ||
| 347 | NativeInput.connectControllers(playerIndex, value) | ||
| 348 | |||
| 349 | override val defaultValue = playerIndex == 0 | ||
| 350 | |||
| 351 | override fun getValueAsString(needsGlobal: Boolean): String = | ||
| 352 | getBoolean(needsGlobal).toString() | ||
| 353 | |||
| 354 | override fun reset() = setBoolean(defaultValue) | ||
| 355 | } | ||
| 356 | add(SwitchSetting(connectedSetting, R.string.connected)) | ||
| 357 | |||
| 358 | val styleTags = NativeInput.getSupportedStyleTags(playerIndex) | ||
| 359 | val npadType = object : AbstractIntSetting { | ||
| 360 | override val key = "npad_type" | ||
| 361 | override fun getInt(needsGlobal: Boolean): Int { | ||
| 362 | val styleIndex = NativeInput.getStyleIndex(playerIndex) | ||
| 363 | return styleTags.indexOfFirst { it == styleIndex } | ||
| 364 | } | ||
| 365 | |||
| 366 | override fun setInt(value: Int) { | ||
| 367 | NativeInput.setStyleIndex(playerIndex, styleTags[value]) | ||
| 368 | settingsViewModel.setReloadListAndNotifyDataset(true) | ||
| 369 | } | ||
| 370 | |||
| 371 | override val defaultValue = NpadStyleIndex.Fullkey.int | ||
| 372 | override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString() | ||
| 373 | override fun reset() = setInt(defaultValue) | ||
| 374 | override val pairedSettingKey: String = "connected" | ||
| 375 | } | ||
| 376 | addAbstract( | ||
| 377 | IntSingleChoiceSetting( | ||
| 378 | npadType, | ||
| 379 | titleId = R.string.controller_type, | ||
| 380 | choices = styleTags.map { context.getString(it.nameId) } | ||
| 381 | .toTypedArray(), | ||
| 382 | values = IntArray(styleTags.size) { it }.toTypedArray() | ||
| 383 | ) | ||
| 384 | ) | ||
| 385 | |||
| 386 | InputHandler.updateControllerData() | ||
| 387 | |||
| 388 | val autoMappingSetting = object : AbstractIntSetting { | ||
| 389 | override val key = "auto_mapping_device" | ||
| 390 | |||
| 391 | override fun getInt(needsGlobal: Boolean): Int = -1 | ||
| 392 | |||
| 393 | override fun setInt(value: Int) { | ||
| 394 | val registeredController = InputHandler.registeredControllers[value + 1] | ||
| 395 | val displayName = registeredController.get( | ||
| 396 | "display", | ||
| 397 | context.getString(R.string.unknown) | ||
| 398 | ) | ||
| 399 | NativeInput.updateMappingsWithDefault( | ||
| 400 | playerIndex, | ||
| 401 | registeredController, | ||
| 402 | displayName | ||
| 403 | ) | ||
| 404 | Toast.makeText( | ||
| 405 | context, | ||
| 406 | context.getString(R.string.attempted_auto_map, displayName), | ||
| 407 | Toast.LENGTH_SHORT | ||
| 408 | ).show() | ||
| 409 | settingsViewModel.setReloadListAndNotifyDataset(true) | ||
| 410 | } | ||
| 411 | |||
| 412 | override val defaultValue = -1 | ||
| 413 | |||
| 414 | override fun getValueAsString(needsGlobal: Boolean) = getInt().toString() | ||
| 415 | |||
| 416 | override fun reset() = setInt(defaultValue) | ||
| 417 | |||
| 418 | override val isRuntimeModifiable: Boolean = true | ||
| 419 | } | ||
| 420 | |||
| 421 | val unknownString = context.getString(R.string.unknown) | ||
| 422 | val prettyAutoMappingControllerList = InputHandler.registeredControllers.mapNotNull { | ||
| 423 | val port = it.get("port", -1) | ||
| 424 | return@mapNotNull if (port == 100 || port == -1) { | ||
| 425 | null | ||
| 426 | } else { | ||
| 427 | it.get("display", unknownString) | ||
| 428 | } | ||
| 429 | }.toTypedArray() | ||
| 430 | add( | ||
| 431 | IntSingleChoiceSetting( | ||
| 432 | autoMappingSetting, | ||
| 433 | titleId = R.string.auto_map, | ||
| 434 | descriptionId = R.string.auto_map_description, | ||
| 435 | choices = prettyAutoMappingControllerList, | ||
| 436 | values = IntArray(prettyAutoMappingControllerList.size) { it }.toTypedArray() | ||
| 437 | ) | ||
| 438 | ) | ||
| 439 | |||
| 440 | val mappingFilterSetting = object : AbstractIntSetting { | ||
| 441 | override val key = "mapping_filter" | ||
| 442 | |||
| 443 | override fun getInt(needsGlobal: Boolean): Int = settingsViewModel.currentDevice | ||
| 444 | |||
| 445 | override fun setInt(value: Int) { | ||
| 446 | settingsViewModel.currentDevice = value | ||
| 447 | } | ||
| 448 | |||
| 449 | override val defaultValue = 0 | ||
| 450 | |||
| 451 | override fun getValueAsString(needsGlobal: Boolean) = getInt().toString() | ||
| 452 | |||
| 453 | override fun reset() = setInt(defaultValue) | ||
| 454 | |||
| 455 | override val isRuntimeModifiable: Boolean = true | ||
| 456 | } | ||
| 457 | |||
| 458 | val prettyControllerList = InputHandler.registeredControllers.mapNotNull { | ||
| 459 | return@mapNotNull if (it.get("port", 0) == 100) { | ||
| 460 | null | ||
| 461 | } else { | ||
| 462 | it.get("display", unknownString) | ||
| 463 | } | ||
| 464 | }.toTypedArray() | ||
| 465 | add( | ||
| 466 | IntSingleChoiceSetting( | ||
| 467 | mappingFilterSetting, | ||
| 468 | titleId = R.string.input_mapping_filter, | ||
| 469 | descriptionId = R.string.input_mapping_filter_description, | ||
| 470 | choices = prettyControllerList, | ||
| 471 | values = IntArray(prettyControllerList.size) { it }.toTypedArray() | ||
| 472 | ) | ||
| 473 | ) | ||
| 474 | |||
| 475 | add(InputProfileSetting(playerIndex)) | ||
| 476 | add( | ||
| 477 | RunnableSetting(titleId = R.string.reset_to_default, isRunnable = true) { | ||
| 478 | settingsViewModel.setShouldShowResetInputDialog(true) | ||
| 479 | } | ||
| 480 | ) | ||
| 481 | |||
| 482 | val styleIndex = NativeInput.getStyleIndex(playerIndex) | ||
| 483 | |||
| 484 | // Buttons | ||
| 485 | when (styleIndex) { | ||
| 486 | NpadStyleIndex.Fullkey, | ||
| 487 | NpadStyleIndex.Handheld, | ||
| 488 | NpadStyleIndex.JoyconDual -> { | ||
| 489 | add(HeaderSetting(R.string.buttons)) | ||
| 490 | add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a)) | ||
| 491 | add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b)) | ||
| 492 | add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x)) | ||
| 493 | add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y)) | ||
| 494 | add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.button_plus)) | ||
| 495 | add(ButtonInputSetting(playerIndex, NativeButton.Minus, R.string.button_minus)) | ||
| 496 | add(ButtonInputSetting(playerIndex, NativeButton.Home, R.string.button_home)) | ||
| 497 | add( | ||
| 498 | ButtonInputSetting( | ||
| 499 | playerIndex, | ||
| 500 | NativeButton.Capture, | ||
| 501 | R.string.button_capture | ||
| 502 | ) | ||
| 503 | ) | ||
| 504 | } | ||
| 505 | |||
| 506 | NpadStyleIndex.JoyconLeft -> { | ||
| 507 | add(HeaderSetting(R.string.buttons)) | ||
| 508 | add(ButtonInputSetting(playerIndex, NativeButton.Minus, R.string.button_minus)) | ||
| 509 | add( | ||
| 510 | ButtonInputSetting( | ||
| 511 | playerIndex, | ||
| 512 | NativeButton.Capture, | ||
| 513 | R.string.button_capture | ||
| 514 | ) | ||
| 515 | ) | ||
| 516 | } | ||
| 517 | |||
| 518 | NpadStyleIndex.JoyconRight -> { | ||
| 519 | add(HeaderSetting(R.string.buttons)) | ||
| 520 | add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a)) | ||
| 521 | add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b)) | ||
| 522 | add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x)) | ||
| 523 | add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y)) | ||
| 524 | add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.button_plus)) | ||
| 525 | add(ButtonInputSetting(playerIndex, NativeButton.Home, R.string.button_home)) | ||
| 526 | } | ||
| 527 | |||
| 528 | NpadStyleIndex.GameCube -> { | ||
| 529 | add(HeaderSetting(R.string.buttons)) | ||
| 530 | add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a)) | ||
| 531 | add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b)) | ||
| 532 | add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x)) | ||
| 533 | add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y)) | ||
| 534 | add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.start_pause)) | ||
| 535 | } | ||
| 536 | |||
| 537 | else -> { | ||
| 538 | // No-op | ||
| 539 | } | ||
| 540 | } | ||
| 541 | |||
| 542 | when (styleIndex) { | ||
| 543 | NpadStyleIndex.Fullkey, | ||
| 544 | NpadStyleIndex.Handheld, | ||
| 545 | NpadStyleIndex.JoyconDual, | ||
| 546 | NpadStyleIndex.JoyconLeft -> { | ||
| 547 | add(HeaderSetting(R.string.dpad)) | ||
| 548 | add(ButtonInputSetting(playerIndex, NativeButton.DUp, R.string.up)) | ||
| 549 | add(ButtonInputSetting(playerIndex, NativeButton.DDown, R.string.down)) | ||
| 550 | add(ButtonInputSetting(playerIndex, NativeButton.DLeft, R.string.left)) | ||
| 551 | add(ButtonInputSetting(playerIndex, NativeButton.DRight, R.string.right)) | ||
| 552 | } | ||
| 553 | |||
| 554 | else -> { | ||
| 555 | // No-op | ||
| 556 | } | ||
| 557 | } | ||
| 558 | |||
| 559 | // Left stick | ||
| 560 | when (styleIndex) { | ||
| 561 | NpadStyleIndex.Fullkey, | ||
| 562 | NpadStyleIndex.Handheld, | ||
| 563 | NpadStyleIndex.JoyconDual, | ||
| 564 | NpadStyleIndex.JoyconLeft -> { | ||
| 565 | add(HeaderSetting(R.string.left_stick)) | ||
| 566 | addAll(getStickDirections(playerIndex, NativeAnalog.LStick)) | ||
| 567 | add(ButtonInputSetting(playerIndex, NativeButton.LStick, R.string.pressed)) | ||
| 568 | addAll(getExtraStickSettings(playerIndex, NativeAnalog.LStick)) | ||
| 569 | } | ||
| 570 | |||
| 571 | NpadStyleIndex.GameCube -> { | ||
| 572 | add(HeaderSetting(R.string.control_stick)) | ||
| 573 | addAll(getStickDirections(playerIndex, NativeAnalog.LStick)) | ||
| 574 | addAll(getExtraStickSettings(playerIndex, NativeAnalog.LStick)) | ||
| 575 | } | ||
| 576 | |||
| 577 | else -> { | ||
| 578 | // No-op | ||
| 579 | } | ||
| 580 | } | ||
| 581 | |||
| 582 | // Right stick | ||
| 583 | when (styleIndex) { | ||
| 584 | NpadStyleIndex.Fullkey, | ||
| 585 | NpadStyleIndex.Handheld, | ||
| 586 | NpadStyleIndex.JoyconDual, | ||
| 587 | NpadStyleIndex.JoyconRight -> { | ||
| 588 | add(HeaderSetting(R.string.right_stick)) | ||
| 589 | addAll(getStickDirections(playerIndex, NativeAnalog.RStick)) | ||
| 590 | add(ButtonInputSetting(playerIndex, NativeButton.RStick, R.string.pressed)) | ||
| 591 | addAll(getExtraStickSettings(playerIndex, NativeAnalog.RStick)) | ||
| 592 | } | ||
| 593 | |||
| 594 | NpadStyleIndex.GameCube -> { | ||
| 595 | add(HeaderSetting(R.string.c_stick)) | ||
| 596 | addAll(getStickDirections(playerIndex, NativeAnalog.RStick)) | ||
| 597 | addAll(getExtraStickSettings(playerIndex, NativeAnalog.RStick)) | ||
| 598 | } | ||
| 599 | |||
| 600 | else -> { | ||
| 601 | // No-op | ||
| 602 | } | ||
| 603 | } | ||
| 604 | |||
| 605 | // L/R, ZL/ZR, and SL/SR | ||
| 606 | when (styleIndex) { | ||
| 607 | NpadStyleIndex.Fullkey, | ||
| 608 | NpadStyleIndex.Handheld -> { | ||
| 609 | add(HeaderSetting(R.string.triggers)) | ||
| 610 | add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l)) | ||
| 611 | add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r)) | ||
| 612 | add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl)) | ||
| 613 | add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr)) | ||
| 614 | } | ||
| 615 | |||
| 616 | NpadStyleIndex.JoyconDual -> { | ||
| 617 | add(HeaderSetting(R.string.triggers)) | ||
| 618 | add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l)) | ||
| 619 | add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r)) | ||
| 620 | add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl)) | ||
| 621 | add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr)) | ||
| 622 | add( | ||
| 623 | ButtonInputSetting( | ||
| 624 | playerIndex, | ||
| 625 | NativeButton.SLLeft, | ||
| 626 | R.string.button_sl_left | ||
| 627 | ) | ||
| 628 | ) | ||
| 629 | add( | ||
| 630 | ButtonInputSetting( | ||
| 631 | playerIndex, | ||
| 632 | NativeButton.SRLeft, | ||
| 633 | R.string.button_sr_left | ||
| 634 | ) | ||
| 635 | ) | ||
| 636 | add( | ||
| 637 | ButtonInputSetting( | ||
| 638 | playerIndex, | ||
| 639 | NativeButton.SLRight, | ||
| 640 | R.string.button_sl_right | ||
| 641 | ) | ||
| 642 | ) | ||
| 643 | add( | ||
| 644 | ButtonInputSetting( | ||
| 645 | playerIndex, | ||
| 646 | NativeButton.SRRight, | ||
| 647 | R.string.button_sr_right | ||
| 648 | ) | ||
| 649 | ) | ||
| 650 | } | ||
| 651 | |||
| 652 | NpadStyleIndex.JoyconLeft -> { | ||
| 653 | add(HeaderSetting(R.string.triggers)) | ||
| 654 | add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l)) | ||
| 655 | add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl)) | ||
| 656 | add( | ||
| 657 | ButtonInputSetting( | ||
| 658 | playerIndex, | ||
| 659 | NativeButton.SLLeft, | ||
| 660 | R.string.button_sl_left | ||
| 661 | ) | ||
| 662 | ) | ||
| 663 | add( | ||
| 664 | ButtonInputSetting( | ||
| 665 | playerIndex, | ||
| 666 | NativeButton.SRLeft, | ||
| 667 | R.string.button_sr_left | ||
| 668 | ) | ||
| 669 | ) | ||
| 670 | } | ||
| 671 | |||
| 672 | NpadStyleIndex.JoyconRight -> { | ||
| 673 | add(HeaderSetting(R.string.triggers)) | ||
| 674 | add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r)) | ||
| 675 | add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr)) | ||
| 676 | add( | ||
| 677 | ButtonInputSetting( | ||
| 678 | playerIndex, | ||
| 679 | NativeButton.SLRight, | ||
| 680 | R.string.button_sl_right | ||
| 681 | ) | ||
| 682 | ) | ||
| 683 | add( | ||
| 684 | ButtonInputSetting( | ||
| 685 | playerIndex, | ||
| 686 | NativeButton.SRRight, | ||
| 687 | R.string.button_sr_right | ||
| 688 | ) | ||
| 689 | ) | ||
| 690 | } | ||
| 691 | |||
| 692 | NpadStyleIndex.GameCube -> { | ||
| 693 | add(HeaderSetting(R.string.triggers)) | ||
| 694 | add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_z)) | ||
| 695 | add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_l)) | ||
| 696 | add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_r)) | ||
| 697 | } | ||
| 698 | |||
| 699 | else -> { | ||
| 700 | // No-op | ||
| 701 | } | ||
| 702 | } | ||
| 703 | |||
| 704 | add(HeaderSetting(R.string.vibration)) | ||
| 705 | val vibrationEnabledSetting = object : AbstractBooleanSetting { | ||
| 706 | override val key = "vibration" | ||
| 707 | |||
| 708 | override fun getBoolean(needsGlobal: Boolean): Boolean = | ||
| 709 | NativeConfig.getInputSettings(true)[playerIndex].vibrationEnabled | ||
| 710 | |||
| 711 | override fun setBoolean(value: Boolean) { | ||
| 712 | val settings = NativeConfig.getInputSettings(true) | ||
| 713 | settings[playerIndex].vibrationEnabled = value | ||
| 714 | NativeConfig.setInputSettings(settings, true) | ||
| 715 | } | ||
| 716 | |||
| 717 | override val defaultValue = true | ||
| 718 | |||
| 719 | override fun getValueAsString(needsGlobal: Boolean): String = | ||
| 720 | getBoolean(needsGlobal).toString() | ||
| 721 | |||
| 722 | override fun reset() = setBoolean(defaultValue) | ||
| 723 | } | ||
| 724 | add(SwitchSetting(vibrationEnabledSetting, R.string.vibration)) | ||
| 725 | |||
| 726 | val useSystemVibratorSetting = object : AbstractBooleanSetting { | ||
| 727 | override val key = "" | ||
| 728 | |||
| 729 | override fun getBoolean(needsGlobal: Boolean): Boolean = | ||
| 730 | NativeConfig.getInputSettings(true)[playerIndex].useSystemVibrator | ||
| 731 | |||
| 732 | override fun setBoolean(value: Boolean) { | ||
| 733 | val settings = NativeConfig.getInputSettings(true) | ||
| 734 | settings[playerIndex].useSystemVibrator = value | ||
| 735 | NativeConfig.setInputSettings(settings, true) | ||
| 736 | } | ||
| 737 | |||
| 738 | override val defaultValue = playerIndex == 0 | ||
| 739 | |||
| 740 | override fun getValueAsString(needsGlobal: Boolean): String = | ||
| 741 | getBoolean(needsGlobal).toString() | ||
| 742 | |||
| 743 | override fun reset() = setBoolean(defaultValue) | ||
| 744 | |||
| 745 | override val pairedSettingKey: String = "vibration" | ||
| 746 | } | ||
| 747 | addAbstract(SwitchSetting(useSystemVibratorSetting, R.string.use_system_vibrator)) | ||
| 748 | |||
| 749 | val vibrationStrengthSetting = object : AbstractIntSetting { | ||
| 750 | override val key = "" | ||
| 751 | |||
| 752 | override fun getInt(needsGlobal: Boolean): Int = | ||
| 753 | NativeConfig.getInputSettings(true)[playerIndex].vibrationStrength | ||
| 754 | |||
| 755 | override fun setInt(value: Int) { | ||
| 756 | val settings = NativeConfig.getInputSettings(true) | ||
| 757 | settings[playerIndex].vibrationStrength = value | ||
| 758 | NativeConfig.setInputSettings(settings, true) | ||
| 759 | } | ||
| 760 | |||
| 761 | override val defaultValue = 100 | ||
| 762 | |||
| 763 | override fun getValueAsString(needsGlobal: Boolean): String = | ||
| 764 | getInt(needsGlobal).toString() | ||
| 765 | |||
| 766 | override fun reset() = setInt(defaultValue) | ||
| 767 | |||
| 768 | override val pairedSettingKey: String = "vibration" | ||
| 769 | } | ||
| 770 | addAbstract( | ||
| 771 | SliderSetting(vibrationStrengthSetting, R.string.vibration_strength, units = "%") | ||
| 772 | ) | ||
| 773 | } | ||
| 774 | } | ||
| 775 | |||
| 776 | // Convenience function for creating AbstractIntSettings for modifier range/stick range/stick deadzones | ||
| 777 | private fun getStickIntSettingFromParam( | ||
| 778 | playerIndex: Int, | ||
| 779 | paramName: String, | ||
| 780 | stick: NativeAnalog, | ||
| 781 | defaultValue: Int | ||
| 782 | ): AbstractIntSetting = | ||
| 783 | object : AbstractIntSetting { | ||
| 784 | val params get() = NativeInput.getStickParam(playerIndex, stick) | ||
| 785 | |||
| 786 | override val key = "" | ||
| 787 | |||
| 788 | override fun getInt(needsGlobal: Boolean): Int = | ||
| 789 | (params.get(paramName, 0.15f) * 100).toInt() | ||
| 790 | |||
| 791 | override fun setInt(value: Int) { | ||
| 792 | val tempParams = params | ||
| 793 | tempParams.set(paramName, value.toFloat() / 100) | ||
| 794 | NativeInput.setStickParam(playerIndex, stick, tempParams) | ||
| 795 | } | ||
| 796 | |||
| 797 | override val defaultValue = defaultValue | ||
| 798 | |||
| 799 | override fun getValueAsString(needsGlobal: Boolean): String = | ||
| 800 | getInt(needsGlobal).toString() | ||
| 801 | |||
| 802 | override fun reset() = setInt(defaultValue) | ||
| 803 | } | ||
| 804 | |||
| 805 | private fun getExtraStickSettings( | ||
| 806 | playerIndex: Int, | ||
| 807 | nativeAnalog: NativeAnalog | ||
| 808 | ): List<SettingsItem> { | ||
| 809 | val stickIsController = | ||
| 810 | NativeInput.isController(NativeInput.getStickParam(playerIndex, nativeAnalog)) | ||
| 811 | val modifierRangeSetting = | ||
| 812 | getStickIntSettingFromParam(playerIndex, "modifier_scale", nativeAnalog, 50) | ||
| 813 | val stickRangeSetting = | ||
| 814 | getStickIntSettingFromParam(playerIndex, "range", nativeAnalog, 95) | ||
| 815 | val stickDeadzoneSetting = | ||
| 816 | getStickIntSettingFromParam(playerIndex, "deadzone", nativeAnalog, 15) | ||
| 817 | |||
| 818 | val out = mutableListOf<SettingsItem>().apply { | ||
| 819 | if (stickIsController) { | ||
| 820 | add(SliderSetting(stickRangeSetting, titleId = R.string.range, min = 25, max = 150)) | ||
| 821 | add(SliderSetting(stickDeadzoneSetting, R.string.deadzone)) | ||
| 822 | } else { | ||
| 823 | add(ModifierInputSetting(playerIndex, NativeAnalog.LStick, R.string.modifier)) | ||
| 824 | add(SliderSetting(modifierRangeSetting, R.string.modifier_range)) | ||
| 825 | } | ||
| 826 | } | ||
| 827 | return out | ||
| 828 | } | ||
| 829 | |||
| 830 | private fun getStickDirections(player: Int, stick: NativeAnalog): List<AnalogInputSetting> = | ||
| 831 | listOf( | ||
| 832 | AnalogInputSetting( | ||
| 833 | player, | ||
| 834 | stick, | ||
| 835 | AnalogDirection.Up, | ||
| 836 | R.string.up | ||
| 837 | ), | ||
| 838 | AnalogInputSetting( | ||
| 839 | player, | ||
| 840 | stick, | ||
| 841 | AnalogDirection.Down, | ||
| 842 | R.string.down | ||
| 843 | ), | ||
| 844 | AnalogInputSetting( | ||
| 845 | player, | ||
| 846 | stick, | ||
| 847 | AnalogDirection.Left, | ||
| 848 | R.string.left | ||
| 849 | ), | ||
| 850 | AnalogInputSetting( | ||
| 851 | player, | ||
| 852 | stick, | ||
| 853 | AnalogDirection.Right, | ||
| 854 | R.string.right | ||
| 855 | ) | ||
| 856 | ) | ||
| 857 | |||
| 166 | private fun addThemeSettings(sl: ArrayList<SettingsItem>) { | 858 | private fun addThemeSettings(sl: ArrayList<SettingsItem>) { |
| 167 | sl.apply { | 859 | sl.apply { |
| 168 | val theme: AbstractIntSetting = object : AbstractIntSetting { | 860 | val theme: AbstractIntSetting = object : AbstractIntSetting { |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSearchFragment.kt index a135b80b4..51740a2ac 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSearchFragment.kt | |||
| @@ -1,7 +1,7 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | 2 | // SPDX-License-Identifier: GPL-2.0-or-later |
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.fragments | 4 | package org.yuzu.yuzu_emu.features.settings.ui |
| 5 | 5 | ||
| 6 | import android.content.Context | 6 | import android.content.Context |
| 7 | import android.os.Bundle | 7 | import android.os.Bundle |
| @@ -26,8 +26,6 @@ import kotlinx.coroutines.launch | |||
| 26 | import org.yuzu.yuzu_emu.R | 26 | import org.yuzu.yuzu_emu.R |
| 27 | import org.yuzu.yuzu_emu.databinding.FragmentSettingsSearchBinding | 27 | import org.yuzu.yuzu_emu.databinding.FragmentSettingsSearchBinding |
| 28 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | 28 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem |
| 29 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | ||
| 30 | import org.yuzu.yuzu_emu.model.SettingsViewModel | ||
| 31 | import org.yuzu.yuzu_emu.utils.NativeConfig | 29 | import org.yuzu.yuzu_emu.utils.NativeConfig |
| 32 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins | 30 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins |
| 33 | 31 | ||
| @@ -119,7 +117,7 @@ class SettingsSearchFragment : Fragment() { | |||
| 119 | val baseList = SettingsItem.settingsItems | 117 | val baseList = SettingsItem.settingsItems |
| 120 | val similarityAlgorithm = if (searchTerm.length > 2) Cosine() else Cosine(1) | 118 | val similarityAlgorithm = if (searchTerm.length > 2) Cosine() else Cosine(1) |
| 121 | val sortedList: List<SettingsItem> = baseList.mapNotNull { item -> | 119 | val sortedList: List<SettingsItem> = baseList.mapNotNull { item -> |
| 122 | val title = getString(item.value.nameId).lowercase() | 120 | val title = item.value.title.lowercase() |
| 123 | val similarity = similarityAlgorithm.similarity(searchTerm, title) | 121 | val similarity = similarityAlgorithm.similarity(searchTerm, title) |
| 124 | if (similarity > 0.08) { | 122 | if (similarity > 0.08) { |
| 125 | Pair(similarity, item) | 123 | Pair(similarity, item) |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt index 5cb6a5d57..fbdca04e9 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt | |||
| @@ -1,20 +1,26 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | 2 | // SPDX-License-Identifier: GPL-2.0-or-later |
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.model | 4 | package org.yuzu.yuzu_emu.features.settings.ui |
| 5 | 5 | ||
| 6 | import androidx.lifecycle.ViewModel | 6 | import androidx.lifecycle.ViewModel |
| 7 | import kotlinx.coroutines.flow.MutableStateFlow | 7 | import kotlinx.coroutines.flow.MutableStateFlow |
| 8 | import kotlinx.coroutines.flow.StateFlow | 8 | import kotlinx.coroutines.flow.StateFlow |
| 9 | import kotlinx.coroutines.flow.asStateFlow | ||
| 9 | import org.yuzu.yuzu_emu.R | 10 | import org.yuzu.yuzu_emu.R |
| 10 | import org.yuzu.yuzu_emu.YuzuApplication | 11 | import org.yuzu.yuzu_emu.YuzuApplication |
| 11 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | 12 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem |
| 13 | import org.yuzu.yuzu_emu.model.Game | ||
| 14 | import org.yuzu.yuzu_emu.utils.InputHandler | ||
| 15 | import org.yuzu.yuzu_emu.utils.ParamPackage | ||
| 12 | 16 | ||
| 13 | class SettingsViewModel : ViewModel() { | 17 | class SettingsViewModel : ViewModel() { |
| 14 | var game: Game? = null | 18 | var game: Game? = null |
| 15 | 19 | ||
| 16 | var clickedItem: SettingsItem? = null | 20 | var clickedItem: SettingsItem? = null |
| 17 | 21 | ||
| 22 | var currentDevice = 0 | ||
| 23 | |||
| 18 | val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate | 24 | val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate |
| 19 | private val _shouldRecreate = MutableStateFlow(false) | 25 | private val _shouldRecreate = MutableStateFlow(false) |
| 20 | 26 | ||
| @@ -36,6 +42,18 @@ class SettingsViewModel : ViewModel() { | |||
| 36 | val adapterItemChanged: StateFlow<Int> get() = _adapterItemChanged | 42 | val adapterItemChanged: StateFlow<Int> get() = _adapterItemChanged |
| 37 | private val _adapterItemChanged = MutableStateFlow(-1) | 43 | private val _adapterItemChanged = MutableStateFlow(-1) |
| 38 | 44 | ||
| 45 | private val _datasetChanged = MutableStateFlow(false) | ||
| 46 | val datasetChanged = _datasetChanged.asStateFlow() | ||
| 47 | |||
| 48 | private val _reloadListAndNotifyDataset = MutableStateFlow(false) | ||
| 49 | val reloadListAndNotifyDataset = _reloadListAndNotifyDataset.asStateFlow() | ||
| 50 | |||
| 51 | private val _shouldShowDeleteProfileDialog = MutableStateFlow("") | ||
| 52 | val shouldShowDeleteProfileDialog = _shouldShowDeleteProfileDialog.asStateFlow() | ||
| 53 | |||
| 54 | private val _shouldShowResetInputDialog = MutableStateFlow(false) | ||
| 55 | val shouldShowResetInputDialog = _shouldShowResetInputDialog.asStateFlow() | ||
| 56 | |||
| 39 | fun setShouldRecreate(value: Boolean) { | 57 | fun setShouldRecreate(value: Boolean) { |
| 40 | _shouldRecreate.value = value | 58 | _shouldRecreate.value = value |
| 41 | } | 59 | } |
| @@ -68,4 +86,27 @@ class SettingsViewModel : ViewModel() { | |||
| 68 | fun setAdapterItemChanged(value: Int) { | 86 | fun setAdapterItemChanged(value: Int) { |
| 69 | _adapterItemChanged.value = value | 87 | _adapterItemChanged.value = value |
| 70 | } | 88 | } |
| 89 | |||
| 90 | fun setDatasetChanged(value: Boolean) { | ||
| 91 | _datasetChanged.value = value | ||
| 92 | } | ||
| 93 | |||
| 94 | fun setReloadListAndNotifyDataset(value: Boolean) { | ||
| 95 | _reloadListAndNotifyDataset.value = value | ||
| 96 | } | ||
| 97 | |||
| 98 | fun setShouldShowDeleteProfileDialog(profile: String) { | ||
| 99 | _shouldShowDeleteProfileDialog.value = profile | ||
| 100 | } | ||
| 101 | |||
| 102 | fun setShouldShowResetInputDialog(value: Boolean) { | ||
| 103 | _shouldShowResetInputDialog.value = value | ||
| 104 | } | ||
| 105 | |||
| 106 | fun getCurrentDeviceParams(defaultParams: ParamPackage): ParamPackage = | ||
| 107 | try { | ||
| 108 | InputHandler.registeredControllers[currentDevice] | ||
| 109 | } catch (e: IndexOutOfBoundsException) { | ||
| 110 | defaultParams | ||
| 111 | } | ||
| 71 | } | 112 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputProfileViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputProfileViewHolder.kt new file mode 100644 index 000000000..81161d5d3 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputProfileViewHolder.kt | |||
| @@ -0,0 +1,33 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.ui.viewholder | ||
| 5 | |||
| 6 | import android.view.View | ||
| 7 | import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding | ||
| 8 | import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting | ||
| 9 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | ||
| 10 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | ||
| 11 | import org.yuzu.yuzu_emu.R | ||
| 12 | |||
| 13 | class InputProfileViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : | ||
| 14 | SettingViewHolder(binding.root, adapter) { | ||
| 15 | private lateinit var setting: InputProfileSetting | ||
| 16 | |||
| 17 | override fun bind(item: SettingsItem) { | ||
| 18 | setting = item as InputProfileSetting | ||
| 19 | binding.textSettingName.text = setting.title | ||
| 20 | binding.textSettingValue.text = | ||
| 21 | setting.getCurrentProfile().ifEmpty { binding.root.context.getString(R.string.not_set) } | ||
| 22 | |||
| 23 | binding.textSettingDescription.visibility = View.GONE | ||
| 24 | binding.buttonClear.visibility = View.GONE | ||
| 25 | binding.icon.visibility = View.GONE | ||
| 26 | binding.buttonClear.visibility = View.GONE | ||
| 27 | } | ||
| 28 | |||
| 29 | override fun onClick(clicked: View) = | ||
| 30 | adapter.onInputProfileClick(setting, bindingAdapterPosition) | ||
| 31 | |||
| 32 | override fun onLongClick(clicked: View): Boolean = false | ||
| 33 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt new file mode 100644 index 000000000..1f1f08190 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt | |||
| @@ -0,0 +1,71 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.ui.viewholder | ||
| 5 | |||
| 6 | import android.view.View | ||
| 7 | import org.yuzu.yuzu_emu.databinding.ListItemSettingInputBinding | ||
| 8 | import org.yuzu.yuzu_emu.features.input.NativeInput | ||
| 9 | import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting | ||
| 10 | import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting | ||
| 11 | import org.yuzu.yuzu_emu.features.settings.model.view.InputSetting | ||
| 12 | import org.yuzu.yuzu_emu.features.settings.model.view.ModifierInputSetting | ||
| 13 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | ||
| 14 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | ||
| 15 | |||
| 16 | class InputViewHolder(val binding: ListItemSettingInputBinding, adapter: SettingsAdapter) : | ||
| 17 | SettingViewHolder(binding.root, adapter) { | ||
| 18 | private lateinit var setting: InputSetting | ||
| 19 | |||
| 20 | override fun bind(item: SettingsItem) { | ||
| 21 | setting = item as InputSetting | ||
| 22 | binding.textSettingName.text = setting.title | ||
| 23 | binding.textSettingValue.text = setting.getSelectedValue() | ||
| 24 | |||
| 25 | binding.buttonOptions.visibility = when (item) { | ||
| 26 | is AnalogInputSetting -> { | ||
| 27 | val param = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog) | ||
| 28 | if ( | ||
| 29 | param.get("engine", "") == "analog_from_button" || | ||
| 30 | param.has("axis_x") || param.has("axis_y") | ||
| 31 | ) { | ||
| 32 | View.VISIBLE | ||
| 33 | } else { | ||
| 34 | View.GONE | ||
| 35 | } | ||
| 36 | } | ||
| 37 | |||
| 38 | is ButtonInputSetting -> { | ||
| 39 | val param = NativeInput.getButtonParam(item.playerIndex, item.nativeButton) | ||
| 40 | if ( | ||
| 41 | param.has("code") || param.has("button") || param.has("hat") || | ||
| 42 | param.has("axis") | ||
| 43 | ) { | ||
| 44 | View.VISIBLE | ||
| 45 | } else { | ||
| 46 | View.GONE | ||
| 47 | } | ||
| 48 | } | ||
| 49 | |||
| 50 | is ModifierInputSetting -> { | ||
| 51 | val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog) | ||
| 52 | if (params.has("modifier")) { | ||
| 53 | View.VISIBLE | ||
| 54 | } else { | ||
| 55 | View.GONE | ||
| 56 | } | ||
| 57 | } | ||
| 58 | } | ||
| 59 | |||
| 60 | binding.buttonOptions.setOnClickListener(null) | ||
| 61 | binding.buttonOptions.setOnClickListener { | ||
| 62 | adapter.onInputOptionsClick(binding.buttonOptions, setting, bindingAdapterPosition) | ||
| 63 | } | ||
| 64 | } | ||
| 65 | |||
| 66 | override fun onClick(clicked: View) = | ||
| 67 | adapter.onInputClick(setting, bindingAdapterPosition) | ||
| 68 | |||
| 69 | override fun onLongClick(clicked: View): Boolean = | ||
| 70 | adapter.onLongClick(setting, bindingAdapterPosition) | ||
| 71 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt index 2cecede48..9705d428c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt | |||
| @@ -47,6 +47,9 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti | |||
| 47 | binding.textSettingValue.text = item.getChoiceAt(item.getSelectedValue()) | 47 | binding.textSettingValue.text = item.getChoiceAt(item.getSelectedValue()) |
| 48 | } | 48 | } |
| 49 | } | 49 | } |
| 50 | if (binding.textSettingValue.text.isEmpty()) { | ||
| 51 | binding.textSettingValue.visibility = View.GONE | ||
| 52 | } | ||
| 50 | 53 | ||
| 51 | binding.buttonClear.visibility = if (setting.setting.global || | 54 | binding.buttonClear.visibility = if (setting.setting.global || |
| 52 | !NativeConfig.isPerGameConfigLoaded() | 55 | !NativeConfig.isPerGameConfigLoaded() |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index 6b25cc525..c737ed5e8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt | |||
| @@ -277,6 +277,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 277 | true | 277 | true |
| 278 | } | 278 | } |
| 279 | 279 | ||
| 280 | R.id.menu_controls -> { | ||
| 281 | val action = HomeNavigationDirections.actionGlobalSettingsActivity( | ||
| 282 | null, | ||
| 283 | Settings.MenuTag.SECTION_INPUT | ||
| 284 | ) | ||
| 285 | binding.root.findNavController().navigate(action) | ||
| 286 | true | ||
| 287 | } | ||
| 288 | |||
| 280 | R.id.menu_overlay_controls -> { | 289 | R.id.menu_overlay_controls -> { |
| 281 | showOverlayOptions() | 290 | showOverlayOptions() |
| 282 | true | 291 | true |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index 87e130d3e..14a2504b6 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt | |||
| @@ -91,6 +91,20 @@ class HomeSettingsFragment : Fragment() { | |||
| 91 | ) | 91 | ) |
| 92 | add( | 92 | add( |
| 93 | HomeSetting( | 93 | HomeSetting( |
| 94 | R.string.preferences_controls, | ||
| 95 | R.string.preferences_controls_description, | ||
| 96 | R.drawable.ic_controller, | ||
| 97 | { | ||
| 98 | val action = HomeNavigationDirections.actionGlobalSettingsActivity( | ||
| 99 | null, | ||
| 100 | Settings.MenuTag.SECTION_INPUT | ||
| 101 | ) | ||
| 102 | binding.root.findNavController().navigate(action) | ||
| 103 | } | ||
| 104 | ) | ||
| 105 | ) | ||
| 106 | add( | ||
| 107 | HomeSetting( | ||
| 94 | R.string.gpu_driver_manager, | 108 | R.string.gpu_driver_manager, |
| 95 | R.string.install_gpu_driver_description, | 109 | R.string.install_gpu_driver_description, |
| 96 | R.drawable.ic_build, | 110 | R.drawable.ic_build, |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt index c87486c90..66907085a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt | |||
| @@ -24,10 +24,10 @@ import androidx.core.content.ContextCompat | |||
| 24 | import androidx.window.layout.WindowMetricsCalculator | 24 | import androidx.window.layout.WindowMetricsCalculator |
| 25 | import kotlin.math.max | 25 | import kotlin.math.max |
| 26 | import kotlin.math.min | 26 | import kotlin.math.min |
| 27 | import org.yuzu.yuzu_emu.NativeLibrary | 27 | import org.yuzu.yuzu_emu.features.input.NativeInput |
| 28 | import org.yuzu.yuzu_emu.NativeLibrary.ButtonType | ||
| 29 | import org.yuzu.yuzu_emu.NativeLibrary.StickType | ||
| 30 | import org.yuzu.yuzu_emu.R | 28 | import org.yuzu.yuzu_emu.R |
| 29 | import org.yuzu.yuzu_emu.features.input.model.NativeAnalog | ||
| 30 | import org.yuzu.yuzu_emu.features.input.model.NativeButton | ||
| 31 | import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting | 31 | import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting |
| 32 | import org.yuzu.yuzu_emu.features.settings.model.IntSetting | 32 | import org.yuzu.yuzu_emu.features.settings.model.IntSetting |
| 33 | import org.yuzu.yuzu_emu.overlay.model.OverlayControl | 33 | import org.yuzu.yuzu_emu.overlay.model.OverlayControl |
| @@ -100,19 +100,19 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 100 | 100 | ||
| 101 | var shouldUpdateView = false | 101 | var shouldUpdateView = false |
| 102 | val playerIndex = | 102 | val playerIndex = |
| 103 | if (NativeLibrary.isHandheldOnly()) { | 103 | if (NativeInput.isHandheldOnly()) { |
| 104 | NativeLibrary.ConsoleDevice | 104 | NativeInput.ConsoleDevice |
| 105 | } else { | 105 | } else { |
| 106 | NativeLibrary.Player1Device | 106 | NativeInput.Player1Device |
| 107 | } | 107 | } |
| 108 | 108 | ||
| 109 | for (button in overlayButtons) { | 109 | for (button in overlayButtons) { |
| 110 | if (!button.updateStatus(event)) { | 110 | if (!button.updateStatus(event)) { |
| 111 | continue | 111 | continue |
| 112 | } | 112 | } |
| 113 | NativeLibrary.onGamePadButtonEvent( | 113 | NativeInput.onOverlayButtonEvent( |
| 114 | playerIndex, | 114 | playerIndex, |
| 115 | button.buttonId, | 115 | button.button, |
| 116 | button.status | 116 | button.status |
| 117 | ) | 117 | ) |
| 118 | playHaptics(event) | 118 | playHaptics(event) |
| @@ -123,24 +123,24 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 123 | if (!dpad.updateStatus(event, BooleanSetting.DPAD_SLIDE.getBoolean())) { | 123 | if (!dpad.updateStatus(event, BooleanSetting.DPAD_SLIDE.getBoolean())) { |
| 124 | continue | 124 | continue |
| 125 | } | 125 | } |
| 126 | NativeLibrary.onGamePadButtonEvent( | 126 | NativeInput.onOverlayButtonEvent( |
| 127 | playerIndex, | 127 | playerIndex, |
| 128 | dpad.upId, | 128 | dpad.up, |
| 129 | dpad.upStatus | 129 | dpad.upStatus |
| 130 | ) | 130 | ) |
| 131 | NativeLibrary.onGamePadButtonEvent( | 131 | NativeInput.onOverlayButtonEvent( |
| 132 | playerIndex, | 132 | playerIndex, |
| 133 | dpad.downId, | 133 | dpad.down, |
| 134 | dpad.downStatus | 134 | dpad.downStatus |
| 135 | ) | 135 | ) |
| 136 | NativeLibrary.onGamePadButtonEvent( | 136 | NativeInput.onOverlayButtonEvent( |
| 137 | playerIndex, | 137 | playerIndex, |
| 138 | dpad.leftId, | 138 | dpad.left, |
| 139 | dpad.leftStatus | 139 | dpad.leftStatus |
| 140 | ) | 140 | ) |
| 141 | NativeLibrary.onGamePadButtonEvent( | 141 | NativeInput.onOverlayButtonEvent( |
| 142 | playerIndex, | 142 | playerIndex, |
| 143 | dpad.rightId, | 143 | dpad.right, |
| 144 | dpad.rightStatus | 144 | dpad.rightStatus |
| 145 | ) | 145 | ) |
| 146 | playHaptics(event) | 146 | playHaptics(event) |
| @@ -151,16 +151,15 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 151 | if (!joystick.updateStatus(event)) { | 151 | if (!joystick.updateStatus(event)) { |
| 152 | continue | 152 | continue |
| 153 | } | 153 | } |
| 154 | val axisID = joystick.joystickId | 154 | NativeInput.onOverlayJoystickEvent( |
| 155 | NativeLibrary.onGamePadJoystickEvent( | ||
| 156 | playerIndex, | 155 | playerIndex, |
| 157 | axisID, | 156 | joystick.joystick, |
| 158 | joystick.xAxis, | 157 | joystick.xAxis, |
| 159 | joystick.realYAxis | 158 | joystick.realYAxis |
| 160 | ) | 159 | ) |
| 161 | NativeLibrary.onGamePadButtonEvent( | 160 | NativeInput.onOverlayButtonEvent( |
| 162 | playerIndex, | 161 | playerIndex, |
| 163 | joystick.buttonId, | 162 | joystick.button, |
| 164 | joystick.buttonStatus | 163 | joystick.buttonStatus |
| 165 | ) | 164 | ) |
| 166 | playHaptics(event) | 165 | playHaptics(event) |
| @@ -187,7 +186,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 187 | motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP | 186 | motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP |
| 188 | 187 | ||
| 189 | if (isActionDown && !isTouchInputConsumed(pointerId)) { | 188 | if (isActionDown && !isTouchInputConsumed(pointerId)) { |
| 190 | NativeLibrary.onTouchPressed(pointerId, xPosition.toFloat(), yPosition.toFloat()) | 189 | NativeInput.onTouchPressed(pointerId, xPosition.toFloat(), yPosition.toFloat()) |
| 191 | } | 190 | } |
| 192 | 191 | ||
| 193 | if (isActionMove) { | 192 | if (isActionMove) { |
| @@ -196,12 +195,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 196 | if (isTouchInputConsumed(fingerId)) { | 195 | if (isTouchInputConsumed(fingerId)) { |
| 197 | continue | 196 | continue |
| 198 | } | 197 | } |
| 199 | NativeLibrary.onTouchMoved(fingerId, event.getX(i), event.getY(i)) | 198 | NativeInput.onTouchMoved(fingerId, event.getX(i), event.getY(i)) |
| 200 | } | 199 | } |
| 201 | } | 200 | } |
| 202 | 201 | ||
| 203 | if (isActionUp && !isTouchInputConsumed(pointerId)) { | 202 | if (isActionUp && !isTouchInputConsumed(pointerId)) { |
| 204 | NativeLibrary.onTouchReleased(pointerId) | 203 | NativeInput.onTouchReleased(pointerId) |
| 205 | } | 204 | } |
| 206 | 205 | ||
| 207 | return true | 206 | return true |
| @@ -359,7 +358,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 359 | windowSize, | 358 | windowSize, |
| 360 | R.drawable.facebutton_a, | 359 | R.drawable.facebutton_a, |
| 361 | R.drawable.facebutton_a_depressed, | 360 | R.drawable.facebutton_a_depressed, |
| 362 | ButtonType.BUTTON_A, | 361 | NativeButton.A, |
| 363 | data, | 362 | data, |
| 364 | position | 363 | position |
| 365 | ) | 364 | ) |
| @@ -373,7 +372,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 373 | windowSize, | 372 | windowSize, |
| 374 | R.drawable.facebutton_b, | 373 | R.drawable.facebutton_b, |
| 375 | R.drawable.facebutton_b_depressed, | 374 | R.drawable.facebutton_b_depressed, |
| 376 | ButtonType.BUTTON_B, | 375 | NativeButton.B, |
| 377 | data, | 376 | data, |
| 378 | position | 377 | position |
| 379 | ) | 378 | ) |
| @@ -387,7 +386,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 387 | windowSize, | 386 | windowSize, |
| 388 | R.drawable.facebutton_x, | 387 | R.drawable.facebutton_x, |
| 389 | R.drawable.facebutton_x_depressed, | 388 | R.drawable.facebutton_x_depressed, |
| 390 | ButtonType.BUTTON_X, | 389 | NativeButton.X, |
| 391 | data, | 390 | data, |
| 392 | position | 391 | position |
| 393 | ) | 392 | ) |
| @@ -401,7 +400,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 401 | windowSize, | 400 | windowSize, |
| 402 | R.drawable.facebutton_y, | 401 | R.drawable.facebutton_y, |
| 403 | R.drawable.facebutton_y_depressed, | 402 | R.drawable.facebutton_y_depressed, |
| 404 | ButtonType.BUTTON_Y, | 403 | NativeButton.Y, |
| 405 | data, | 404 | data, |
| 406 | position | 405 | position |
| 407 | ) | 406 | ) |
| @@ -415,7 +414,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 415 | windowSize, | 414 | windowSize, |
| 416 | R.drawable.facebutton_plus, | 415 | R.drawable.facebutton_plus, |
| 417 | R.drawable.facebutton_plus_depressed, | 416 | R.drawable.facebutton_plus_depressed, |
| 418 | ButtonType.BUTTON_PLUS, | 417 | NativeButton.Plus, |
| 419 | data, | 418 | data, |
| 420 | position | 419 | position |
| 421 | ) | 420 | ) |
| @@ -429,7 +428,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 429 | windowSize, | 428 | windowSize, |
| 430 | R.drawable.facebutton_minus, | 429 | R.drawable.facebutton_minus, |
| 431 | R.drawable.facebutton_minus_depressed, | 430 | R.drawable.facebutton_minus_depressed, |
| 432 | ButtonType.BUTTON_MINUS, | 431 | NativeButton.Minus, |
| 433 | data, | 432 | data, |
| 434 | position | 433 | position |
| 435 | ) | 434 | ) |
| @@ -443,7 +442,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 443 | windowSize, | 442 | windowSize, |
| 444 | R.drawable.facebutton_home, | 443 | R.drawable.facebutton_home, |
| 445 | R.drawable.facebutton_home_depressed, | 444 | R.drawable.facebutton_home_depressed, |
| 446 | ButtonType.BUTTON_HOME, | 445 | NativeButton.Home, |
| 447 | data, | 446 | data, |
| 448 | position | 447 | position |
| 449 | ) | 448 | ) |
| @@ -457,7 +456,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 457 | windowSize, | 456 | windowSize, |
| 458 | R.drawable.facebutton_screenshot, | 457 | R.drawable.facebutton_screenshot, |
| 459 | R.drawable.facebutton_screenshot_depressed, | 458 | R.drawable.facebutton_screenshot_depressed, |
| 460 | ButtonType.BUTTON_CAPTURE, | 459 | NativeButton.Capture, |
| 461 | data, | 460 | data, |
| 462 | position | 461 | position |
| 463 | ) | 462 | ) |
| @@ -471,7 +470,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 471 | windowSize, | 470 | windowSize, |
| 472 | R.drawable.l_shoulder, | 471 | R.drawable.l_shoulder, |
| 473 | R.drawable.l_shoulder_depressed, | 472 | R.drawable.l_shoulder_depressed, |
| 474 | ButtonType.TRIGGER_L, | 473 | NativeButton.L, |
| 475 | data, | 474 | data, |
| 476 | position | 475 | position |
| 477 | ) | 476 | ) |
| @@ -485,7 +484,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 485 | windowSize, | 484 | windowSize, |
| 486 | R.drawable.r_shoulder, | 485 | R.drawable.r_shoulder, |
| 487 | R.drawable.r_shoulder_depressed, | 486 | R.drawable.r_shoulder_depressed, |
| 488 | ButtonType.TRIGGER_R, | 487 | NativeButton.R, |
| 489 | data, | 488 | data, |
| 490 | position | 489 | position |
| 491 | ) | 490 | ) |
| @@ -499,7 +498,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 499 | windowSize, | 498 | windowSize, |
| 500 | R.drawable.zl_trigger, | 499 | R.drawable.zl_trigger, |
| 501 | R.drawable.zl_trigger_depressed, | 500 | R.drawable.zl_trigger_depressed, |
| 502 | ButtonType.TRIGGER_ZL, | 501 | NativeButton.ZL, |
| 503 | data, | 502 | data, |
| 504 | position | 503 | position |
| 505 | ) | 504 | ) |
| @@ -513,7 +512,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 513 | windowSize, | 512 | windowSize, |
| 514 | R.drawable.zr_trigger, | 513 | R.drawable.zr_trigger, |
| 515 | R.drawable.zr_trigger_depressed, | 514 | R.drawable.zr_trigger_depressed, |
| 516 | ButtonType.TRIGGER_ZR, | 515 | NativeButton.ZR, |
| 517 | data, | 516 | data, |
| 518 | position | 517 | position |
| 519 | ) | 518 | ) |
| @@ -527,7 +526,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 527 | windowSize, | 526 | windowSize, |
| 528 | R.drawable.button_l3, | 527 | R.drawable.button_l3, |
| 529 | R.drawable.button_l3_depressed, | 528 | R.drawable.button_l3_depressed, |
| 530 | ButtonType.STICK_L, | 529 | NativeButton.LStick, |
| 531 | data, | 530 | data, |
| 532 | position | 531 | position |
| 533 | ) | 532 | ) |
| @@ -541,7 +540,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 541 | windowSize, | 540 | windowSize, |
| 542 | R.drawable.button_r3, | 541 | R.drawable.button_r3, |
| 543 | R.drawable.button_r3_depressed, | 542 | R.drawable.button_r3_depressed, |
| 544 | ButtonType.STICK_R, | 543 | NativeButton.RStick, |
| 545 | data, | 544 | data, |
| 546 | position | 545 | position |
| 547 | ) | 546 | ) |
| @@ -556,8 +555,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 556 | R.drawable.joystick_range, | 555 | R.drawable.joystick_range, |
| 557 | R.drawable.joystick, | 556 | R.drawable.joystick, |
| 558 | R.drawable.joystick_depressed, | 557 | R.drawable.joystick_depressed, |
| 559 | StickType.STICK_L, | 558 | NativeAnalog.LStick, |
| 560 | ButtonType.STICK_L, | 559 | NativeButton.LStick, |
| 561 | data, | 560 | data, |
| 562 | position | 561 | position |
| 563 | ) | 562 | ) |
| @@ -572,8 +571,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 572 | R.drawable.joystick_range, | 571 | R.drawable.joystick_range, |
| 573 | R.drawable.joystick, | 572 | R.drawable.joystick, |
| 574 | R.drawable.joystick_depressed, | 573 | R.drawable.joystick_depressed, |
| 575 | StickType.STICK_R, | 574 | NativeAnalog.RStick, |
| 576 | ButtonType.STICK_R, | 575 | NativeButton.RStick, |
| 577 | data, | 576 | data, |
| 578 | position | 577 | position |
| 579 | ) | 578 | ) |
| @@ -835,7 +834,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 835 | windowSize: Pair<Point, Point>, | 834 | windowSize: Pair<Point, Point>, |
| 836 | defaultResId: Int, | 835 | defaultResId: Int, |
| 837 | pressedResId: Int, | 836 | pressedResId: Int, |
| 838 | buttonId: Int, | 837 | button: NativeButton, |
| 839 | overlayControlData: OverlayControlData, | 838 | overlayControlData: OverlayControlData, |
| 840 | position: Pair<Double, Double> | 839 | position: Pair<Double, Double> |
| 841 | ): InputOverlayDrawableButton { | 840 | ): InputOverlayDrawableButton { |
| @@ -869,7 +868,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 869 | res, | 868 | res, |
| 870 | defaultStateBitmap, | 869 | defaultStateBitmap, |
| 871 | pressedStateBitmap, | 870 | pressedStateBitmap, |
| 872 | buttonId, | 871 | button, |
| 873 | overlayControlData | 872 | overlayControlData |
| 874 | ) | 873 | ) |
| 875 | 874 | ||
| @@ -940,11 +939,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 940 | res, | 939 | res, |
| 941 | defaultStateBitmap, | 940 | defaultStateBitmap, |
| 942 | pressedOneDirectionStateBitmap, | 941 | pressedOneDirectionStateBitmap, |
| 943 | pressedTwoDirectionsStateBitmap, | 942 | pressedTwoDirectionsStateBitmap |
| 944 | ButtonType.DPAD_UP, | ||
| 945 | ButtonType.DPAD_DOWN, | ||
| 946 | ButtonType.DPAD_LEFT, | ||
| 947 | ButtonType.DPAD_RIGHT | ||
| 948 | ) | 943 | ) |
| 949 | 944 | ||
| 950 | // Get the minimum and maximum coordinates of the screen where the button can be placed. | 945 | // Get the minimum and maximum coordinates of the screen where the button can be placed. |
| @@ -993,8 +988,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 993 | resOuter: Int, | 988 | resOuter: Int, |
| 994 | defaultResInner: Int, | 989 | defaultResInner: Int, |
| 995 | pressedResInner: Int, | 990 | pressedResInner: Int, |
| 996 | joystick: Int, | 991 | joystick: NativeAnalog, |
| 997 | buttonId: Int, | 992 | button: NativeButton, |
| 998 | overlayControlData: OverlayControlData, | 993 | overlayControlData: OverlayControlData, |
| 999 | position: Pair<Double, Double> | 994 | position: Pair<Double, Double> |
| 1000 | ): InputOverlayDrawableJoystick { | 995 | ): InputOverlayDrawableJoystick { |
| @@ -1042,7 +1037,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : | |||
| 1042 | outerRect, | 1037 | outerRect, |
| 1043 | innerRect, | 1038 | innerRect, |
| 1044 | joystick, | 1039 | joystick, |
| 1045 | buttonId, | 1040 | button, |
| 1046 | overlayControlData.id | 1041 | overlayControlData.id |
| 1047 | ) | 1042 | ) |
| 1048 | 1043 | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt index b14a4f96e..fee3d04ee 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt | |||
| @@ -9,7 +9,8 @@ import android.graphics.Canvas | |||
| 9 | import android.graphics.Rect | 9 | import android.graphics.Rect |
| 10 | import android.graphics.drawable.BitmapDrawable | 10 | import android.graphics.drawable.BitmapDrawable |
| 11 | import android.view.MotionEvent | 11 | import android.view.MotionEvent |
| 12 | import org.yuzu.yuzu_emu.NativeLibrary.ButtonState | 12 | import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState |
| 13 | import org.yuzu.yuzu_emu.features.input.model.NativeButton | ||
| 13 | import org.yuzu.yuzu_emu.overlay.model.OverlayControlData | 14 | import org.yuzu.yuzu_emu.overlay.model.OverlayControlData |
| 14 | 15 | ||
| 15 | /** | 16 | /** |
| @@ -19,13 +20,13 @@ import org.yuzu.yuzu_emu.overlay.model.OverlayControlData | |||
| 19 | * @param res [Resources] instance. | 20 | * @param res [Resources] instance. |
| 20 | * @param defaultStateBitmap [Bitmap] to use with the default state Drawable. | 21 | * @param defaultStateBitmap [Bitmap] to use with the default state Drawable. |
| 21 | * @param pressedStateBitmap [Bitmap] to use with the pressed state Drawable. | 22 | * @param pressedStateBitmap [Bitmap] to use with the pressed state Drawable. |
| 22 | * @param buttonId Identifier for this type of button. | 23 | * @param button [NativeButton] for this type of button. |
| 23 | */ | 24 | */ |
| 24 | class InputOverlayDrawableButton( | 25 | class InputOverlayDrawableButton( |
| 25 | res: Resources, | 26 | res: Resources, |
| 26 | defaultStateBitmap: Bitmap, | 27 | defaultStateBitmap: Bitmap, |
| 27 | pressedStateBitmap: Bitmap, | 28 | pressedStateBitmap: Bitmap, |
| 28 | val buttonId: Int, | 29 | val button: NativeButton, |
| 29 | val overlayControlData: OverlayControlData | 30 | val overlayControlData: OverlayControlData |
| 30 | ) { | 31 | ) { |
| 31 | // The ID value what motion event is tracking | 32 | // The ID value what motion event is tracking |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt index 8aef6f5a5..0cb6ff244 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt | |||
| @@ -9,7 +9,8 @@ import android.graphics.Canvas | |||
| 9 | import android.graphics.Rect | 9 | import android.graphics.Rect |
| 10 | import android.graphics.drawable.BitmapDrawable | 10 | import android.graphics.drawable.BitmapDrawable |
| 11 | import android.view.MotionEvent | 11 | import android.view.MotionEvent |
| 12 | import org.yuzu.yuzu_emu.NativeLibrary.ButtonState | 12 | import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState |
| 13 | import org.yuzu.yuzu_emu.features.input.model.NativeButton | ||
| 13 | 14 | ||
| 14 | /** | 15 | /** |
| 15 | * Custom [BitmapDrawable] that is capable | 16 | * Custom [BitmapDrawable] that is capable |
| @@ -19,20 +20,12 @@ import org.yuzu.yuzu_emu.NativeLibrary.ButtonState | |||
| 19 | * @param defaultStateBitmap [Bitmap] of the default state. | 20 | * @param defaultStateBitmap [Bitmap] of the default state. |
| 20 | * @param pressedOneDirectionStateBitmap [Bitmap] of the pressed state in one direction. | 21 | * @param pressedOneDirectionStateBitmap [Bitmap] of the pressed state in one direction. |
| 21 | * @param pressedTwoDirectionsStateBitmap [Bitmap] of the pressed state in two direction. | 22 | * @param pressedTwoDirectionsStateBitmap [Bitmap] of the pressed state in two direction. |
| 22 | * @param buttonUp Identifier for the up button. | ||
| 23 | * @param buttonDown Identifier for the down button. | ||
| 24 | * @param buttonLeft Identifier for the left button. | ||
| 25 | * @param buttonRight Identifier for the right button. | ||
| 26 | */ | 23 | */ |
| 27 | class InputOverlayDrawableDpad( | 24 | class InputOverlayDrawableDpad( |
| 28 | res: Resources, | 25 | res: Resources, |
| 29 | defaultStateBitmap: Bitmap, | 26 | defaultStateBitmap: Bitmap, |
| 30 | pressedOneDirectionStateBitmap: Bitmap, | 27 | pressedOneDirectionStateBitmap: Bitmap, |
| 31 | pressedTwoDirectionsStateBitmap: Bitmap, | 28 | pressedTwoDirectionsStateBitmap: Bitmap |
| 32 | buttonUp: Int, | ||
| 33 | buttonDown: Int, | ||
| 34 | buttonLeft: Int, | ||
| 35 | buttonRight: Int | ||
| 36 | ) { | 29 | ) { |
| 37 | /** | 30 | /** |
| 38 | * Gets one of the InputOverlayDrawableDpad's button IDs. | 31 | * Gets one of the InputOverlayDrawableDpad's button IDs. |
| @@ -40,10 +33,10 @@ class InputOverlayDrawableDpad( | |||
| 40 | * @return the requested InputOverlayDrawableDpad's button ID. | 33 | * @return the requested InputOverlayDrawableDpad's button ID. |
| 41 | */ | 34 | */ |
| 42 | // The ID identifying what type of button this Drawable represents. | 35 | // The ID identifying what type of button this Drawable represents. |
| 43 | val upId: Int | 36 | val up = NativeButton.DUp |
| 44 | val downId: Int | 37 | val down = NativeButton.DDown |
| 45 | val leftId: Int | 38 | val left = NativeButton.DLeft |
| 46 | val rightId: Int | 39 | val right = NativeButton.DRight |
| 47 | var trackId: Int | 40 | var trackId: Int |
| 48 | 41 | ||
| 49 | val width: Int | 42 | val width: Int |
| @@ -69,10 +62,6 @@ class InputOverlayDrawableDpad( | |||
| 69 | this.pressedTwoDirectionsStateBitmap = BitmapDrawable(res, pressedTwoDirectionsStateBitmap) | 62 | this.pressedTwoDirectionsStateBitmap = BitmapDrawable(res, pressedTwoDirectionsStateBitmap) |
| 70 | width = this.defaultStateBitmap.intrinsicWidth | 63 | width = this.defaultStateBitmap.intrinsicWidth |
| 71 | height = this.defaultStateBitmap.intrinsicHeight | 64 | height = this.defaultStateBitmap.intrinsicHeight |
| 72 | upId = buttonUp | ||
| 73 | downId = buttonDown | ||
| 74 | leftId = buttonLeft | ||
| 75 | rightId = buttonRight | ||
| 76 | trackId = -1 | 65 | trackId = -1 |
| 77 | } | 66 | } |
| 78 | 67 | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt index 113bf7c24..4b07107fc 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt | |||
| @@ -13,7 +13,9 @@ import kotlin.math.atan2 | |||
| 13 | import kotlin.math.cos | 13 | import kotlin.math.cos |
| 14 | import kotlin.math.sin | 14 | import kotlin.math.sin |
| 15 | import kotlin.math.sqrt | 15 | import kotlin.math.sqrt |
| 16 | import org.yuzu.yuzu_emu.NativeLibrary | 16 | import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState |
| 17 | import org.yuzu.yuzu_emu.features.input.model.NativeAnalog | ||
| 18 | import org.yuzu.yuzu_emu.features.input.model.NativeButton | ||
| 17 | import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting | 19 | import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting |
| 18 | 20 | ||
| 19 | /** | 21 | /** |
| @@ -26,8 +28,8 @@ import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting | |||
| 26 | * @param bitmapInnerPressed [Bitmap] which represents the pressed inner movable part of the joystick. | 28 | * @param bitmapInnerPressed [Bitmap] which represents the pressed inner movable part of the joystick. |
| 27 | * @param rectOuter [Rect] which represents the outer joystick bounds. | 29 | * @param rectOuter [Rect] which represents the outer joystick bounds. |
| 28 | * @param rectInner [Rect] which represents the inner joystick bounds. | 30 | * @param rectInner [Rect] which represents the inner joystick bounds. |
| 29 | * @param joystickId The ID value what type of joystick this Drawable represents. | 31 | * @param joystick The [NativeAnalog] this Drawable represents. |
| 30 | * @param buttonId The ID value what type of button this Drawable represents. | 32 | * @param button The [NativeButton] this Drawable represents. |
| 31 | */ | 33 | */ |
| 32 | class InputOverlayDrawableJoystick( | 34 | class InputOverlayDrawableJoystick( |
| 33 | res: Resources, | 35 | res: Resources, |
| @@ -36,8 +38,8 @@ class InputOverlayDrawableJoystick( | |||
| 36 | bitmapInnerPressed: Bitmap, | 38 | bitmapInnerPressed: Bitmap, |
| 37 | rectOuter: Rect, | 39 | rectOuter: Rect, |
| 38 | rectInner: Rect, | 40 | rectInner: Rect, |
| 39 | val joystickId: Int, | 41 | val joystick: NativeAnalog, |
| 40 | val buttonId: Int, | 42 | val button: NativeButton, |
| 41 | val prefId: String | 43 | val prefId: String |
| 42 | ) { | 44 | ) { |
| 43 | // The ID value what motion event is tracking | 45 | // The ID value what motion event is tracking |
| @@ -69,8 +71,7 @@ class InputOverlayDrawableJoystick( | |||
| 69 | 71 | ||
| 70 | // TODO: Add button support | 72 | // TODO: Add button support |
| 71 | val buttonStatus: Int | 73 | val buttonStatus: Int |
| 72 | get() = | 74 | get() = ButtonState.RELEASED |
| 73 | NativeLibrary.ButtonState.RELEASED | ||
| 74 | var bounds: Rect | 75 | var bounds: Rect |
| 75 | get() = outerBitmap.bounds | 76 | get() = outerBitmap.bounds |
| 76 | set(bounds) { | 77 | set(bounds) { |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt index e63382e1d..2c7356e6a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt | |||
| @@ -6,439 +6,89 @@ package org.yuzu.yuzu_emu.utils | |||
| 6 | import android.view.InputDevice | 6 | import android.view.InputDevice |
| 7 | import android.view.KeyEvent | 7 | import android.view.KeyEvent |
| 8 | import android.view.MotionEvent | 8 | import android.view.MotionEvent |
| 9 | import kotlin.math.sqrt | 9 | import org.yuzu.yuzu_emu.features.input.NativeInput |
| 10 | import org.yuzu.yuzu_emu.NativeLibrary | 10 | import org.yuzu.yuzu_emu.features.input.YuzuInputOverlayDevice |
| 11 | import org.yuzu.yuzu_emu.features.input.YuzuPhysicalDevice | ||
| 11 | 12 | ||
| 12 | object InputHandler { | 13 | object InputHandler { |
| 13 | private var controllerIds = getGameControllerIds() | 14 | var androidControllers = mapOf<Int, YuzuPhysicalDevice>() |
| 14 | 15 | var registeredControllers = mutableListOf<ParamPackage>() | |
| 15 | fun initialize() { | ||
| 16 | // Connect first controller | ||
| 17 | NativeLibrary.onGamePadConnectEvent(getPlayerNumber(NativeLibrary.Player1Device)) | ||
| 18 | } | ||
| 19 | |||
| 20 | fun updateControllerIds() { | ||
| 21 | controllerIds = getGameControllerIds() | ||
| 22 | } | ||
| 23 | 16 | ||
| 24 | fun dispatchKeyEvent(event: KeyEvent): Boolean { | 17 | fun dispatchKeyEvent(event: KeyEvent): Boolean { |
| 25 | val button: Int = when (event.device.vendorId) { | ||
| 26 | 0x045E -> getInputXboxButtonKey(event.keyCode) | ||
| 27 | 0x054C -> getInputDS5ButtonKey(event.keyCode) | ||
| 28 | 0x057E -> getInputJoyconButtonKey(event.keyCode) | ||
| 29 | 0x1532 -> getInputRazerButtonKey(event.keyCode) | ||
| 30 | 0x3537 -> getInputRedmagicButtonKey(event.keyCode) | ||
| 31 | 0x358A -> getInputBackboneLabsButtonKey(event.keyCode) | ||
| 32 | else -> getInputGenericButtonKey(event.keyCode) | ||
| 33 | } | ||
| 34 | |||
| 35 | val action = when (event.action) { | 18 | val action = when (event.action) { |
| 36 | KeyEvent.ACTION_DOWN -> NativeLibrary.ButtonState.PRESSED | 19 | KeyEvent.ACTION_DOWN -> NativeInput.ButtonState.PRESSED |
| 37 | KeyEvent.ACTION_UP -> NativeLibrary.ButtonState.RELEASED | 20 | KeyEvent.ACTION_UP -> NativeInput.ButtonState.RELEASED |
| 38 | else -> return false | 21 | else -> return false |
| 39 | } | 22 | } |
| 40 | 23 | ||
| 41 | // Ignore invalid buttons | 24 | var controllerData = androidControllers[event.device.controllerNumber] |
| 42 | if (button < 0) { | 25 | if (controllerData == null) { |
| 43 | return false | 26 | updateControllerData() |
| 27 | controllerData = androidControllers[event.device.controllerNumber] ?: return false | ||
| 44 | } | 28 | } |
| 45 | 29 | ||
| 46 | return NativeLibrary.onGamePadButtonEvent( | 30 | NativeInput.onGamePadButtonEvent( |
| 47 | getPlayerNumber(event.device.controllerNumber, event.deviceId), | 31 | controllerData.getGUID(), |
| 48 | button, | 32 | controllerData.getPort(), |
| 33 | event.keyCode, | ||
| 49 | action | 34 | action |
| 50 | ) | 35 | ) |
| 36 | return true | ||
| 51 | } | 37 | } |
| 52 | 38 | ||
| 53 | fun dispatchGenericMotionEvent(event: MotionEvent): Boolean { | 39 | fun dispatchGenericMotionEvent(event: MotionEvent): Boolean { |
| 54 | val device = event.device | 40 | val controllerData = |
| 55 | // Check every axis input available on the controller | 41 | androidControllers[event.device.controllerNumber] ?: return false |
| 56 | for (range in device.motionRanges) { | 42 | event.device.motionRanges.forEach { |
| 57 | val axis = range.axis | 43 | NativeInput.onGamePadAxisEvent( |
| 58 | when (device.vendorId) { | 44 | controllerData.getGUID(), |
| 59 | 0x045E -> setGenericAxisInput(event, axis) | 45 | controllerData.getPort(), |
| 60 | 0x054C -> setGenericAxisInput(event, axis) | 46 | it.axis, |
| 61 | 0x057E -> setJoyconAxisInput(event, axis) | 47 | event.getAxisValue(it.axis) |
| 62 | 0x1532 -> setRazerAxisInput(event, axis) | 48 | ) |
| 63 | else -> setGenericAxisInput(event, axis) | ||
| 64 | } | ||
| 65 | } | 49 | } |
| 66 | |||
| 67 | return true | 50 | return true |
| 68 | } | 51 | } |
| 69 | 52 | ||
| 70 | private fun getPlayerNumber(index: Int, deviceId: Int = -1): Int { | 53 | fun getDevices(): Map<Int, YuzuPhysicalDevice> { |
| 71 | var deviceIndex = index | 54 | val gameControllerDeviceIds = mutableMapOf<Int, YuzuPhysicalDevice>() |
| 72 | if (deviceId != -1) { | ||
| 73 | deviceIndex = controllerIds[deviceId] ?: 0 | ||
| 74 | } | ||
| 75 | |||
| 76 | // TODO: Joycons are handled as different controllers. Find a way to merge them. | ||
| 77 | return when (deviceIndex) { | ||
| 78 | 2 -> NativeLibrary.Player2Device | ||
| 79 | 3 -> NativeLibrary.Player3Device | ||
| 80 | 4 -> NativeLibrary.Player4Device | ||
| 81 | 5 -> NativeLibrary.Player5Device | ||
| 82 | 6 -> NativeLibrary.Player6Device | ||
| 83 | 7 -> NativeLibrary.Player7Device | ||
| 84 | 8 -> NativeLibrary.Player8Device | ||
| 85 | else -> if (NativeLibrary.isHandheldOnly()) { | ||
| 86 | NativeLibrary.ConsoleDevice | ||
| 87 | } else { | ||
| 88 | NativeLibrary.Player1Device | ||
| 89 | } | ||
| 90 | } | ||
| 91 | } | ||
| 92 | |||
| 93 | private fun setStickState(playerNumber: Int, index: Int, xAxis: Float, yAxis: Float) { | ||
| 94 | // Calculate vector size | ||
| 95 | val r2 = xAxis * xAxis + yAxis * yAxis | ||
| 96 | var r = sqrt(r2.toDouble()).toFloat() | ||
| 97 | |||
| 98 | // Adjust range of joystick | ||
| 99 | val deadzone = 0.15f | ||
| 100 | var x = xAxis | ||
| 101 | var y = yAxis | ||
| 102 | |||
| 103 | if (r > deadzone) { | ||
| 104 | val deadzoneFactor = 1.0f / r * (r - deadzone) / (1.0f - deadzone) | ||
| 105 | x *= deadzoneFactor | ||
| 106 | y *= deadzoneFactor | ||
| 107 | r *= deadzoneFactor | ||
| 108 | } else { | ||
| 109 | x = 0.0f | ||
| 110 | y = 0.0f | ||
| 111 | } | ||
| 112 | |||
| 113 | // Normalize joystick | ||
| 114 | if (r > 1.0f) { | ||
| 115 | x /= r | ||
| 116 | y /= r | ||
| 117 | } | ||
| 118 | |||
| 119 | NativeLibrary.onGamePadJoystickEvent( | ||
| 120 | playerNumber, | ||
| 121 | index, | ||
| 122 | x, | ||
| 123 | -y | ||
| 124 | ) | ||
| 125 | } | ||
| 126 | |||
| 127 | private fun getAxisToButton(axis: Float): Int { | ||
| 128 | return if (axis > 0.5f) { | ||
| 129 | NativeLibrary.ButtonState.PRESSED | ||
| 130 | } else { | ||
| 131 | NativeLibrary.ButtonState.RELEASED | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | private fun setAxisDpadState(playerNumber: Int, xAxis: Float, yAxis: Float) { | ||
| 136 | NativeLibrary.onGamePadButtonEvent( | ||
| 137 | playerNumber, | ||
| 138 | NativeLibrary.ButtonType.DPAD_UP, | ||
| 139 | getAxisToButton(-yAxis) | ||
| 140 | ) | ||
| 141 | NativeLibrary.onGamePadButtonEvent( | ||
| 142 | playerNumber, | ||
| 143 | NativeLibrary.ButtonType.DPAD_DOWN, | ||
| 144 | getAxisToButton(yAxis) | ||
| 145 | ) | ||
| 146 | NativeLibrary.onGamePadButtonEvent( | ||
| 147 | playerNumber, | ||
| 148 | NativeLibrary.ButtonType.DPAD_LEFT, | ||
| 149 | getAxisToButton(-xAxis) | ||
| 150 | ) | ||
| 151 | NativeLibrary.onGamePadButtonEvent( | ||
| 152 | playerNumber, | ||
| 153 | NativeLibrary.ButtonType.DPAD_RIGHT, | ||
| 154 | getAxisToButton(xAxis) | ||
| 155 | ) | ||
| 156 | } | ||
| 157 | |||
| 158 | private fun getInputDS5ButtonKey(key: Int): Int { | ||
| 159 | // The missing ds5 buttons are axis | ||
| 160 | return when (key) { | ||
| 161 | KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B | ||
| 162 | KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A | ||
| 163 | KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y | ||
| 164 | KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X | ||
| 165 | KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L | ||
| 166 | KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R | ||
| 167 | KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L | ||
| 168 | KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R | ||
| 169 | KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS | ||
| 170 | KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS | ||
| 171 | else -> -1 | ||
| 172 | } | ||
| 173 | } | ||
| 174 | |||
| 175 | private fun getInputJoyconButtonKey(key: Int): Int { | ||
| 176 | // Joycon support is half dead. A lot of buttons can't be mapped | ||
| 177 | return when (key) { | ||
| 178 | KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B | ||
| 179 | KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A | ||
| 180 | KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X | ||
| 181 | KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y | ||
| 182 | KeyEvent.KEYCODE_DPAD_UP -> NativeLibrary.ButtonType.DPAD_UP | ||
| 183 | KeyEvent.KEYCODE_DPAD_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN | ||
| 184 | KeyEvent.KEYCODE_DPAD_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT | ||
| 185 | KeyEvent.KEYCODE_DPAD_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT | ||
| 186 | KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L | ||
| 187 | KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R | ||
| 188 | KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL | ||
| 189 | KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR | ||
| 190 | KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L | ||
| 191 | KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R | ||
| 192 | KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS | ||
| 193 | KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS | ||
| 194 | else -> -1 | ||
| 195 | } | ||
| 196 | } | ||
| 197 | |||
| 198 | private fun getInputXboxButtonKey(key: Int): Int { | ||
| 199 | // The missing xbox buttons are axis | ||
| 200 | return when (key) { | ||
| 201 | KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A | ||
| 202 | KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B | ||
| 203 | KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X | ||
| 204 | KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y | ||
| 205 | KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L | ||
| 206 | KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R | ||
| 207 | KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L | ||
| 208 | KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R | ||
| 209 | KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS | ||
| 210 | KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS | ||
| 211 | else -> -1 | ||
| 212 | } | ||
| 213 | } | ||
| 214 | |||
| 215 | private fun getInputRazerButtonKey(key: Int): Int { | ||
| 216 | // The missing xbox buttons are axis | ||
| 217 | return when (key) { | ||
| 218 | KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B | ||
| 219 | KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A | ||
| 220 | KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y | ||
| 221 | KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X | ||
| 222 | KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L | ||
| 223 | KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R | ||
| 224 | KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L | ||
| 225 | KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R | ||
| 226 | KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS | ||
| 227 | KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS | ||
| 228 | else -> -1 | ||
| 229 | } | ||
| 230 | } | ||
| 231 | |||
| 232 | private fun getInputRedmagicButtonKey(key: Int): Int { | ||
| 233 | return when (key) { | ||
| 234 | KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B | ||
| 235 | KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A | ||
| 236 | KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y | ||
| 237 | KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X | ||
| 238 | KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L | ||
| 239 | KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R | ||
| 240 | KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL | ||
| 241 | KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR | ||
| 242 | KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L | ||
| 243 | KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R | ||
| 244 | KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS | ||
| 245 | KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS | ||
| 246 | else -> -1 | ||
| 247 | } | ||
| 248 | } | ||
| 249 | |||
| 250 | private fun getInputBackboneLabsButtonKey(key: Int): Int { | ||
| 251 | return when (key) { | ||
| 252 | KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B | ||
| 253 | KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A | ||
| 254 | KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y | ||
| 255 | KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X | ||
| 256 | KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L | ||
| 257 | KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R | ||
| 258 | KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL | ||
| 259 | KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR | ||
| 260 | KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L | ||
| 261 | KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R | ||
| 262 | KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS | ||
| 263 | KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS | ||
| 264 | else -> -1 | ||
| 265 | } | ||
| 266 | } | ||
| 267 | |||
| 268 | private fun getInputGenericButtonKey(key: Int): Int { | ||
| 269 | return when (key) { | ||
| 270 | KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A | ||
| 271 | KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B | ||
| 272 | KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X | ||
| 273 | KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y | ||
| 274 | KeyEvent.KEYCODE_DPAD_UP -> NativeLibrary.ButtonType.DPAD_UP | ||
| 275 | KeyEvent.KEYCODE_DPAD_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN | ||
| 276 | KeyEvent.KEYCODE_DPAD_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT | ||
| 277 | KeyEvent.KEYCODE_DPAD_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT | ||
| 278 | KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L | ||
| 279 | KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R | ||
| 280 | KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL | ||
| 281 | KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR | ||
| 282 | KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L | ||
| 283 | KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R | ||
| 284 | KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS | ||
| 285 | KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS | ||
| 286 | else -> -1 | ||
| 287 | } | ||
| 288 | } | ||
| 289 | |||
| 290 | private fun setGenericAxisInput(event: MotionEvent, axis: Int) { | ||
| 291 | val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId) | ||
| 292 | |||
| 293 | when (axis) { | ||
| 294 | MotionEvent.AXIS_X, MotionEvent.AXIS_Y -> | ||
| 295 | setStickState( | ||
| 296 | playerNumber, | ||
| 297 | NativeLibrary.StickType.STICK_L, | ||
| 298 | event.getAxisValue(MotionEvent.AXIS_X), | ||
| 299 | event.getAxisValue(MotionEvent.AXIS_Y) | ||
| 300 | ) | ||
| 301 | MotionEvent.AXIS_RX, MotionEvent.AXIS_RY -> | ||
| 302 | setStickState( | ||
| 303 | playerNumber, | ||
| 304 | NativeLibrary.StickType.STICK_R, | ||
| 305 | event.getAxisValue(MotionEvent.AXIS_RX), | ||
| 306 | event.getAxisValue(MotionEvent.AXIS_RY) | ||
| 307 | ) | ||
| 308 | MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ -> | ||
| 309 | setStickState( | ||
| 310 | playerNumber, | ||
| 311 | NativeLibrary.StickType.STICK_R, | ||
| 312 | event.getAxisValue(MotionEvent.AXIS_Z), | ||
| 313 | event.getAxisValue(MotionEvent.AXIS_RZ) | ||
| 314 | ) | ||
| 315 | MotionEvent.AXIS_LTRIGGER -> | ||
| 316 | NativeLibrary.onGamePadButtonEvent( | ||
| 317 | playerNumber, | ||
| 318 | NativeLibrary.ButtonType.TRIGGER_ZL, | ||
| 319 | getAxisToButton(event.getAxisValue(MotionEvent.AXIS_LTRIGGER)) | ||
| 320 | ) | ||
| 321 | MotionEvent.AXIS_BRAKE -> | ||
| 322 | NativeLibrary.onGamePadButtonEvent( | ||
| 323 | playerNumber, | ||
| 324 | NativeLibrary.ButtonType.TRIGGER_ZL, | ||
| 325 | getAxisToButton(event.getAxisValue(MotionEvent.AXIS_BRAKE)) | ||
| 326 | ) | ||
| 327 | MotionEvent.AXIS_RTRIGGER -> | ||
| 328 | NativeLibrary.onGamePadButtonEvent( | ||
| 329 | playerNumber, | ||
| 330 | NativeLibrary.ButtonType.TRIGGER_ZR, | ||
| 331 | getAxisToButton(event.getAxisValue(MotionEvent.AXIS_RTRIGGER)) | ||
| 332 | ) | ||
| 333 | MotionEvent.AXIS_GAS -> | ||
| 334 | NativeLibrary.onGamePadButtonEvent( | ||
| 335 | playerNumber, | ||
| 336 | NativeLibrary.ButtonType.TRIGGER_ZR, | ||
| 337 | getAxisToButton(event.getAxisValue(MotionEvent.AXIS_GAS)) | ||
| 338 | ) | ||
| 339 | MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y -> | ||
| 340 | setAxisDpadState( | ||
| 341 | playerNumber, | ||
| 342 | event.getAxisValue(MotionEvent.AXIS_HAT_X), | ||
| 343 | event.getAxisValue(MotionEvent.AXIS_HAT_Y) | ||
| 344 | ) | ||
| 345 | } | ||
| 346 | } | ||
| 347 | |||
| 348 | private fun setJoyconAxisInput(event: MotionEvent, axis: Int) { | ||
| 349 | // Joycon support is half dead. Right joystick doesn't work | ||
| 350 | val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId) | ||
| 351 | |||
| 352 | when (axis) { | ||
| 353 | MotionEvent.AXIS_X, MotionEvent.AXIS_Y -> | ||
| 354 | setStickState( | ||
| 355 | playerNumber, | ||
| 356 | NativeLibrary.StickType.STICK_L, | ||
| 357 | event.getAxisValue(MotionEvent.AXIS_X), | ||
| 358 | event.getAxisValue(MotionEvent.AXIS_Y) | ||
| 359 | ) | ||
| 360 | MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ -> | ||
| 361 | setStickState( | ||
| 362 | playerNumber, | ||
| 363 | NativeLibrary.StickType.STICK_R, | ||
| 364 | event.getAxisValue(MotionEvent.AXIS_Z), | ||
| 365 | event.getAxisValue(MotionEvent.AXIS_RZ) | ||
| 366 | ) | ||
| 367 | MotionEvent.AXIS_RX, MotionEvent.AXIS_RY -> | ||
| 368 | setStickState( | ||
| 369 | playerNumber, | ||
| 370 | NativeLibrary.StickType.STICK_R, | ||
| 371 | event.getAxisValue(MotionEvent.AXIS_RX), | ||
| 372 | event.getAxisValue(MotionEvent.AXIS_RY) | ||
| 373 | ) | ||
| 374 | } | ||
| 375 | } | ||
| 376 | |||
| 377 | private fun setRazerAxisInput(event: MotionEvent, axis: Int) { | ||
| 378 | val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId) | ||
| 379 | |||
| 380 | when (axis) { | ||
| 381 | MotionEvent.AXIS_X, MotionEvent.AXIS_Y -> | ||
| 382 | setStickState( | ||
| 383 | playerNumber, | ||
| 384 | NativeLibrary.StickType.STICK_L, | ||
| 385 | event.getAxisValue(MotionEvent.AXIS_X), | ||
| 386 | event.getAxisValue(MotionEvent.AXIS_Y) | ||
| 387 | ) | ||
| 388 | MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ -> | ||
| 389 | setStickState( | ||
| 390 | playerNumber, | ||
| 391 | NativeLibrary.StickType.STICK_R, | ||
| 392 | event.getAxisValue(MotionEvent.AXIS_Z), | ||
| 393 | event.getAxisValue(MotionEvent.AXIS_RZ) | ||
| 394 | ) | ||
| 395 | MotionEvent.AXIS_BRAKE -> | ||
| 396 | NativeLibrary.onGamePadButtonEvent( | ||
| 397 | playerNumber, | ||
| 398 | NativeLibrary.ButtonType.TRIGGER_ZL, | ||
| 399 | getAxisToButton(event.getAxisValue(MotionEvent.AXIS_BRAKE)) | ||
| 400 | ) | ||
| 401 | MotionEvent.AXIS_GAS -> | ||
| 402 | NativeLibrary.onGamePadButtonEvent( | ||
| 403 | playerNumber, | ||
| 404 | NativeLibrary.ButtonType.TRIGGER_ZR, | ||
| 405 | getAxisToButton(event.getAxisValue(MotionEvent.AXIS_GAS)) | ||
| 406 | ) | ||
| 407 | MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y -> | ||
| 408 | setAxisDpadState( | ||
| 409 | playerNumber, | ||
| 410 | event.getAxisValue(MotionEvent.AXIS_HAT_X), | ||
| 411 | event.getAxisValue(MotionEvent.AXIS_HAT_Y) | ||
| 412 | ) | ||
| 413 | } | ||
| 414 | } | ||
| 415 | |||
| 416 | fun getGameControllerIds(): Map<Int, Int> { | ||
| 417 | val gameControllerDeviceIds = mutableMapOf<Int, Int>() | ||
| 418 | val deviceIds = InputDevice.getDeviceIds() | 55 | val deviceIds = InputDevice.getDeviceIds() |
| 419 | var controllerSlot = 1 | 56 | var port = 0 |
| 57 | val inputSettings = NativeConfig.getInputSettings(true) | ||
| 420 | deviceIds.forEach { deviceId -> | 58 | deviceIds.forEach { deviceId -> |
| 421 | InputDevice.getDevice(deviceId)?.apply { | 59 | InputDevice.getDevice(deviceId)?.apply { |
| 422 | // Don't over-assign controllers | ||
| 423 | if (controllerSlot >= 8) { | ||
| 424 | return gameControllerDeviceIds | ||
| 425 | } | ||
| 426 | |||
| 427 | // Verify that the device has gamepad buttons, control sticks, or both. | 60 | // Verify that the device has gamepad buttons, control sticks, or both. |
| 428 | if (sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD || | 61 | if (sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD || |
| 429 | sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK | 62 | sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK |
| 430 | ) { | 63 | ) { |
| 431 | // This device is a game controller. Store its device ID. | 64 | if (!gameControllerDeviceIds.contains(controllerNumber)) { |
| 432 | if (deviceId and id and vendorId and productId != 0) { | 65 | gameControllerDeviceIds[controllerNumber] = YuzuPhysicalDevice( |
| 433 | // Additionally filter out devices that have no ID | 66 | this, |
| 434 | gameControllerDeviceIds | 67 | port, |
| 435 | .takeIf { !it.contains(deviceId) } | 68 | inputSettings[port].useSystemVibrator |
| 436 | ?.put(deviceId, controllerSlot) | 69 | ) |
| 437 | controllerSlot++ | ||
| 438 | } | 70 | } |
| 71 | port++ | ||
| 439 | } | 72 | } |
| 440 | } | 73 | } |
| 441 | } | 74 | } |
| 442 | return gameControllerDeviceIds | 75 | return gameControllerDeviceIds |
| 443 | } | 76 | } |
| 77 | |||
| 78 | fun updateControllerData() { | ||
| 79 | androidControllers = getDevices() | ||
| 80 | androidControllers.forEach { | ||
| 81 | NativeInput.registerController(it.value) | ||
| 82 | } | ||
| 83 | |||
| 84 | // Register the input overlay on a dedicated port for all player 1 vibrations | ||
| 85 | NativeInput.registerController(YuzuInputOverlayDevice(androidControllers.isEmpty(), 100)) | ||
| 86 | registeredControllers.clear() | ||
| 87 | NativeInput.getInputDevices().forEach { | ||
| 88 | registeredControllers.add(ParamPackage(it)) | ||
| 89 | } | ||
| 90 | registeredControllers.sortBy { it.get("port", 0) } | ||
| 91 | } | ||
| 92 | |||
| 93 | fun InputDevice.getGUID(): String = String.format("%016x%016x", productId, vendorId) | ||
| 444 | } | 94 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt index a4c14b3a7..7228f25d2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt | |||
| @@ -6,6 +6,8 @@ package org.yuzu.yuzu_emu.utils | |||
| 6 | import org.yuzu.yuzu_emu.model.GameDir | 6 | import org.yuzu.yuzu_emu.model.GameDir |
| 7 | import org.yuzu.yuzu_emu.overlay.model.OverlayControlData | 7 | import org.yuzu.yuzu_emu.overlay.model.OverlayControlData |
| 8 | 8 | ||
| 9 | import org.yuzu.yuzu_emu.features.input.model.PlayerInput | ||
| 10 | |||
| 9 | object NativeConfig { | 11 | object NativeConfig { |
| 10 | /** | 12 | /** |
| 11 | * Loads global config. | 13 | * Loads global config. |
| @@ -168,4 +170,17 @@ object NativeConfig { | |||
| 168 | */ | 170 | */ |
| 169 | @Synchronized | 171 | @Synchronized |
| 170 | external fun setOverlayControlData(overlayControlData: Array<OverlayControlData>) | 172 | external fun setOverlayControlData(overlayControlData: Array<OverlayControlData>) |
| 173 | |||
| 174 | @Synchronized | ||
| 175 | external fun getInputSettings(global: Boolean): Array<PlayerInput> | ||
| 176 | |||
| 177 | @Synchronized | ||
| 178 | external fun setInputSettings(value: Array<PlayerInput>, global: Boolean) | ||
| 179 | |||
| 180 | /** | ||
| 181 | * Saves control values for a specific player | ||
| 182 | * Must be used when per game config is loaded | ||
| 183 | */ | ||
| 184 | @Synchronized | ||
| 185 | external fun saveControlPlayerValues() | ||
| 171 | } | 186 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt index 68ed66565..331b7ddca 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt | |||
| @@ -14,7 +14,7 @@ import android.os.Build | |||
| 14 | import android.os.Handler | 14 | import android.os.Handler |
| 15 | import android.os.Looper | 15 | import android.os.Looper |
| 16 | import java.io.IOException | 16 | import java.io.IOException |
| 17 | import org.yuzu.yuzu_emu.NativeLibrary | 17 | import org.yuzu.yuzu_emu.features.input.NativeInput |
| 18 | 18 | ||
| 19 | class NfcReader(private val activity: Activity) { | 19 | class NfcReader(private val activity: Activity) { |
| 20 | private var nfcAdapter: NfcAdapter? = null | 20 | private var nfcAdapter: NfcAdapter? = null |
| @@ -76,12 +76,12 @@ class NfcReader(private val activity: Activity) { | |||
| 76 | amiibo.connect() | 76 | amiibo.connect() |
| 77 | 77 | ||
| 78 | val tagData = ntag215ReadAll(amiibo) ?: return | 78 | val tagData = ntag215ReadAll(amiibo) ?: return |
| 79 | NativeLibrary.onReadNfcTag(tagData) | 79 | NativeInput.onReadNfcTag(tagData) |
| 80 | 80 | ||
| 81 | nfcAdapter?.ignore( | 81 | nfcAdapter?.ignore( |
| 82 | tag, | 82 | tag, |
| 83 | 1000, | 83 | 1000, |
| 84 | { NativeLibrary.onRemoveNfcTag() }, | 84 | { NativeInput.onRemoveNfcTag() }, |
| 85 | Handler(Looper.getMainLooper()) | 85 | Handler(Looper.getMainLooper()) |
| 86 | ) | 86 | ) |
| 87 | } | 87 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ParamPackage.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ParamPackage.kt new file mode 100644 index 000000000..83fc7da3c --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ParamPackage.kt | |||
| @@ -0,0 +1,141 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.utils | ||
| 5 | |||
| 6 | // Kotlin version of src/common/param_package.h | ||
| 7 | class ParamPackage(serialized: String = "") { | ||
| 8 | private val KEY_VALUE_SEPARATOR = ":" | ||
| 9 | private val PARAM_SEPARATOR = "," | ||
| 10 | |||
| 11 | private val ESCAPE_CHARACTER = "$" | ||
| 12 | private val KEY_VALUE_SEPARATOR_ESCAPE = "$0" | ||
| 13 | private val PARAM_SEPARATOR_ESCAPE = "$1" | ||
| 14 | private val ESCAPE_CHARACTER_ESCAPE = "$2" | ||
| 15 | |||
| 16 | private val EMPTY_PLACEHOLDER = "[empty]" | ||
| 17 | |||
| 18 | val data = mutableMapOf<String, String>() | ||
| 19 | |||
| 20 | init { | ||
| 21 | val pairs = serialized.split(PARAM_SEPARATOR) | ||
| 22 | for (pair in pairs) { | ||
| 23 | val keyValue = pair.split(KEY_VALUE_SEPARATOR).toMutableList() | ||
| 24 | if (keyValue.size != 2) { | ||
| 25 | Log.error("[ParamPackage] Invalid key pair $keyValue") | ||
| 26 | continue | ||
| 27 | } | ||
| 28 | |||
| 29 | keyValue.forEachIndexed { i: Int, _: String -> | ||
| 30 | keyValue[i] = keyValue[i].replace(KEY_VALUE_SEPARATOR_ESCAPE, KEY_VALUE_SEPARATOR) | ||
| 31 | keyValue[i] = keyValue[i].replace(PARAM_SEPARATOR_ESCAPE, PARAM_SEPARATOR) | ||
| 32 | keyValue[i] = keyValue[i].replace(ESCAPE_CHARACTER_ESCAPE, ESCAPE_CHARACTER) | ||
| 33 | } | ||
| 34 | |||
| 35 | set(keyValue[0], keyValue[1]) | ||
| 36 | } | ||
| 37 | } | ||
| 38 | |||
| 39 | constructor(params: List<Pair<String, String>>) : this() { | ||
| 40 | params.forEach { | ||
| 41 | data[it.first] = it.second | ||
| 42 | } | ||
| 43 | } | ||
| 44 | |||
| 45 | fun serialize(): String { | ||
| 46 | if (data.isEmpty()) { | ||
| 47 | return EMPTY_PLACEHOLDER | ||
| 48 | } | ||
| 49 | |||
| 50 | val result = StringBuilder() | ||
| 51 | data.forEach { | ||
| 52 | val keyValue = mutableListOf(it.key, it.value) | ||
| 53 | keyValue.forEachIndexed { i, _ -> | ||
| 54 | keyValue[i] = keyValue[i].replace(ESCAPE_CHARACTER, ESCAPE_CHARACTER_ESCAPE) | ||
| 55 | keyValue[i] = keyValue[i].replace(PARAM_SEPARATOR, PARAM_SEPARATOR_ESCAPE) | ||
| 56 | keyValue[i] = keyValue[i].replace(KEY_VALUE_SEPARATOR, KEY_VALUE_SEPARATOR_ESCAPE) | ||
| 57 | } | ||
| 58 | result.append("${keyValue[0]}$KEY_VALUE_SEPARATOR${keyValue[1]}$PARAM_SEPARATOR") | ||
| 59 | } | ||
| 60 | return result.removeSuffix(PARAM_SEPARATOR).toString() | ||
| 61 | } | ||
| 62 | |||
| 63 | fun get(key: String, defaultValue: String): String = | ||
| 64 | if (has(key)) { | ||
| 65 | data[key]!! | ||
| 66 | } else { | ||
| 67 | Log.debug("[ParamPackage] key $key not found") | ||
| 68 | defaultValue | ||
| 69 | } | ||
| 70 | |||
| 71 | fun get(key: String, defaultValue: Int): Int = | ||
| 72 | if (has(key)) { | ||
| 73 | try { | ||
| 74 | data[key]!!.toInt() | ||
| 75 | } catch (e: NumberFormatException) { | ||
| 76 | Log.debug("[ParamPackage] failed to convert ${data[key]!!} to int") | ||
| 77 | defaultValue | ||
| 78 | } | ||
| 79 | } else { | ||
| 80 | Log.debug("[ParamPackage] key $key not found") | ||
| 81 | defaultValue | ||
| 82 | } | ||
| 83 | |||
| 84 | private fun Int.toBoolean(): Boolean = | ||
| 85 | if (this == 1) { | ||
| 86 | true | ||
| 87 | } else if (this == 0) { | ||
| 88 | false | ||
| 89 | } else { | ||
| 90 | throw Exception("Tried to convert a value to a boolean that was not 0 or 1!") | ||
| 91 | } | ||
| 92 | |||
| 93 | fun get(key: String, defaultValue: Boolean): Boolean = | ||
| 94 | if (has(key)) { | ||
| 95 | try { | ||
| 96 | get(key, if (defaultValue) 1 else 0).toBoolean() | ||
| 97 | } catch (e: Exception) { | ||
| 98 | Log.debug("[ParamPackage] failed to convert ${data[key]!!} to boolean") | ||
| 99 | defaultValue | ||
| 100 | } | ||
| 101 | } else { | ||
| 102 | Log.debug("[ParamPackage] key $key not found") | ||
| 103 | defaultValue | ||
| 104 | } | ||
| 105 | |||
| 106 | fun get(key: String, defaultValue: Float): Float = | ||
| 107 | if (has(key)) { | ||
| 108 | try { | ||
| 109 | data[key]!!.toFloat() | ||
| 110 | } catch (e: NumberFormatException) { | ||
| 111 | Log.debug("[ParamPackage] failed to convert ${data[key]!!} to float") | ||
| 112 | defaultValue | ||
| 113 | } | ||
| 114 | } else { | ||
| 115 | Log.debug("[ParamPackage] key $key not found") | ||
| 116 | defaultValue | ||
| 117 | } | ||
| 118 | |||
| 119 | fun set(key: String, value: String) { | ||
| 120 | data[key] = value | ||
| 121 | } | ||
| 122 | |||
| 123 | fun set(key: String, value: Int) { | ||
| 124 | data[key] = value.toString() | ||
| 125 | } | ||
| 126 | |||
| 127 | fun Boolean.toInt(): Int = if (this) 1 else 0 | ||
| 128 | fun set(key: String, value: Boolean) { | ||
| 129 | data[key] = value.toInt().toString() | ||
| 130 | } | ||
| 131 | |||
| 132 | fun set(key: String, value: Float) { | ||
| 133 | data[key] = value.toString() | ||
| 134 | } | ||
| 135 | |||
| 136 | fun has(key: String): Boolean = data.containsKey(key) | ||
| 137 | |||
| 138 | fun erase(key: String) = data.remove(key) | ||
| 139 | |||
| 140 | fun clear() = data.clear() | ||
| 141 | } | ||
diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt index 20b319c12..ec8ae5c57 100644 --- a/src/android/app/src/main/jni/CMakeLists.txt +++ b/src/android/app/src/main/jni/CMakeLists.txt | |||
| @@ -12,6 +12,7 @@ add_library(yuzu-android SHARED | |||
| 12 | native_log.cpp | 12 | native_log.cpp |
| 13 | android_config.cpp | 13 | android_config.cpp |
| 14 | android_config.h | 14 | android_config.h |
| 15 | native_input.cpp | ||
| 15 | ) | 16 | ) |
| 16 | 17 | ||
| 17 | set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR}) | 18 | set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR}) |
diff --git a/src/android/app/src/main/jni/android_config.cpp b/src/android/app/src/main/jni/android_config.cpp index e147560c3..a79a64afb 100644 --- a/src/android/app/src/main/jni/android_config.cpp +++ b/src/android/app/src/main/jni/android_config.cpp | |||
| @@ -1,6 +1,8 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | 2 | // SPDX-License-Identifier: GPL-2.0-or-later |
| 3 | 3 | ||
| 4 | #include <common/logging/log.h> | ||
| 5 | #include <input_common/main.h> | ||
| 4 | #include "android_config.h" | 6 | #include "android_config.h" |
| 5 | #include "android_settings.h" | 7 | #include "android_settings.h" |
| 6 | #include "common/settings_setting.h" | 8 | #include "common/settings_setting.h" |
| @@ -32,6 +34,7 @@ void AndroidConfig::ReadAndroidValues() { | |||
| 32 | ReadOverlayValues(); | 34 | ReadOverlayValues(); |
| 33 | } | 35 | } |
| 34 | ReadDriverValues(); | 36 | ReadDriverValues(); |
| 37 | ReadAndroidControlValues(); | ||
| 35 | } | 38 | } |
| 36 | 39 | ||
| 37 | void AndroidConfig::ReadAndroidUIValues() { | 40 | void AndroidConfig::ReadAndroidUIValues() { |
| @@ -107,6 +110,76 @@ void AndroidConfig::ReadOverlayValues() { | |||
| 107 | EndGroup(); | 110 | EndGroup(); |
| 108 | } | 111 | } |
| 109 | 112 | ||
| 113 | void AndroidConfig::ReadAndroidPlayerValues(std::size_t player_index) { | ||
| 114 | std::string player_prefix; | ||
| 115 | if (type != ConfigType::InputProfile) { | ||
| 116 | player_prefix.append("player_").append(ToString(player_index)).append("_"); | ||
| 117 | } | ||
| 118 | |||
| 119 | auto& player = Settings::values.players.GetValue()[player_index]; | ||
| 120 | if (IsCustomConfig()) { | ||
| 121 | const auto profile_name = | ||
| 122 | ReadStringSetting(std::string(player_prefix).append("profile_name")); | ||
| 123 | if (profile_name.empty()) { | ||
| 124 | // Use the global input config | ||
| 125 | player = Settings::values.players.GetValue(true)[player_index]; | ||
| 126 | player.profile_name = ""; | ||
| 127 | return; | ||
| 128 | } | ||
| 129 | } | ||
| 130 | |||
| 131 | // Android doesn't have default options for controllers. We have the input overlay for that. | ||
| 132 | for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) { | ||
| 133 | const std::string default_param; | ||
| 134 | auto& player_buttons = player.buttons[i]; | ||
| 135 | |||
| 136 | player_buttons = ReadStringSetting( | ||
| 137 | std::string(player_prefix).append(Settings::NativeButton::mapping[i]), default_param); | ||
| 138 | if (player_buttons.empty()) { | ||
| 139 | player_buttons = default_param; | ||
| 140 | } | ||
| 141 | } | ||
| 142 | for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) { | ||
| 143 | const std::string default_param; | ||
| 144 | auto& player_analogs = player.analogs[i]; | ||
| 145 | |||
| 146 | player_analogs = ReadStringSetting( | ||
| 147 | std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]), default_param); | ||
| 148 | if (player_analogs.empty()) { | ||
| 149 | player_analogs = default_param; | ||
| 150 | } | ||
| 151 | } | ||
| 152 | for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) { | ||
| 153 | const std::string default_param; | ||
| 154 | auto& player_motions = player.motions[i]; | ||
| 155 | |||
| 156 | player_motions = ReadStringSetting( | ||
| 157 | std::string(player_prefix).append(Settings::NativeMotion::mapping[i]), default_param); | ||
| 158 | if (player_motions.empty()) { | ||
| 159 | player_motions = default_param; | ||
| 160 | } | ||
| 161 | } | ||
| 162 | player.use_system_vibrator = ReadBooleanSetting( | ||
| 163 | std::string(player_prefix).append("use_system_vibrator"), player_index == 0); | ||
| 164 | } | ||
| 165 | |||
| 166 | void AndroidConfig::ReadAndroidControlValues() { | ||
| 167 | BeginGroup(Settings::TranslateCategory(Settings::Category::Controls)); | ||
| 168 | |||
| 169 | Settings::values.players.SetGlobal(!IsCustomConfig()); | ||
| 170 | for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) { | ||
| 171 | ReadAndroidPlayerValues(p); | ||
| 172 | } | ||
| 173 | if (IsCustomConfig()) { | ||
| 174 | EndGroup(); | ||
| 175 | return; | ||
| 176 | } | ||
| 177 | // ReadDebugControlValues(); | ||
| 178 | // ReadHidbusValues(); | ||
| 179 | |||
| 180 | EndGroup(); | ||
| 181 | } | ||
| 182 | |||
| 110 | void AndroidConfig::SaveAndroidValues() { | 183 | void AndroidConfig::SaveAndroidValues() { |
| 111 | if (global) { | 184 | if (global) { |
| 112 | SaveAndroidUIValues(); | 185 | SaveAndroidUIValues(); |
| @@ -114,6 +187,7 @@ void AndroidConfig::SaveAndroidValues() { | |||
| 114 | SaveOverlayValues(); | 187 | SaveOverlayValues(); |
| 115 | } | 188 | } |
| 116 | SaveDriverValues(); | 189 | SaveDriverValues(); |
| 190 | SaveAndroidControlValues(); | ||
| 117 | 191 | ||
| 118 | WriteToIni(); | 192 | WriteToIni(); |
| 119 | } | 193 | } |
| @@ -187,6 +261,52 @@ void AndroidConfig::SaveOverlayValues() { | |||
| 187 | EndGroup(); | 261 | EndGroup(); |
| 188 | } | 262 | } |
| 189 | 263 | ||
| 264 | void AndroidConfig::SaveAndroidPlayerValues(std::size_t player_index) { | ||
| 265 | std::string player_prefix; | ||
| 266 | if (type != ConfigType::InputProfile) { | ||
| 267 | player_prefix = std::string("player_").append(ToString(player_index)).append("_"); | ||
| 268 | } | ||
| 269 | |||
| 270 | const auto& player = Settings::values.players.GetValue()[player_index]; | ||
| 271 | if (IsCustomConfig() && player.profile_name.empty()) { | ||
| 272 | // No custom profile selected | ||
| 273 | return; | ||
| 274 | } | ||
| 275 | |||
| 276 | const std::string default_param; | ||
| 277 | for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) { | ||
| 278 | WriteStringSetting(std::string(player_prefix).append(Settings::NativeButton::mapping[i]), | ||
| 279 | player.buttons[i], std::make_optional(default_param)); | ||
| 280 | } | ||
| 281 | for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) { | ||
| 282 | WriteStringSetting(std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]), | ||
| 283 | player.analogs[i], std::make_optional(default_param)); | ||
| 284 | } | ||
| 285 | for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) { | ||
| 286 | WriteStringSetting(std::string(player_prefix).append(Settings::NativeMotion::mapping[i]), | ||
| 287 | player.motions[i], std::make_optional(default_param)); | ||
| 288 | } | ||
| 289 | WriteBooleanSetting(std::string(player_prefix).append("use_system_vibrator"), | ||
| 290 | player.use_system_vibrator, std::make_optional(player_index == 0)); | ||
| 291 | } | ||
| 292 | |||
| 293 | void AndroidConfig::SaveAndroidControlValues() { | ||
| 294 | BeginGroup(Settings::TranslateCategory(Settings::Category::Controls)); | ||
| 295 | |||
| 296 | Settings::values.players.SetGlobal(!IsCustomConfig()); | ||
| 297 | for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) { | ||
| 298 | SaveAndroidPlayerValues(p); | ||
| 299 | } | ||
| 300 | if (IsCustomConfig()) { | ||
| 301 | EndGroup(); | ||
| 302 | return; | ||
| 303 | } | ||
| 304 | // SaveDebugControlValues(); | ||
| 305 | // SaveHidbusValues(); | ||
| 306 | |||
| 307 | EndGroup(); | ||
| 308 | } | ||
| 309 | |||
| 190 | std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) { | 310 | std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) { |
| 191 | auto& map = Settings::values.linkage.by_category; | 311 | auto& map = Settings::values.linkage.by_category; |
| 192 | if (map.contains(category)) { | 312 | if (map.contains(category)) { |
| @@ -194,3 +314,24 @@ std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings:: | |||
| 194 | } | 314 | } |
| 195 | return AndroidSettings::values.linkage.by_category[category]; | 315 | return AndroidSettings::values.linkage.by_category[category]; |
| 196 | } | 316 | } |
| 317 | |||
| 318 | void AndroidConfig::ReadAndroidControlPlayerValues(std::size_t player_index) { | ||
| 319 | BeginGroup(Settings::TranslateCategory(Settings::Category::Controls)); | ||
| 320 | |||
| 321 | ReadPlayerValues(player_index); | ||
| 322 | ReadAndroidPlayerValues(player_index); | ||
| 323 | |||
| 324 | EndGroup(); | ||
| 325 | } | ||
| 326 | |||
| 327 | void AndroidConfig::SaveAndroidControlPlayerValues(std::size_t player_index) { | ||
| 328 | BeginGroup(Settings::TranslateCategory(Settings::Category::Controls)); | ||
| 329 | |||
| 330 | LOG_DEBUG(Config, "Saving players control configuration values"); | ||
| 331 | SavePlayerValues(player_index); | ||
| 332 | SaveAndroidPlayerValues(player_index); | ||
| 333 | |||
| 334 | EndGroup(); | ||
| 335 | |||
| 336 | WriteToIni(); | ||
| 337 | } | ||
diff --git a/src/android/app/src/main/jni/android_config.h b/src/android/app/src/main/jni/android_config.h index 693e1e3f0..28ef5d0a8 100644 --- a/src/android/app/src/main/jni/android_config.h +++ b/src/android/app/src/main/jni/android_config.h | |||
| @@ -13,7 +13,12 @@ public: | |||
| 13 | void ReloadAllValues() override; | 13 | void ReloadAllValues() override; |
| 14 | void SaveAllValues() override; | 14 | void SaveAllValues() override; |
| 15 | 15 | ||
| 16 | void ReadAndroidControlPlayerValues(std::size_t player_index); | ||
| 17 | void SaveAndroidControlPlayerValues(std::size_t player_index); | ||
| 18 | |||
| 16 | protected: | 19 | protected: |
| 20 | void ReadAndroidPlayerValues(std::size_t player_index); | ||
| 21 | void ReadAndroidControlValues(); | ||
| 17 | void ReadAndroidValues(); | 22 | void ReadAndroidValues(); |
| 18 | void ReadAndroidUIValues(); | 23 | void ReadAndroidUIValues(); |
| 19 | void ReadDriverValues(); | 24 | void ReadDriverValues(); |
| @@ -27,6 +32,8 @@ protected: | |||
| 27 | void ReadUILayoutValues() override {} | 32 | void ReadUILayoutValues() override {} |
| 28 | void ReadMultiplayerValues() override {} | 33 | void ReadMultiplayerValues() override {} |
| 29 | 34 | ||
| 35 | void SaveAndroidPlayerValues(std::size_t player_index); | ||
| 36 | void SaveAndroidControlValues(); | ||
| 30 | void SaveAndroidValues(); | 37 | void SaveAndroidValues(); |
| 31 | void SaveAndroidUIValues(); | 38 | void SaveAndroidUIValues(); |
| 32 | void SaveDriverValues(); | 39 | void SaveDriverValues(); |
diff --git a/src/android/app/src/main/jni/emu_window/emu_window.cpp b/src/android/app/src/main/jni/emu_window/emu_window.cpp index c927cddda..2768a01c9 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.cpp +++ b/src/android/app/src/main/jni/emu_window/emu_window.cpp | |||
| @@ -5,6 +5,7 @@ | |||
| 5 | 5 | ||
| 6 | #include "common/android/id_cache.h" | 6 | #include "common/android/id_cache.h" |
| 7 | #include "common/logging/log.h" | 7 | #include "common/logging/log.h" |
| 8 | #include "input_common/drivers/android.h" | ||
| 8 | #include "input_common/drivers/touch_screen.h" | 9 | #include "input_common/drivers/touch_screen.h" |
| 9 | #include "input_common/drivers/virtual_amiibo.h" | 10 | #include "input_common/drivers/virtual_amiibo.h" |
| 10 | #include "input_common/drivers/virtual_gamepad.h" | 11 | #include "input_common/drivers/virtual_gamepad.h" |
| @@ -22,43 +23,6 @@ void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) { | |||
| 22 | window_info.render_surface = reinterpret_cast<void*>(surface); | 23 | window_info.render_surface = reinterpret_cast<void*>(surface); |
| 23 | } | 24 | } |
| 24 | 25 | ||
| 25 | void EmuWindow_Android::OnTouchPressed(int id, float x, float y) { | ||
| 26 | const auto [touch_x, touch_y] = MapToTouchScreen(x, y); | ||
| 27 | m_input_subsystem->GetTouchScreen()->TouchPressed(touch_x, touch_y, id); | ||
| 28 | } | ||
| 29 | |||
| 30 | void EmuWindow_Android::OnTouchMoved(int id, float x, float y) { | ||
| 31 | const auto [touch_x, touch_y] = MapToTouchScreen(x, y); | ||
| 32 | m_input_subsystem->GetTouchScreen()->TouchMoved(touch_x, touch_y, id); | ||
| 33 | } | ||
| 34 | |||
| 35 | void EmuWindow_Android::OnTouchReleased(int id) { | ||
| 36 | m_input_subsystem->GetTouchScreen()->TouchReleased(id); | ||
| 37 | } | ||
| 38 | |||
| 39 | void EmuWindow_Android::OnGamepadButtonEvent(int player_index, int button_id, bool pressed) { | ||
| 40 | m_input_subsystem->GetVirtualGamepad()->SetButtonState(player_index, button_id, pressed); | ||
| 41 | } | ||
| 42 | |||
| 43 | void EmuWindow_Android::OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y) { | ||
| 44 | m_input_subsystem->GetVirtualGamepad()->SetStickPosition(player_index, stick_id, x, y); | ||
| 45 | } | ||
| 46 | |||
| 47 | void EmuWindow_Android::OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x, | ||
| 48 | float gyro_y, float gyro_z, float accel_x, | ||
| 49 | float accel_y, float accel_z) { | ||
| 50 | m_input_subsystem->GetVirtualGamepad()->SetMotionState( | ||
| 51 | player_index, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z); | ||
| 52 | } | ||
| 53 | |||
| 54 | void EmuWindow_Android::OnReadNfcTag(std::span<u8> data) { | ||
| 55 | m_input_subsystem->GetVirtualAmiibo()->LoadAmiibo(data); | ||
| 56 | } | ||
| 57 | |||
| 58 | void EmuWindow_Android::OnRemoveNfcTag() { | ||
| 59 | m_input_subsystem->GetVirtualAmiibo()->CloseAmiibo(); | ||
| 60 | } | ||
| 61 | |||
| 62 | void EmuWindow_Android::OnFrameDisplayed() { | 26 | void EmuWindow_Android::OnFrameDisplayed() { |
| 63 | if (!m_first_frame) { | 27 | if (!m_first_frame) { |
| 64 | Common::Android::RunJNIOnFiber<void>( | 28 | Common::Android::RunJNIOnFiber<void>( |
| @@ -67,10 +31,9 @@ void EmuWindow_Android::OnFrameDisplayed() { | |||
| 67 | } | 31 | } |
| 68 | } | 32 | } |
| 69 | 33 | ||
| 70 | EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem, | 34 | EmuWindow_Android::EmuWindow_Android(ANativeWindow* surface, |
| 71 | ANativeWindow* surface, | ||
| 72 | std::shared_ptr<Common::DynamicLibrary> driver_library) | 35 | std::shared_ptr<Common::DynamicLibrary> driver_library) |
| 73 | : m_input_subsystem{input_subsystem}, m_driver_library{driver_library} { | 36 | : m_driver_library{driver_library} { |
| 74 | LOG_INFO(Frontend, "initializing"); | 37 | LOG_INFO(Frontend, "initializing"); |
| 75 | 38 | ||
| 76 | if (!surface) { | 39 | if (!surface) { |
| @@ -80,10 +43,4 @@ EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsyste | |||
| 80 | 43 | ||
| 81 | OnSurfaceChanged(surface); | 44 | OnSurfaceChanged(surface); |
| 82 | window_info.type = Core::Frontend::WindowSystemType::Android; | 45 | window_info.type = Core::Frontend::WindowSystemType::Android; |
| 83 | |||
| 84 | m_input_subsystem->Initialize(); | ||
| 85 | } | ||
| 86 | |||
| 87 | EmuWindow_Android::~EmuWindow_Android() { | ||
| 88 | m_input_subsystem->Shutdown(); | ||
| 89 | } | 46 | } |
diff --git a/src/android/app/src/main/jni/emu_window/emu_window.h b/src/android/app/src/main/jni/emu_window/emu_window.h index a34a0e479..34704ae95 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.h +++ b/src/android/app/src/main/jni/emu_window/emu_window.h | |||
| @@ -30,21 +30,12 @@ private: | |||
| 30 | class EmuWindow_Android final : public Core::Frontend::EmuWindow { | 30 | class EmuWindow_Android final : public Core::Frontend::EmuWindow { |
| 31 | 31 | ||
| 32 | public: | 32 | public: |
| 33 | EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem, ANativeWindow* surface, | 33 | EmuWindow_Android(ANativeWindow* surface, |
| 34 | std::shared_ptr<Common::DynamicLibrary> driver_library); | 34 | std::shared_ptr<Common::DynamicLibrary> driver_library); |
| 35 | 35 | ||
| 36 | ~EmuWindow_Android(); | 36 | ~EmuWindow_Android() = default; |
| 37 | 37 | ||
| 38 | void OnSurfaceChanged(ANativeWindow* surface); | 38 | void OnSurfaceChanged(ANativeWindow* surface); |
| 39 | void OnTouchPressed(int id, float x, float y); | ||
| 40 | void OnTouchMoved(int id, float x, float y); | ||
| 41 | void OnTouchReleased(int id); | ||
| 42 | void OnGamepadButtonEvent(int player_index, int button_id, bool pressed); | ||
| 43 | void OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y); | ||
| 44 | void OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x, float gyro_y, | ||
| 45 | float gyro_z, float accel_x, float accel_y, float accel_z); | ||
| 46 | void OnReadNfcTag(std::span<u8> data); | ||
| 47 | void OnRemoveNfcTag(); | ||
| 48 | void OnFrameDisplayed() override; | 39 | void OnFrameDisplayed() override; |
| 49 | 40 | ||
| 50 | std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override { | 41 | std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override { |
| @@ -55,8 +46,6 @@ public: | |||
| 55 | }; | 46 | }; |
| 56 | 47 | ||
| 57 | private: | 48 | private: |
| 58 | InputCommon::InputSubsystem* m_input_subsystem{}; | ||
| 59 | |||
| 60 | float m_window_width{}; | 49 | float m_window_width{}; |
| 61 | float m_window_height{}; | 50 | float m_window_height{}; |
| 62 | 51 | ||
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index a4d8454e8..50cef5d2a 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp | |||
| @@ -88,6 +88,10 @@ FileSys::ManualContentProvider* EmulationSession::GetContentProvider() { | |||
| 88 | return m_manual_provider.get(); | 88 | return m_manual_provider.get(); |
| 89 | } | 89 | } |
| 90 | 90 | ||
| 91 | InputCommon::InputSubsystem& EmulationSession::GetInputSubsystem() { | ||
| 92 | return m_input_subsystem; | ||
| 93 | } | ||
| 94 | |||
| 91 | const EmuWindow_Android& EmulationSession::Window() const { | 95 | const EmuWindow_Android& EmulationSession::Window() const { |
| 92 | return *m_window; | 96 | return *m_window; |
| 93 | } | 97 | } |
| @@ -198,6 +202,8 @@ void EmulationSession::InitializeSystem(bool reload) { | |||
| 198 | Common::Log::Initialize(); | 202 | Common::Log::Initialize(); |
| 199 | Common::Log::SetColorConsoleBackendEnabled(true); | 203 | Common::Log::SetColorConsoleBackendEnabled(true); |
| 200 | Common::Log::Start(); | 204 | Common::Log::Start(); |
| 205 | |||
| 206 | m_input_subsystem.Initialize(); | ||
| 201 | } | 207 | } |
| 202 | 208 | ||
| 203 | // Initialize filesystem. | 209 | // Initialize filesystem. |
| @@ -222,8 +228,7 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string | |||
| 222 | std::scoped_lock lock(m_mutex); | 228 | std::scoped_lock lock(m_mutex); |
| 223 | 229 | ||
| 224 | // Create the render window. | 230 | // Create the render window. |
| 225 | m_window = | 231 | m_window = std::make_unique<EmuWindow_Android>(m_native_window, m_vulkan_library); |
| 226 | std::make_unique<EmuWindow_Android>(&m_input_subsystem, m_native_window, m_vulkan_library); | ||
| 227 | 232 | ||
| 228 | // Initialize system. | 233 | // Initialize system. |
| 229 | jauto android_keyboard = std::make_unique<Common::Android::SoftwareKeyboard::AndroidKeyboard>(); | 234 | jauto android_keyboard = std::make_unique<Common::Android::SoftwareKeyboard::AndroidKeyboard>(); |
| @@ -355,60 +360,6 @@ void EmulationSession::RunEmulation() { | |||
| 355 | m_applet_id = static_cast<int>(Service::AM::AppletId::Application); | 360 | m_applet_id = static_cast<int>(Service::AM::AppletId::Application); |
| 356 | } | 361 | } |
| 357 | 362 | ||
| 358 | bool EmulationSession::IsHandheldOnly() { | ||
| 359 | jconst npad_style_set = m_system.HIDCore().GetSupportedStyleTag(); | ||
| 360 | |||
| 361 | if (npad_style_set.fullkey == 1) { | ||
| 362 | return false; | ||
| 363 | } | ||
| 364 | |||
| 365 | if (npad_style_set.handheld == 0) { | ||
| 366 | return false; | ||
| 367 | } | ||
| 368 | |||
| 369 | return !Settings::IsDockedMode(); | ||
| 370 | } | ||
| 371 | |||
| 372 | void EmulationSession::SetDeviceType([[maybe_unused]] int index, int type) { | ||
| 373 | jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index); | ||
| 374 | controller->SetNpadStyleIndex(static_cast<Core::HID::NpadStyleIndex>(type)); | ||
| 375 | } | ||
| 376 | |||
| 377 | void EmulationSession::OnGamepadConnectEvent([[maybe_unused]] int index) { | ||
| 378 | jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index); | ||
| 379 | |||
| 380 | // Ensure that player1 is configured correctly and handheld disconnected | ||
| 381 | if (controller->GetNpadIdType() == Core::HID::NpadIdType::Player1) { | ||
| 382 | jauto handheld = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld); | ||
| 383 | |||
| 384 | if (controller->GetNpadStyleIndex() == Core::HID::NpadStyleIndex::Handheld) { | ||
| 385 | handheld->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey); | ||
| 386 | controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey); | ||
| 387 | handheld->Disconnect(); | ||
| 388 | } | ||
| 389 | } | ||
| 390 | |||
| 391 | // Ensure that handheld is configured correctly and player 1 disconnected | ||
| 392 | if (controller->GetNpadIdType() == Core::HID::NpadIdType::Handheld) { | ||
| 393 | jauto player1 = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1); | ||
| 394 | |||
| 395 | if (controller->GetNpadStyleIndex() != Core::HID::NpadStyleIndex::Handheld) { | ||
| 396 | player1->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld); | ||
| 397 | controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld); | ||
| 398 | player1->Disconnect(); | ||
| 399 | } | ||
| 400 | } | ||
| 401 | |||
| 402 | if (!controller->IsConnected()) { | ||
| 403 | controller->Connect(); | ||
| 404 | } | ||
| 405 | } | ||
| 406 | |||
| 407 | void EmulationSession::OnGamepadDisconnectEvent([[maybe_unused]] int index) { | ||
| 408 | jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index); | ||
| 409 | controller->Disconnect(); | ||
| 410 | } | ||
| 411 | |||
| 412 | Common::Android::SoftwareKeyboard::AndroidKeyboard* EmulationSession::SoftwareKeyboard() { | 363 | Common::Android::SoftwareKeyboard::AndroidKeyboard* EmulationSession::SoftwareKeyboard() { |
| 413 | return m_software_keyboard; | 364 | return m_software_keyboard; |
| 414 | } | 365 | } |
| @@ -574,14 +525,14 @@ jobjectArray Java_org_yuzu_yuzu_1emu_utils_GpuDriverHelper_getSystemDriverInfo( | |||
| 574 | nullptr, nullptr, file_redirect_dir_, nullptr); | 525 | nullptr, nullptr, file_redirect_dir_, nullptr); |
| 575 | auto driver_library = std::make_shared<Common::DynamicLibrary>(handle); | 526 | auto driver_library = std::make_shared<Common::DynamicLibrary>(handle); |
| 576 | InputCommon::InputSubsystem input_subsystem; | 527 | InputCommon::InputSubsystem input_subsystem; |
| 577 | auto m_window = std::make_unique<EmuWindow_Android>( | 528 | auto window = |
| 578 | &input_subsystem, ANativeWindow_fromSurface(env, j_surf), driver_library); | 529 | std::make_unique<EmuWindow_Android>(ANativeWindow_fromSurface(env, j_surf), driver_library); |
| 579 | 530 | ||
| 580 | Vulkan::vk::InstanceDispatch dld; | 531 | Vulkan::vk::InstanceDispatch dld; |
| 581 | Vulkan::vk::Instance vk_instance = Vulkan::CreateInstance( | 532 | Vulkan::vk::Instance vk_instance = Vulkan::CreateInstance( |
| 582 | *driver_library, dld, VK_API_VERSION_1_1, Core::Frontend::WindowSystemType::Android); | 533 | *driver_library, dld, VK_API_VERSION_1_1, Core::Frontend::WindowSystemType::Android); |
| 583 | 534 | ||
| 584 | auto surface = Vulkan::CreateSurface(vk_instance, m_window->GetWindowInfo()); | 535 | auto surface = Vulkan::CreateSurface(vk_instance, window->GetWindowInfo()); |
| 585 | 536 | ||
| 586 | auto device = Vulkan::CreateDevice(vk_instance, dld, *surface); | 537 | auto device = Vulkan::CreateDevice(vk_instance, dld, *surface); |
| 587 | 538 | ||
| @@ -622,103 +573,6 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused(JNIEnv* env, jclass claz | |||
| 622 | return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused()); | 573 | return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused()); |
| 623 | } | 574 | } |
| 624 | 575 | ||
| 625 | jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly(JNIEnv* env, jclass clazz) { | ||
| 626 | return EmulationSession::GetInstance().IsHandheldOnly(); | ||
| 627 | } | ||
| 628 | |||
| 629 | jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_setDeviceType(JNIEnv* env, jclass clazz, | ||
| 630 | jint j_device, jint j_type) { | ||
| 631 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 632 | EmulationSession::GetInstance().SetDeviceType(j_device, j_type); | ||
| 633 | } | ||
| 634 | return static_cast<jboolean>(true); | ||
| 635 | } | ||
| 636 | |||
| 637 | jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadConnectEvent(JNIEnv* env, jclass clazz, | ||
| 638 | jint j_device) { | ||
| 639 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 640 | EmulationSession::GetInstance().OnGamepadConnectEvent(j_device); | ||
| 641 | } | ||
| 642 | return static_cast<jboolean>(true); | ||
| 643 | } | ||
| 644 | |||
| 645 | jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadDisconnectEvent(JNIEnv* env, jclass clazz, | ||
| 646 | jint j_device) { | ||
| 647 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 648 | EmulationSession::GetInstance().OnGamepadDisconnectEvent(j_device); | ||
| 649 | } | ||
| 650 | return static_cast<jboolean>(true); | ||
| 651 | } | ||
| 652 | jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadButtonEvent(JNIEnv* env, jclass clazz, | ||
| 653 | jint j_device, jint j_button, | ||
| 654 | jint action) { | ||
| 655 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 656 | // Ensure gamepad is connected | ||
| 657 | EmulationSession::GetInstance().OnGamepadConnectEvent(j_device); | ||
| 658 | EmulationSession::GetInstance().Window().OnGamepadButtonEvent(j_device, j_button, | ||
| 659 | action != 0); | ||
| 660 | } | ||
| 661 | return static_cast<jboolean>(true); | ||
| 662 | } | ||
| 663 | |||
| 664 | jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadJoystickEvent(JNIEnv* env, jclass clazz, | ||
| 665 | jint j_device, jint stick_id, | ||
| 666 | jfloat x, jfloat y) { | ||
| 667 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 668 | EmulationSession::GetInstance().Window().OnGamepadJoystickEvent(j_device, stick_id, x, y); | ||
| 669 | } | ||
| 670 | return static_cast<jboolean>(true); | ||
| 671 | } | ||
| 672 | |||
| 673 | jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadMotionEvent( | ||
| 674 | JNIEnv* env, jclass clazz, jint j_device, jlong delta_timestamp, jfloat gyro_x, jfloat gyro_y, | ||
| 675 | jfloat gyro_z, jfloat accel_x, jfloat accel_y, jfloat accel_z) { | ||
| 676 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 677 | EmulationSession::GetInstance().Window().OnGamepadMotionEvent( | ||
| 678 | j_device, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z); | ||
| 679 | } | ||
| 680 | return static_cast<jboolean>(true); | ||
| 681 | } | ||
| 682 | |||
| 683 | jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onReadNfcTag(JNIEnv* env, jclass clazz, | ||
| 684 | jbyteArray j_data) { | ||
| 685 | jboolean isCopy{false}; | ||
| 686 | std::span<u8> data(reinterpret_cast<u8*>(env->GetByteArrayElements(j_data, &isCopy)), | ||
| 687 | static_cast<size_t>(env->GetArrayLength(j_data))); | ||
| 688 | |||
| 689 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 690 | EmulationSession::GetInstance().Window().OnReadNfcTag(data); | ||
| 691 | } | ||
| 692 | return static_cast<jboolean>(true); | ||
| 693 | } | ||
| 694 | |||
| 695 | jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onRemoveNfcTag(JNIEnv* env, jclass clazz) { | ||
| 696 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 697 | EmulationSession::GetInstance().Window().OnRemoveNfcTag(); | ||
| 698 | } | ||
| 699 | return static_cast<jboolean>(true); | ||
| 700 | } | ||
| 701 | |||
| 702 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchPressed(JNIEnv* env, jclass clazz, jint id, | ||
| 703 | jfloat x, jfloat y) { | ||
| 704 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 705 | EmulationSession::GetInstance().Window().OnTouchPressed(id, x, y); | ||
| 706 | } | ||
| 707 | } | ||
| 708 | |||
| 709 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, jclass clazz, jint id, | ||
| 710 | jfloat x, jfloat y) { | ||
| 711 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 712 | EmulationSession::GetInstance().Window().OnTouchMoved(id, x, y); | ||
| 713 | } | ||
| 714 | } | ||
| 715 | |||
| 716 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchReleased(JNIEnv* env, jclass clazz, jint id) { | ||
| 717 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 718 | EmulationSession::GetInstance().Window().OnTouchReleased(id); | ||
| 719 | } | ||
| 720 | } | ||
| 721 | |||
| 722 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass clazz, | 576 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass clazz, |
| 723 | jboolean reload) { | 577 | jboolean reload) { |
| 724 | // Initialize the emulated system. | 578 | // Initialize the emulated system. |
| @@ -759,6 +613,7 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGpuDriver(JNIEnv* env, jobject | |||
| 759 | 613 | ||
| 760 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) { | 614 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) { |
| 761 | EmulationSession::GetInstance().System().ApplySettings(); | 615 | EmulationSession::GetInstance().System().ApplySettings(); |
| 616 | EmulationSession::GetInstance().System().HIDCore().ReloadInputDevices(); | ||
| 762 | } | 617 | } |
| 763 | 618 | ||
| 764 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_logSettings(JNIEnv* env, jobject jobj) { | 619 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_logSettings(JNIEnv* env, jobject jobj) { |
diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h index 47936e305..6a4551ada 100644 --- a/src/android/app/src/main/jni/native.h +++ b/src/android/app/src/main/jni/native.h | |||
| @@ -23,6 +23,7 @@ public: | |||
| 23 | const Core::System& System() const; | 23 | const Core::System& System() const; |
| 24 | Core::System& System(); | 24 | Core::System& System(); |
| 25 | FileSys::ManualContentProvider* GetContentProvider(); | 25 | FileSys::ManualContentProvider* GetContentProvider(); |
| 26 | InputCommon::InputSubsystem& GetInputSubsystem(); | ||
| 26 | 27 | ||
| 27 | const EmuWindow_Android& Window() const; | 28 | const EmuWindow_Android& Window() const; |
| 28 | EmuWindow_Android& Window(); | 29 | EmuWindow_Android& Window(); |
| @@ -50,10 +51,6 @@ public: | |||
| 50 | const std::size_t program_index, | 51 | const std::size_t program_index, |
| 51 | const bool frontend_initiated); | 52 | const bool frontend_initiated); |
| 52 | 53 | ||
| 53 | bool IsHandheldOnly(); | ||
| 54 | void SetDeviceType([[maybe_unused]] int index, int type); | ||
| 55 | void OnGamepadConnectEvent([[maybe_unused]] int index); | ||
| 56 | void OnGamepadDisconnectEvent([[maybe_unused]] int index); | ||
| 57 | Common::Android::SoftwareKeyboard::AndroidKeyboard* SoftwareKeyboard(); | 54 | Common::Android::SoftwareKeyboard::AndroidKeyboard* SoftwareKeyboard(); |
| 58 | 55 | ||
| 59 | static void OnEmulationStarted(); | 56 | static void OnEmulationStarted(); |
diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp index 8ae10fbc7..0b26280c6 100644 --- a/src/android/app/src/main/jni/native_config.cpp +++ b/src/android/app/src/main/jni/native_config.cpp | |||
| @@ -3,7 +3,6 @@ | |||
| 3 | 3 | ||
| 4 | #include <string> | 4 | #include <string> |
| 5 | 5 | ||
| 6 | #include <common/fs/fs_util.h> | ||
| 7 | #include <jni.h> | 6 | #include <jni.h> |
| 8 | 7 | ||
| 9 | #include "android_config.h" | 8 | #include "android_config.h" |
| @@ -425,4 +424,120 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setOverlayControlData( | |||
| 425 | } | 424 | } |
| 426 | } | 425 | } |
| 427 | 426 | ||
| 427 | jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInputSettings(JNIEnv* env, jobject obj, | ||
| 428 | jboolean j_global) { | ||
| 429 | Settings::values.players.SetGlobal(static_cast<bool>(j_global)); | ||
| 430 | auto& players = Settings::values.players.GetValue(); | ||
| 431 | jobjectArray j_input_settings = | ||
| 432 | env->NewObjectArray(players.size(), Common::Android::GetPlayerInputClass(), nullptr); | ||
| 433 | for (size_t i = 0; i < players.size(); ++i) { | ||
| 434 | auto j_connected = static_cast<jboolean>(players[i].connected); | ||
| 435 | |||
| 436 | jobjectArray j_buttons = env->NewObjectArray( | ||
| 437 | players[i].buttons.size(), Common::Android::GetStringClass(), env->NewStringUTF("")); | ||
| 438 | for (size_t j = 0; j < players[i].buttons.size(); ++j) { | ||
| 439 | env->SetObjectArrayElement(j_buttons, j, | ||
| 440 | Common::Android::ToJString(env, players[i].buttons[j])); | ||
| 441 | } | ||
| 442 | jobjectArray j_analogs = env->NewObjectArray( | ||
| 443 | players[i].analogs.size(), Common::Android::GetStringClass(), env->NewStringUTF("")); | ||
| 444 | for (size_t j = 0; j < players[i].analogs.size(); ++j) { | ||
| 445 | env->SetObjectArrayElement(j_analogs, j, | ||
| 446 | Common::Android::ToJString(env, players[i].analogs[j])); | ||
| 447 | } | ||
| 448 | jobjectArray j_motions = env->NewObjectArray( | ||
| 449 | players[i].motions.size(), Common::Android::GetStringClass(), env->NewStringUTF("")); | ||
| 450 | for (size_t j = 0; j < players[i].motions.size(); ++j) { | ||
| 451 | env->SetObjectArrayElement(j_motions, j, | ||
| 452 | Common::Android::ToJString(env, players[i].motions[j])); | ||
| 453 | } | ||
| 454 | |||
| 455 | auto j_vibration_enabled = static_cast<jboolean>(players[i].vibration_enabled); | ||
| 456 | auto j_vibration_strength = static_cast<jint>(players[i].vibration_strength); | ||
| 457 | |||
| 458 | auto j_body_color_left = static_cast<jlong>(players[i].body_color_left); | ||
| 459 | auto j_body_color_right = static_cast<jlong>(players[i].body_color_right); | ||
| 460 | auto j_button_color_left = static_cast<jlong>(players[i].button_color_left); | ||
| 461 | auto j_button_color_right = static_cast<jlong>(players[i].button_color_right); | ||
| 462 | |||
| 463 | auto j_profile_name = Common::Android::ToJString(env, players[i].profile_name); | ||
| 464 | |||
| 465 | auto j_use_system_vibrator = players[i].use_system_vibrator; | ||
| 466 | |||
| 467 | jobject playerInput = env->NewObject( | ||
| 468 | Common::Android::GetPlayerInputClass(), Common::Android::GetPlayerInputConstructor(), | ||
| 469 | j_connected, j_buttons, j_analogs, j_motions, j_vibration_enabled, j_vibration_strength, | ||
| 470 | j_body_color_left, j_body_color_right, j_button_color_left, j_button_color_right, | ||
| 471 | j_profile_name, j_use_system_vibrator); | ||
| 472 | env->SetObjectArrayElement(j_input_settings, i, playerInput); | ||
| 473 | } | ||
| 474 | return j_input_settings; | ||
| 475 | } | ||
| 476 | |||
| 477 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInputSettings(JNIEnv* env, jobject obj, | ||
| 478 | jobjectArray j_value, | ||
| 479 | jboolean j_global) { | ||
| 480 | auto& players = Settings::values.players.GetValue(static_cast<bool>(j_global)); | ||
| 481 | int playersSize = env->GetArrayLength(j_value); | ||
| 482 | for (int i = 0; i < playersSize; ++i) { | ||
| 483 | jobject jplayer = env->GetObjectArrayElement(j_value, i); | ||
| 484 | |||
| 485 | players[i].connected = static_cast<bool>( | ||
| 486 | env->GetBooleanField(jplayer, Common::Android::GetPlayerInputConnectedField())); | ||
| 487 | |||
| 488 | auto j_buttons_array = static_cast<jobjectArray>( | ||
| 489 | env->GetObjectField(jplayer, Common::Android::GetPlayerInputButtonsField())); | ||
| 490 | int buttons_size = env->GetArrayLength(j_buttons_array); | ||
| 491 | for (int j = 0; j < buttons_size; ++j) { | ||
| 492 | auto button = static_cast<jstring>(env->GetObjectArrayElement(j_buttons_array, j)); | ||
| 493 | players[i].buttons[j] = Common::Android::GetJString(env, button); | ||
| 494 | } | ||
| 495 | auto j_analogs_array = static_cast<jobjectArray>( | ||
| 496 | env->GetObjectField(jplayer, Common::Android::GetPlayerInputAnalogsField())); | ||
| 497 | int analogs_size = env->GetArrayLength(j_analogs_array); | ||
| 498 | for (int j = 0; j < analogs_size; ++j) { | ||
| 499 | auto analog = static_cast<jstring>(env->GetObjectArrayElement(j_analogs_array, j)); | ||
| 500 | players[i].analogs[j] = Common::Android::GetJString(env, analog); | ||
| 501 | } | ||
| 502 | auto j_motions_array = static_cast<jobjectArray>( | ||
| 503 | env->GetObjectField(jplayer, Common::Android::GetPlayerInputMotionsField())); | ||
| 504 | int motions_size = env->GetArrayLength(j_motions_array); | ||
| 505 | for (int j = 0; j < motions_size; ++j) { | ||
| 506 | auto motion = static_cast<jstring>(env->GetObjectArrayElement(j_motions_array, j)); | ||
| 507 | players[i].motions[j] = Common::Android::GetJString(env, motion); | ||
| 508 | } | ||
| 509 | |||
| 510 | players[i].vibration_enabled = static_cast<bool>( | ||
| 511 | env->GetBooleanField(jplayer, Common::Android::GetPlayerInputVibrationEnabledField())); | ||
| 512 | players[i].vibration_strength = static_cast<int>( | ||
| 513 | env->GetIntField(jplayer, Common::Android::GetPlayerInputVibrationStrengthField())); | ||
| 514 | |||
| 515 | players[i].body_color_left = static_cast<u32>( | ||
| 516 | env->GetLongField(jplayer, Common::Android::GetPlayerInputBodyColorLeftField())); | ||
| 517 | players[i].body_color_right = static_cast<u32>( | ||
| 518 | env->GetLongField(jplayer, Common::Android::GetPlayerInputBodyColorRightField())); | ||
| 519 | players[i].button_color_left = static_cast<u32>( | ||
| 520 | env->GetLongField(jplayer, Common::Android::GetPlayerInputButtonColorLeftField())); | ||
| 521 | players[i].button_color_right = static_cast<u32>( | ||
| 522 | env->GetLongField(jplayer, Common::Android::GetPlayerInputButtonColorRightField())); | ||
| 523 | |||
| 524 | auto profileName = static_cast<jstring>( | ||
| 525 | env->GetObjectField(jplayer, Common::Android::GetPlayerInputProfileNameField())); | ||
| 526 | players[i].profile_name = Common::Android::GetJString(env, profileName); | ||
| 527 | |||
| 528 | players[i].use_system_vibrator = | ||
| 529 | env->GetBooleanField(jplayer, Common::Android::GetPlayerInputUseSystemVibratorField()); | ||
| 530 | } | ||
| 531 | } | ||
| 532 | |||
| 533 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveControlPlayerValues(JNIEnv* env, jobject obj) { | ||
| 534 | Settings::values.players.SetGlobal(false); | ||
| 535 | |||
| 536 | // Clear all controls from the config in case the user reverted back to globals | ||
| 537 | per_game_config->ClearControlPlayerValues(); | ||
| 538 | for (size_t index = 0; index < Settings::values.players.GetValue().size(); ++index) { | ||
| 539 | per_game_config->SaveAndroidControlPlayerValues(index); | ||
| 540 | } | ||
| 541 | } | ||
| 542 | |||
| 428 | } // extern "C" | 543 | } // extern "C" |
diff --git a/src/android/app/src/main/jni/native_input.cpp b/src/android/app/src/main/jni/native_input.cpp new file mode 100644 index 000000000..ddf2f297b --- /dev/null +++ b/src/android/app/src/main/jni/native_input.cpp | |||
| @@ -0,0 +1,631 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | #include <common/fs/fs.h> | ||
| 5 | #include <common/fs/path_util.h> | ||
| 6 | #include <common/settings.h> | ||
| 7 | #include <hid_core/hid_types.h> | ||
| 8 | #include <jni.h> | ||
| 9 | |||
| 10 | #include "android_config.h" | ||
| 11 | #include "common/android/android_common.h" | ||
| 12 | #include "common/android/id_cache.h" | ||
| 13 | #include "hid_core/frontend/emulated_controller.h" | ||
| 14 | #include "hid_core/hid_core.h" | ||
| 15 | #include "input_common/drivers/android.h" | ||
| 16 | #include "input_common/drivers/touch_screen.h" | ||
| 17 | #include "input_common/drivers/virtual_amiibo.h" | ||
| 18 | #include "input_common/drivers/virtual_gamepad.h" | ||
| 19 | #include "native.h" | ||
| 20 | |||
| 21 | std::unordered_map<std::string, std::unique_ptr<AndroidConfig>> map_profiles; | ||
| 22 | |||
| 23 | bool IsHandheldOnly() { | ||
| 24 | const auto npad_style_set = | ||
| 25 | EmulationSession::GetInstance().System().HIDCore().GetSupportedStyleTag(); | ||
| 26 | |||
| 27 | if (npad_style_set.fullkey == 1) { | ||
| 28 | return false; | ||
| 29 | } | ||
| 30 | |||
| 31 | if (npad_style_set.handheld == 0) { | ||
| 32 | return false; | ||
| 33 | } | ||
| 34 | |||
| 35 | return !Settings::IsDockedMode(); | ||
| 36 | } | ||
| 37 | |||
| 38 | std::filesystem::path GetNameWithoutExtension(std::filesystem::path filename) { | ||
| 39 | return filename.replace_extension(); | ||
| 40 | } | ||
| 41 | |||
| 42 | bool IsProfileNameValid(std::string_view profile_name) { | ||
| 43 | return profile_name.find_first_of("<>:;\"/\\|,.!?*") == std::string::npos; | ||
| 44 | } | ||
| 45 | |||
| 46 | bool ProfileExistsInFilesystem(std::string_view profile_name) { | ||
| 47 | return Common::FS::Exists(Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "input" / | ||
| 48 | fmt::format("{}.ini", profile_name)); | ||
| 49 | } | ||
| 50 | |||
| 51 | bool ProfileExistsInMap(const std::string& profile_name) { | ||
| 52 | return map_profiles.find(profile_name) != map_profiles.end(); | ||
| 53 | } | ||
| 54 | |||
| 55 | bool SaveProfile(const std::string& profile_name, std::size_t player_index) { | ||
| 56 | if (!ProfileExistsInMap(profile_name)) { | ||
| 57 | return false; | ||
| 58 | } | ||
| 59 | |||
| 60 | Settings::values.players.GetValue()[player_index].profile_name = profile_name; | ||
| 61 | map_profiles[profile_name]->SaveAndroidControlPlayerValues(player_index); | ||
| 62 | return true; | ||
| 63 | } | ||
| 64 | |||
| 65 | bool LoadProfile(std::string& profile_name, std::size_t player_index) { | ||
| 66 | if (!ProfileExistsInMap(profile_name)) { | ||
| 67 | return false; | ||
| 68 | } | ||
| 69 | |||
| 70 | if (!ProfileExistsInFilesystem(profile_name)) { | ||
| 71 | map_profiles.erase(profile_name); | ||
| 72 | return false; | ||
| 73 | } | ||
| 74 | |||
| 75 | LOG_INFO(Config, "Loading input profile `{}`", profile_name); | ||
| 76 | |||
| 77 | Settings::values.players.GetValue()[player_index].profile_name = profile_name; | ||
| 78 | map_profiles[profile_name]->ReadAndroidControlPlayerValues(player_index); | ||
| 79 | return true; | ||
| 80 | } | ||
| 81 | |||
| 82 | void ApplyControllerConfig(size_t player_index, | ||
| 83 | const std::function<void(Core::HID::EmulatedController*)>& apply) { | ||
| 84 | auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); | ||
| 85 | if (player_index == 0) { | ||
| 86 | auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld); | ||
| 87 | auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1); | ||
| 88 | handheld->EnableConfiguration(); | ||
| 89 | player_one->EnableConfiguration(); | ||
| 90 | apply(handheld); | ||
| 91 | apply(player_one); | ||
| 92 | handheld->DisableConfiguration(); | ||
| 93 | player_one->DisableConfiguration(); | ||
| 94 | handheld->SaveCurrentConfig(); | ||
| 95 | player_one->SaveCurrentConfig(); | ||
| 96 | } else { | ||
| 97 | auto* controller = hid_core.GetEmulatedControllerByIndex(player_index); | ||
| 98 | controller->EnableConfiguration(); | ||
| 99 | apply(controller); | ||
| 100 | controller->DisableConfiguration(); | ||
| 101 | controller->SaveCurrentConfig(); | ||
| 102 | } | ||
| 103 | } | ||
| 104 | |||
| 105 | void ConnectController(size_t player_index, bool connected) { | ||
| 106 | auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); | ||
| 107 | if (player_index == 0) { | ||
| 108 | auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld); | ||
| 109 | auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1); | ||
| 110 | handheld->EnableConfiguration(); | ||
| 111 | player_one->EnableConfiguration(); | ||
| 112 | if (player_one->GetNpadStyleIndex(true) == Core::HID::NpadStyleIndex::Handheld) { | ||
| 113 | if (connected) { | ||
| 114 | handheld->Connect(); | ||
| 115 | } else { | ||
| 116 | handheld->Disconnect(); | ||
| 117 | } | ||
| 118 | player_one->Disconnect(); | ||
| 119 | } else { | ||
| 120 | if (connected) { | ||
| 121 | player_one->Connect(); | ||
| 122 | } else { | ||
| 123 | player_one->Disconnect(); | ||
| 124 | } | ||
| 125 | handheld->Disconnect(); | ||
| 126 | } | ||
| 127 | handheld->DisableConfiguration(); | ||
| 128 | player_one->DisableConfiguration(); | ||
| 129 | handheld->SaveCurrentConfig(); | ||
| 130 | player_one->SaveCurrentConfig(); | ||
| 131 | } else { | ||
| 132 | auto* controller = hid_core.GetEmulatedControllerByIndex(player_index); | ||
| 133 | controller->EnableConfiguration(); | ||
| 134 | if (connected) { | ||
| 135 | controller->Connect(); | ||
| 136 | } else { | ||
| 137 | controller->Disconnect(); | ||
| 138 | } | ||
| 139 | controller->DisableConfiguration(); | ||
| 140 | controller->SaveCurrentConfig(); | ||
| 141 | } | ||
| 142 | } | ||
| 143 | |||
| 144 | extern "C" { | ||
| 145 | |||
| 146 | jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isHandheldOnly(JNIEnv* env, | ||
| 147 | jobject j_obj) { | ||
| 148 | return IsHandheldOnly(); | ||
| 149 | } | ||
| 150 | |||
| 151 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadButtonEvent( | ||
| 152 | JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jint j_button_id, jint j_action) { | ||
| 153 | EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetButtonState( | ||
| 154 | Common::Android::GetJString(env, j_guid), j_port, j_button_id, j_action != 0); | ||
| 155 | } | ||
| 156 | |||
| 157 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadAxisEvent( | ||
| 158 | JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jint j_stick_id, jfloat j_value) { | ||
| 159 | EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetAxisPosition( | ||
| 160 | Common::Android::GetJString(env, j_guid), j_port, j_stick_id, j_value); | ||
| 161 | } | ||
| 162 | |||
| 163 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadMotionEvent( | ||
| 164 | JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jlong j_delta_timestamp, | ||
| 165 | jfloat j_x_gyro, jfloat j_y_gyro, jfloat j_z_gyro, jfloat j_x_accel, jfloat j_y_accel, | ||
| 166 | jfloat j_z_accel) { | ||
| 167 | EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetMotionState( | ||
| 168 | Common::Android::GetJString(env, j_guid), j_port, j_delta_timestamp, j_x_gyro, j_y_gyro, | ||
| 169 | j_z_gyro, j_x_accel, j_y_accel, j_z_accel); | ||
| 170 | } | ||
| 171 | |||
| 172 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onReadNfcTag(JNIEnv* env, jobject j_obj, | ||
| 173 | jbyteArray j_data) { | ||
| 174 | jboolean isCopy{false}; | ||
| 175 | std::span<u8> data(reinterpret_cast<u8*>(env->GetByteArrayElements(j_data, &isCopy)), | ||
| 176 | static_cast<size_t>(env->GetArrayLength(j_data))); | ||
| 177 | |||
| 178 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 179 | EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo()->LoadAmiibo(data); | ||
| 180 | } | ||
| 181 | } | ||
| 182 | |||
| 183 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onRemoveNfcTag(JNIEnv* env, jobject j_obj) { | ||
| 184 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 185 | EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo()->CloseAmiibo(); | ||
| 186 | } | ||
| 187 | } | ||
| 188 | |||
| 189 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchPressed(JNIEnv* env, jobject j_obj, | ||
| 190 | jint j_id, jfloat j_x_axis, | ||
| 191 | jfloat j_y_axis) { | ||
| 192 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 193 | EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchPressed( | ||
| 194 | j_id, j_x_axis, j_y_axis); | ||
| 195 | } | ||
| 196 | } | ||
| 197 | |||
| 198 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchMoved(JNIEnv* env, jobject j_obj, | ||
| 199 | jint j_id, jfloat j_x_axis, | ||
| 200 | jfloat j_y_axis) { | ||
| 201 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 202 | EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchMoved( | ||
| 203 | j_id, j_x_axis, j_y_axis); | ||
| 204 | } | ||
| 205 | } | ||
| 206 | |||
| 207 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchReleased(JNIEnv* env, jobject j_obj, | ||
| 208 | jint j_id) { | ||
| 209 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 210 | EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchReleased(j_id); | ||
| 211 | } | ||
| 212 | } | ||
| 213 | |||
| 214 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onOverlayButtonEventImpl( | ||
| 215 | JNIEnv* env, jobject j_obj, jint j_port, jint j_button_id, jint j_action) { | ||
| 216 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 217 | EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetButtonState( | ||
| 218 | j_port, j_button_id, j_action == 1); | ||
| 219 | } | ||
| 220 | } | ||
| 221 | |||
| 222 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onOverlayJoystickEventImpl( | ||
| 223 | JNIEnv* env, jobject j_obj, jint j_port, jint j_stick_id, jfloat j_x_axis, jfloat j_y_axis) { | ||
| 224 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 225 | EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetStickPosition( | ||
| 226 | j_port, j_stick_id, j_x_axis, j_y_axis); | ||
| 227 | } | ||
| 228 | } | ||
| 229 | |||
| 230 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onDeviceMotionEvent( | ||
| 231 | JNIEnv* env, jobject j_obj, jint j_port, jlong j_delta_timestamp, jfloat j_x_gyro, | ||
| 232 | jfloat j_y_gyro, jfloat j_z_gyro, jfloat j_x_accel, jfloat j_y_accel, jfloat j_z_accel) { | ||
| 233 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 234 | EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetMotionState( | ||
| 235 | j_port, j_delta_timestamp, j_x_gyro, j_y_gyro, j_z_gyro, j_x_accel, j_y_accel, | ||
| 236 | j_z_accel); | ||
| 237 | } | ||
| 238 | } | ||
| 239 | |||
| 240 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_reloadInputDevices(JNIEnv* env, | ||
| 241 | jobject j_obj) { | ||
| 242 | EmulationSession::GetInstance().System().HIDCore().ReloadInputDevices(); | ||
| 243 | } | ||
| 244 | |||
| 245 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_registerController(JNIEnv* env, | ||
| 246 | jobject j_obj, | ||
| 247 | jobject j_device) { | ||
| 248 | EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->RegisterController(j_device); | ||
| 249 | } | ||
| 250 | |||
| 251 | jobjectArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getInputDevices(JNIEnv* env, | ||
| 252 | jobject j_obj) { | ||
| 253 | auto devices = EmulationSession::GetInstance().GetInputSubsystem().GetInputDevices(); | ||
| 254 | jobjectArray jdevices = env->NewObjectArray(devices.size(), Common::Android::GetStringClass(), | ||
| 255 | Common::Android::ToJString(env, "")); | ||
| 256 | for (size_t i = 0; i < devices.size(); ++i) { | ||
| 257 | env->SetObjectArrayElement(jdevices, i, | ||
| 258 | Common::Android::ToJString(env, devices[i].Serialize())); | ||
| 259 | } | ||
| 260 | return jdevices; | ||
| 261 | } | ||
| 262 | |||
| 263 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadInputProfiles(JNIEnv* env, | ||
| 264 | jobject j_obj) { | ||
| 265 | map_profiles.clear(); | ||
| 266 | const auto input_profile_loc = | ||
| 267 | Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "input"; | ||
| 268 | |||
| 269 | if (Common::FS::IsDir(input_profile_loc)) { | ||
| 270 | Common::FS::IterateDirEntries( | ||
| 271 | input_profile_loc, | ||
| 272 | [&](const std::filesystem::path& full_path) { | ||
| 273 | const auto filename = full_path.filename(); | ||
| 274 | const auto name_without_ext = | ||
| 275 | Common::FS::PathToUTF8String(GetNameWithoutExtension(filename)); | ||
| 276 | |||
| 277 | if (filename.extension() == ".ini" && IsProfileNameValid(name_without_ext)) { | ||
| 278 | map_profiles.insert_or_assign( | ||
| 279 | name_without_ext, std::make_unique<AndroidConfig>( | ||
| 280 | name_without_ext, Config::ConfigType::InputProfile)); | ||
| 281 | } | ||
| 282 | |||
| 283 | return true; | ||
| 284 | }, | ||
| 285 | Common::FS::DirEntryFilter::File); | ||
| 286 | } | ||
| 287 | } | ||
| 288 | |||
| 289 | jobjectArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getInputProfileNames( | ||
| 290 | JNIEnv* env, jobject j_obj) { | ||
| 291 | std::vector<std::string> profile_names; | ||
| 292 | profile_names.reserve(map_profiles.size()); | ||
| 293 | |||
| 294 | auto it = map_profiles.cbegin(); | ||
| 295 | while (it != map_profiles.cend()) { | ||
| 296 | const auto& [profile_name, config] = *it; | ||
| 297 | if (!ProfileExistsInFilesystem(profile_name)) { | ||
| 298 | it = map_profiles.erase(it); | ||
| 299 | continue; | ||
| 300 | } | ||
| 301 | |||
| 302 | profile_names.push_back(profile_name); | ||
| 303 | ++it; | ||
| 304 | } | ||
| 305 | |||
| 306 | std::stable_sort(profile_names.begin(), profile_names.end()); | ||
| 307 | |||
| 308 | jobjectArray j_profile_names = | ||
| 309 | env->NewObjectArray(profile_names.size(), Common::Android::GetStringClass(), | ||
| 310 | Common::Android::ToJString(env, "")); | ||
| 311 | for (size_t i = 0; i < profile_names.size(); ++i) { | ||
| 312 | env->SetObjectArrayElement(j_profile_names, i, | ||
| 313 | Common::Android::ToJString(env, profile_names[i])); | ||
| 314 | } | ||
| 315 | |||
| 316 | return j_profile_names; | ||
| 317 | } | ||
| 318 | |||
| 319 | jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isProfileNameValid(JNIEnv* env, | ||
| 320 | jobject j_obj, | ||
| 321 | jstring j_name) { | ||
| 322 | return Common::Android::GetJString(env, j_name).find_first_of("<>:;\"/\\|,.!?*") == | ||
| 323 | std::string::npos; | ||
| 324 | } | ||
| 325 | |||
| 326 | jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_createProfile(JNIEnv* env, | ||
| 327 | jobject j_obj, | ||
| 328 | jstring j_name, | ||
| 329 | jint j_player_index) { | ||
| 330 | auto profile_name = Common::Android::GetJString(env, j_name); | ||
| 331 | if (ProfileExistsInMap(profile_name)) { | ||
| 332 | return false; | ||
| 333 | } | ||
| 334 | |||
| 335 | map_profiles.insert_or_assign( | ||
| 336 | profile_name, | ||
| 337 | std::make_unique<AndroidConfig>(profile_name, Config::ConfigType::InputProfile)); | ||
| 338 | |||
| 339 | return SaveProfile(profile_name, j_player_index); | ||
| 340 | } | ||
| 341 | |||
| 342 | jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_deleteProfile(JNIEnv* env, | ||
| 343 | jobject j_obj, | ||
| 344 | jstring j_name, | ||
| 345 | jint j_player_index) { | ||
| 346 | auto profile_name = Common::Android::GetJString(env, j_name); | ||
| 347 | if (!ProfileExistsInMap(profile_name)) { | ||
| 348 | return false; | ||
| 349 | } | ||
| 350 | |||
| 351 | if (!ProfileExistsInFilesystem(profile_name) || | ||
| 352 | Common::FS::RemoveFile(map_profiles[profile_name]->GetConfigFilePath())) { | ||
| 353 | map_profiles.erase(profile_name); | ||
| 354 | } | ||
| 355 | |||
| 356 | Settings::values.players.GetValue()[j_player_index].profile_name = ""; | ||
| 357 | return !ProfileExistsInMap(profile_name) && !ProfileExistsInFilesystem(profile_name); | ||
| 358 | } | ||
| 359 | |||
| 360 | jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadProfile(JNIEnv* env, jobject j_obj, | ||
| 361 | jstring j_name, | ||
| 362 | jint j_player_index) { | ||
| 363 | auto profile_name = Common::Android::GetJString(env, j_name); | ||
| 364 | return LoadProfile(profile_name, j_player_index); | ||
| 365 | } | ||
| 366 | |||
| 367 | jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_saveProfile(JNIEnv* env, jobject j_obj, | ||
| 368 | jstring j_name, | ||
| 369 | jint j_player_index) { | ||
| 370 | auto profile_name = Common::Android::GetJString(env, j_name); | ||
| 371 | return SaveProfile(profile_name, j_player_index); | ||
| 372 | } | ||
| 373 | |||
| 374 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadPerGameConfiguration( | ||
| 375 | JNIEnv* env, jobject j_obj, jint j_player_index, jint j_selected_index, | ||
| 376 | jstring j_selected_profile_name) { | ||
| 377 | static constexpr size_t HANDHELD_INDEX = 8; | ||
| 378 | |||
| 379 | auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); | ||
| 380 | Settings::values.players.SetGlobal(false); | ||
| 381 | |||
| 382 | auto profile_name = Common::Android::GetJString(env, j_selected_profile_name); | ||
| 383 | auto* emulated_controller = hid_core.GetEmulatedControllerByIndex(j_player_index); | ||
| 384 | |||
| 385 | if (j_selected_index == 0) { | ||
| 386 | Settings::values.players.GetValue()[j_player_index].profile_name = ""; | ||
| 387 | if (j_player_index == 0) { | ||
| 388 | Settings::values.players.GetValue()[HANDHELD_INDEX] = {}; | ||
| 389 | } | ||
| 390 | Settings::values.players.SetGlobal(true); | ||
| 391 | emulated_controller->ReloadFromSettings(); | ||
| 392 | return; | ||
| 393 | } | ||
| 394 | if (profile_name.empty()) { | ||
| 395 | return; | ||
| 396 | } | ||
| 397 | auto& player = Settings::values.players.GetValue()[j_player_index]; | ||
| 398 | auto& global_player = Settings::values.players.GetValue(true)[j_player_index]; | ||
| 399 | player.profile_name = profile_name; | ||
| 400 | global_player.profile_name = profile_name; | ||
| 401 | // Read from the profile into the custom player settings | ||
| 402 | LoadProfile(profile_name, j_player_index); | ||
| 403 | // Make sure the controller is connected | ||
| 404 | player.connected = true; | ||
| 405 | |||
| 406 | emulated_controller->ReloadFromSettings(); | ||
| 407 | |||
| 408 | if (j_player_index > 0) { | ||
| 409 | return; | ||
| 410 | } | ||
| 411 | // Handle Handheld cases | ||
| 412 | auto& handheld_player = Settings::values.players.GetValue()[HANDHELD_INDEX]; | ||
| 413 | auto* handheld_controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld); | ||
| 414 | if (player.controller_type == Settings::ControllerType::Handheld) { | ||
| 415 | handheld_player = player; | ||
| 416 | } else { | ||
| 417 | handheld_player = {}; | ||
| 418 | } | ||
| 419 | handheld_controller->ReloadFromSettings(); | ||
| 420 | } | ||
| 421 | |||
| 422 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_beginMapping(JNIEnv* env, jobject j_obj, | ||
| 423 | jint jtype) { | ||
| 424 | EmulationSession::GetInstance().GetInputSubsystem().BeginMapping( | ||
| 425 | static_cast<InputCommon::Polling::InputType>(jtype)); | ||
| 426 | } | ||
| 427 | |||
| 428 | jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getNextInput(JNIEnv* env, | ||
| 429 | jobject j_obj) { | ||
| 430 | return Common::Android::ToJString( | ||
| 431 | env, EmulationSession::GetInstance().GetInputSubsystem().GetNextInput().Serialize()); | ||
| 432 | } | ||
| 433 | |||
| 434 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_stopMapping(JNIEnv* env, jobject j_obj) { | ||
| 435 | EmulationSession::GetInstance().GetInputSubsystem().StopMapping(); | ||
| 436 | } | ||
| 437 | |||
| 438 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_updateMappingsWithDefaultImpl( | ||
| 439 | JNIEnv* env, jobject j_obj, jint j_player_index, jstring j_device_params, | ||
| 440 | jstring j_display_name) { | ||
| 441 | auto& input_subsystem = EmulationSession::GetInstance().GetInputSubsystem(); | ||
| 442 | |||
| 443 | // Clear all previous mappings | ||
| 444 | for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) { | ||
| 445 | ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { | ||
| 446 | controller->SetButtonParam(button_id, {}); | ||
| 447 | }); | ||
| 448 | } | ||
| 449 | for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) { | ||
| 450 | ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { | ||
| 451 | controller->SetStickParam(analog_id, {}); | ||
| 452 | }); | ||
| 453 | } | ||
| 454 | |||
| 455 | // Apply new mappings | ||
| 456 | auto device = Common::ParamPackage(Common::Android::GetJString(env, j_device_params)); | ||
| 457 | auto button_mappings = input_subsystem.GetButtonMappingForDevice(device); | ||
| 458 | auto analog_mappings = input_subsystem.GetAnalogMappingForDevice(device); | ||
| 459 | auto display_name = Common::Android::GetJString(env, j_display_name); | ||
| 460 | for (const auto& button_mapping : button_mappings) { | ||
| 461 | const std::size_t index = button_mapping.first; | ||
| 462 | auto named_mapping = button_mapping.second; | ||
| 463 | named_mapping.Set("display", display_name); | ||
| 464 | ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { | ||
| 465 | controller->SetButtonParam(index, named_mapping); | ||
| 466 | }); | ||
| 467 | } | ||
| 468 | for (const auto& analog_mapping : analog_mappings) { | ||
| 469 | const std::size_t index = analog_mapping.first; | ||
| 470 | auto named_mapping = analog_mapping.second; | ||
| 471 | named_mapping.Set("display", display_name); | ||
| 472 | ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { | ||
| 473 | controller->SetStickParam(index, named_mapping); | ||
| 474 | }); | ||
| 475 | } | ||
| 476 | } | ||
| 477 | |||
| 478 | jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getButtonParamImpl(JNIEnv* env, | ||
| 479 | jobject j_obj, | ||
| 480 | jint j_player_index, | ||
| 481 | jint j_button) { | ||
| 482 | return Common::Android::ToJString(env, EmulationSession::GetInstance() | ||
| 483 | .System() | ||
| 484 | .HIDCore() | ||
| 485 | .GetEmulatedControllerByIndex(j_player_index) | ||
| 486 | ->GetButtonParam(j_button) | ||
| 487 | .Serialize()); | ||
| 488 | } | ||
| 489 | |||
| 490 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setButtonParamImpl( | ||
| 491 | JNIEnv* env, jobject j_obj, jint j_player_index, jint j_button_id, jstring j_param) { | ||
| 492 | ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { | ||
| 493 | controller->SetButtonParam(j_button_id, | ||
| 494 | Common::ParamPackage(Common::Android::GetJString(env, j_param))); | ||
| 495 | }); | ||
| 496 | } | ||
| 497 | |||
| 498 | jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getStickParamImpl(JNIEnv* env, | ||
| 499 | jobject j_obj, | ||
| 500 | jint j_player_index, | ||
| 501 | jint j_stick) { | ||
| 502 | return Common::Android::ToJString(env, EmulationSession::GetInstance() | ||
| 503 | .System() | ||
| 504 | .HIDCore() | ||
| 505 | .GetEmulatedControllerByIndex(j_player_index) | ||
| 506 | ->GetStickParam(j_stick) | ||
| 507 | .Serialize()); | ||
| 508 | } | ||
| 509 | |||
| 510 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setStickParamImpl( | ||
| 511 | JNIEnv* env, jobject j_obj, jint j_player_index, jint j_stick_id, jstring j_param) { | ||
| 512 | ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { | ||
| 513 | controller->SetStickParam(j_stick_id, | ||
| 514 | Common::ParamPackage(Common::Android::GetJString(env, j_param))); | ||
| 515 | }); | ||
| 516 | } | ||
| 517 | |||
| 518 | jint Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getButtonNameImpl(JNIEnv* env, | ||
| 519 | jobject j_obj, | ||
| 520 | jstring j_param) { | ||
| 521 | return static_cast<jint>(EmulationSession::GetInstance().GetInputSubsystem().GetButtonName( | ||
| 522 | Common::ParamPackage(Common::Android::GetJString(env, j_param)))); | ||
| 523 | } | ||
| 524 | |||
| 525 | jintArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getSupportedStyleTagsImpl( | ||
| 526 | JNIEnv* env, jobject j_obj, jint j_player_index) { | ||
| 527 | auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); | ||
| 528 | const auto npad_style_set = hid_core.GetSupportedStyleTag(); | ||
| 529 | std::vector<s32> supported_indexes; | ||
| 530 | if (npad_style_set.fullkey == 1) { | ||
| 531 | supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::Fullkey)); | ||
| 532 | } | ||
| 533 | |||
| 534 | if (npad_style_set.joycon_dual == 1) { | ||
| 535 | supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconDual)); | ||
| 536 | } | ||
| 537 | |||
| 538 | if (npad_style_set.joycon_left == 1) { | ||
| 539 | supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconLeft)); | ||
| 540 | } | ||
| 541 | |||
| 542 | if (npad_style_set.joycon_right == 1) { | ||
| 543 | supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconRight)); | ||
| 544 | } | ||
| 545 | |||
| 546 | if (j_player_index == 0 && npad_style_set.handheld == 1) { | ||
| 547 | supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::Handheld)); | ||
| 548 | } | ||
| 549 | |||
| 550 | if (npad_style_set.gamecube == 1) { | ||
| 551 | supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::GameCube)); | ||
| 552 | } | ||
| 553 | |||
| 554 | jintArray j_supported_indexes = env->NewIntArray(supported_indexes.size()); | ||
| 555 | env->SetIntArrayRegion(j_supported_indexes, 0, supported_indexes.size(), | ||
| 556 | supported_indexes.data()); | ||
| 557 | return j_supported_indexes; | ||
| 558 | } | ||
| 559 | |||
| 560 | jint Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getStyleIndexImpl(JNIEnv* env, | ||
| 561 | jobject j_obj, | ||
| 562 | jint j_player_index) { | ||
| 563 | return static_cast<s32>(EmulationSession::GetInstance() | ||
| 564 | .System() | ||
| 565 | .HIDCore() | ||
| 566 | .GetEmulatedControllerByIndex(j_player_index) | ||
| 567 | ->GetNpadStyleIndex(true)); | ||
| 568 | } | ||
| 569 | |||
| 570 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setStyleIndexImpl(JNIEnv* env, | ||
| 571 | jobject j_obj, | ||
| 572 | jint j_player_index, | ||
| 573 | jint j_style_index) { | ||
| 574 | auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); | ||
| 575 | auto type = static_cast<Core::HID::NpadStyleIndex>(j_style_index); | ||
| 576 | ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { | ||
| 577 | controller->SetNpadStyleIndex(type); | ||
| 578 | }); | ||
| 579 | if (j_player_index == 0) { | ||
| 580 | auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld); | ||
| 581 | auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1); | ||
| 582 | ConnectController(j_player_index, | ||
| 583 | player_one->IsConnected(true) || handheld->IsConnected(true)); | ||
| 584 | } | ||
| 585 | } | ||
| 586 | |||
| 587 | jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isControllerImpl(JNIEnv* env, | ||
| 588 | jobject j_obj, | ||
| 589 | jstring jparams) { | ||
| 590 | return static_cast<jint>(EmulationSession::GetInstance().GetInputSubsystem().IsController( | ||
| 591 | Common::ParamPackage(Common::Android::GetJString(env, jparams)))); | ||
| 592 | } | ||
| 593 | |||
| 594 | jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getIsConnected(JNIEnv* env, | ||
| 595 | jobject j_obj, | ||
| 596 | jint j_player_index) { | ||
| 597 | auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); | ||
| 598 | auto* controller = hid_core.GetEmulatedControllerByIndex(static_cast<size_t>(j_player_index)); | ||
| 599 | if (j_player_index == 0 && | ||
| 600 | controller->GetNpadStyleIndex(true) == Core::HID::NpadStyleIndex::Handheld) { | ||
| 601 | return hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld)->IsConnected(true); | ||
| 602 | } | ||
| 603 | return controller->IsConnected(true); | ||
| 604 | } | ||
| 605 | |||
| 606 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_connectControllersImpl( | ||
| 607 | JNIEnv* env, jobject j_obj, jbooleanArray j_connected) { | ||
| 608 | jboolean isCopy = false; | ||
| 609 | auto j_connected_array_size = env->GetArrayLength(j_connected); | ||
| 610 | jboolean* j_connected_array = env->GetBooleanArrayElements(j_connected, &isCopy); | ||
| 611 | for (int i = 0; i < j_connected_array_size; ++i) { | ||
| 612 | ConnectController(i, j_connected_array[i]); | ||
| 613 | } | ||
| 614 | } | ||
| 615 | |||
| 616 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_resetControllerMappings( | ||
| 617 | JNIEnv* env, jobject j_obj, jint j_player_index) { | ||
| 618 | // Clear all previous mappings | ||
| 619 | for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) { | ||
| 620 | ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { | ||
| 621 | controller->SetButtonParam(button_id, {}); | ||
| 622 | }); | ||
| 623 | } | ||
| 624 | for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) { | ||
| 625 | ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { | ||
| 626 | controller->SetStickParam(analog_id, {}); | ||
| 627 | }); | ||
| 628 | } | ||
| 629 | } | ||
| 630 | |||
| 631 | } // extern "C" | ||
diff --git a/src/android/app/src/main/res/drawable/button_anim.xml b/src/android/app/src/main/res/drawable/button_anim.xml new file mode 100644 index 000000000..ccdc5ca6a --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_anim.xml | |||
| @@ -0,0 +1,142 @@ | |||
| 1 | <animated-vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | xmlns:aapt="http://schemas.android.com/aapt"> | ||
| 3 | <aapt:attr name="android:drawable"> | ||
| 4 | <vector | ||
| 5 | android:width="1000dp" | ||
| 6 | android:height="1000dp" | ||
| 7 | android:viewportWidth="1000" | ||
| 8 | android:viewportHeight="1000"> | ||
| 9 | <group android:name="_R_G"> | ||
| 10 | <group | ||
| 11 | android:name="_R_G_L_0_G" | ||
| 12 | android:pivotX="100" | ||
| 13 | android:pivotY="100" | ||
| 14 | android:scaleX="4.5" | ||
| 15 | android:scaleY="4.5" | ||
| 16 | android:translateX="400" | ||
| 17 | android:translateY="400"> | ||
| 18 | <path | ||
| 19 | android:name="_R_G_L_0_G_D_0_P_0" | ||
| 20 | android:fillAlpha="1" | ||
| 21 | android:fillColor="?attr/colorSecondaryContainer" | ||
| 22 | android:fillType="nonZero" | ||
| 23 | android:pathData=" M198.56 100 C198.56,154.43 154.43,198.56 100,198.56 C45.57,198.56 1.44,154.43 1.44,100 C1.44,45.57 45.57,1.44 100,1.44 C154.43,1.44 198.56,45.57 198.56,100c " /> | ||
| 24 | <path | ||
| 25 | android:name="_R_G_L_0_G_D_2_P_0" | ||
| 26 | android:fillAlpha="0.8" | ||
| 27 | android:fillColor="?attr/colorOnSecondaryContainer" | ||
| 28 | android:fillType="nonZero" | ||
| 29 | android:pathData=" M50.14 151.21 C50.53,150.18 89.6,49.87 90.1,48.63 C90.1,48.63 90.67,47.2 90.67,47.2 C90.67,47.2 101.67,47.2 101.67,47.2 C101.67,47.2 112.67,47.2 112.67,47.2 C112.67,47.2 133.47,99.12 133.47,99.12 C144.91,127.68 154.32,151.17 154.38,151.33 C154.47,151.56 152.2,151.6 143.14,151.55 C143.14,151.55 131.79,151.48 131.79,151.48 C131.79,151.48 127.22,139.57 127.22,139.57 C127.22,139.57 122.65,127.66 122.65,127.66 C122.65,127.66 101.68,127.73 101.68,127.73 C101.68,127.73 80.71,127.8 80.71,127.8 C80.71,127.8 76.38,139.71 76.38,139.71 C76.38,139.71 72.06,151.62 72.06,151.62 C72.06,151.62 61.02,151.62 61.02,151.62 C50.61,151.62 50,151.55 50.14,151.22 C50.14,151.22 50.14,151.21 50.14,151.21c M115.86 110.06 C115.8,109.91 112.55,101.13 108.62,90.56 C104.7,80 101.42,71.43 101.34,71.53 C101.22,71.66 92.84,94.61 87.25,110.06 C87.17,110.29 90.13,110.34 101.56,110.34 C113,110.34 115.95,110.28 115.86,110.06c " /> | ||
| 30 | </group> | ||
| 31 | </group> | ||
| 32 | <group android:name="time_group" /> | ||
| 33 | </vector> | ||
| 34 | </aapt:attr> | ||
| 35 | <target android:name="_R_G_L_0_G"> | ||
| 36 | <aapt:attr name="android:animation"> | ||
| 37 | <set android:ordering="together"> | ||
| 38 | <objectAnimator | ||
| 39 | android:duration="100" | ||
| 40 | android:propertyName="scaleX" | ||
| 41 | android:startOffset="0" | ||
| 42 | android:valueFrom="4.5" | ||
| 43 | android:valueTo="3.75" | ||
| 44 | android:valueType="floatType"> | ||
| 45 | <aapt:attr name="android:interpolator"> | ||
| 46 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> | ||
| 47 | </aapt:attr> | ||
| 48 | </objectAnimator> | ||
| 49 | <objectAnimator | ||
| 50 | android:duration="100" | ||
| 51 | android:propertyName="scaleY" | ||
| 52 | android:startOffset="0" | ||
| 53 | android:valueFrom="4.5" | ||
| 54 | android:valueTo="3.75" | ||
| 55 | android:valueType="floatType"> | ||
| 56 | <aapt:attr name="android:interpolator"> | ||
| 57 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> | ||
| 58 | </aapt:attr> | ||
| 59 | </objectAnimator> | ||
| 60 | <objectAnimator | ||
| 61 | android:duration="234" | ||
| 62 | android:propertyName="scaleX" | ||
| 63 | android:startOffset="100" | ||
| 64 | android:valueFrom="3.75" | ||
| 65 | android:valueTo="3.75" | ||
| 66 | android:valueType="floatType"> | ||
| 67 | <aapt:attr name="android:interpolator"> | ||
| 68 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> | ||
| 69 | </aapt:attr> | ||
| 70 | </objectAnimator> | ||
| 71 | <objectAnimator | ||
| 72 | android:duration="234" | ||
| 73 | android:propertyName="scaleY" | ||
| 74 | android:startOffset="100" | ||
| 75 | android:valueFrom="3.75" | ||
| 76 | android:valueTo="3.75" | ||
| 77 | android:valueType="floatType"> | ||
| 78 | <aapt:attr name="android:interpolator"> | ||
| 79 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> | ||
| 80 | </aapt:attr> | ||
| 81 | </objectAnimator> | ||
| 82 | <objectAnimator | ||
| 83 | android:duration="167" | ||
| 84 | android:propertyName="scaleX" | ||
| 85 | android:startOffset="334" | ||
| 86 | android:valueFrom="3.75" | ||
| 87 | android:valueTo="4.75" | ||
| 88 | android:valueType="floatType"> | ||
| 89 | <aapt:attr name="android:interpolator"> | ||
| 90 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> | ||
| 91 | </aapt:attr> | ||
| 92 | </objectAnimator> | ||
| 93 | <objectAnimator | ||
| 94 | android:duration="167" | ||
| 95 | android:propertyName="scaleY" | ||
| 96 | android:startOffset="334" | ||
| 97 | android:valueFrom="3.75" | ||
| 98 | android:valueTo="4.75" | ||
| 99 | android:valueType="floatType"> | ||
| 100 | <aapt:attr name="android:interpolator"> | ||
| 101 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> | ||
| 102 | </aapt:attr> | ||
| 103 | </objectAnimator> | ||
| 104 | <objectAnimator | ||
| 105 | android:duration="67" | ||
| 106 | android:propertyName="scaleX" | ||
| 107 | android:startOffset="501" | ||
| 108 | android:valueFrom="4.75" | ||
| 109 | android:valueTo="4.5" | ||
| 110 | android:valueType="floatType"> | ||
| 111 | <aapt:attr name="android:interpolator"> | ||
| 112 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> | ||
| 113 | </aapt:attr> | ||
| 114 | </objectAnimator> | ||
| 115 | <objectAnimator | ||
| 116 | android:duration="67" | ||
| 117 | android:propertyName="scaleY" | ||
| 118 | android:startOffset="501" | ||
| 119 | android:valueFrom="4.75" | ||
| 120 | android:valueTo="4.5" | ||
| 121 | android:valueType="floatType"> | ||
| 122 | <aapt:attr name="android:interpolator"> | ||
| 123 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> | ||
| 124 | </aapt:attr> | ||
| 125 | </objectAnimator> | ||
| 126 | </set> | ||
| 127 | </aapt:attr> | ||
| 128 | </target> | ||
| 129 | <target android:name="time_group"> | ||
| 130 | <aapt:attr name="android:animation"> | ||
| 131 | <set android:ordering="together"> | ||
| 132 | <objectAnimator | ||
| 133 | android:duration="1034" | ||
| 134 | android:propertyName="translateX" | ||
| 135 | android:startOffset="0" | ||
| 136 | android:valueFrom="0" | ||
| 137 | android:valueTo="1" | ||
| 138 | android:valueType="floatType" /> | ||
| 139 | </set> | ||
| 140 | </aapt:attr> | ||
| 141 | </target> | ||
| 142 | </animated-vector> | ||
diff --git a/src/android/app/src/main/res/drawable/ic_controller_disconnected.xml b/src/android/app/src/main/res/drawable/ic_controller_disconnected.xml new file mode 100644 index 000000000..8e3c66f74 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_controller_disconnected.xml | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | android:width="24dp" | ||
| 3 | android:height="24dp" | ||
| 4 | android:viewportWidth="960" | ||
| 5 | android:viewportHeight="960"> | ||
| 6 | <path | ||
| 7 | android:fillColor="?attr/colorControlNormal" | ||
| 8 | android:pathData="M700,480q-25,0 -42.5,-17.5T640,420q0,-25 17.5,-42.5T700,360q25,0 42.5,17.5T760,420q0,25 -17.5,42.5T700,480ZM366,480ZM280,600v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80ZM160,720q-33,0 -56.5,-23.5T80,640v-320q0,-34 24,-57.5t58,-23.5h77l81,81L160,320v320h366L55,169l57,-57 736,736 -57,57 -185,-185L160,720ZM880,640q0,26 -14,46t-37,29l-29,-29v-366L434,320l-80,-80h446q33,0 56.5,23.5T880,320v320ZM617,503Z" /> | ||
| 9 | </vector> | ||
diff --git a/src/android/app/src/main/res/drawable/ic_more_vert.xml b/src/android/app/src/main/res/drawable/ic_more_vert.xml new file mode 100644 index 000000000..9f62ac595 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_more_vert.xml | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | android:height="24dp" | ||
| 3 | android:viewportHeight="24" | ||
| 4 | android:viewportWidth="24" | ||
| 5 | android:width="24dp"> | ||
| 6 | <path | ||
| 7 | android:fillColor="?attr/colorControlNormal" | ||
| 8 | android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" /> | ||
| 9 | </vector> | ||
diff --git a/src/android/app/src/main/res/drawable/ic_new_label.xml b/src/android/app/src/main/res/drawable/ic_new_label.xml new file mode 100644 index 000000000..fac562c26 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_new_label.xml | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | android:width="24dp" | ||
| 3 | android:height="24dp" | ||
| 4 | android:viewportWidth="24" | ||
| 5 | android:viewportHeight="24"> | ||
| 6 | <path | ||
| 7 | android:fillColor="?attr/colorControlNormal" | ||
| 8 | android:pathData="M21,12l-4.37,6.16C16.26,18.68 15.65,19 15,19h-3l0,-6H9v-3H3V7c0,-1.1 0.9,-2 2,-2h10c0.65,0 1.26,0.31 1.63,0.84L21,12zM10,15H7v-3H5v3H2v2h3v3h2v-3h3V15z" /> | ||
| 9 | </vector> | ||
diff --git a/src/android/app/src/main/res/drawable/ic_overlay.xml b/src/android/app/src/main/res/drawable/ic_overlay.xml new file mode 100644 index 000000000..c7986c5a2 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_overlay.xml | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | android:width="24dp" | ||
| 3 | android:height="24dp" | ||
| 4 | android:viewportWidth="24" | ||
| 5 | android:viewportHeight="24"> | ||
| 6 | <path | ||
| 7 | android:fillColor="?attr/colorControlNormal" | ||
| 8 | android:pathData="M21,5H3C1.9,5 1,5.9 1,7v10c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V7C23,5.9 22.1,5 21,5zM18,17H6V7h12V17z" /> | ||
| 9 | <path | ||
| 10 | android:fillColor="?attr/colorControlNormal" | ||
| 11 | android:pathData="M15,11.25h1.5v1.5h-1.5z" /> | ||
| 12 | <path | ||
| 13 | android:fillColor="?attr/colorControlNormal" | ||
| 14 | android:pathData="M12.5,11.25h1.5v1.5h-1.5z" /> | ||
| 15 | <path | ||
| 16 | android:fillColor="?attr/colorControlNormal" | ||
| 17 | android:pathData="M10,11.25h1.5v1.5h-1.5z" /> | ||
| 18 | <path | ||
| 19 | android:fillColor="?attr/colorControlNormal" | ||
| 20 | android:pathData="M7.5,11.25h1.5v1.5h-1.5z" /> | ||
| 21 | </vector> | ||
diff --git a/src/android/app/src/main/res/drawable/ic_share.xml b/src/android/app/src/main/res/drawable/ic_share.xml new file mode 100644 index 000000000..3fc2f3c99 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_share.xml | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | android:width="24dp" | ||
| 3 | android:height="24dp" | ||
| 4 | android:viewportWidth="24" | ||
| 5 | android:viewportHeight="24"> | ||
| 6 | <path | ||
| 7 | android:fillColor="?attr/colorControlNormal" | ||
| 8 | android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" /> | ||
| 9 | </vector> | ||
diff --git a/src/android/app/src/main/res/drawable/stick_one_direction_anim.xml b/src/android/app/src/main/res/drawable/stick_one_direction_anim.xml new file mode 100644 index 000000000..a1da1316f --- /dev/null +++ b/src/android/app/src/main/res/drawable/stick_one_direction_anim.xml | |||
| @@ -0,0 +1,118 @@ | |||
| 1 | <animated-vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | xmlns:aapt="http://schemas.android.com/aapt"> | ||
| 3 | <aapt:attr name="android:drawable"> | ||
| 4 | <vector | ||
| 5 | android:width="1000dp" | ||
| 6 | android:height="1000dp" | ||
| 7 | android:viewportWidth="1000" | ||
| 8 | android:viewportHeight="1000"> | ||
| 9 | <group android:name="_R_G"> | ||
| 10 | <group | ||
| 11 | android:name="_R_G_L_1_G" | ||
| 12 | android:pivotX="100" | ||
| 13 | android:pivotY="100" | ||
| 14 | android:scaleX="5" | ||
| 15 | android:scaleY="5" | ||
| 16 | android:translateX="400" | ||
| 17 | android:translateY="400"> | ||
| 18 | <path | ||
| 19 | android:name="_R_G_L_1_G_D_0_P_0" | ||
| 20 | android:pathData=" M100 199.39 C59.8,199.39 23.56,175.17 8.18,138.04 C-7.2,100.9 1.3,58.15 29.73,29.72 C58.15,1.3 100.9,-7.21 138.04,8.18 C175.18,23.56 199.39,59.8 199.39,100 C199.33,154.87 154.87,199.33 100,199.39c " | ||
| 21 | android:strokeWidth="1" | ||
| 22 | android:strokeAlpha="0.6" | ||
| 23 | android:strokeColor="?attr/colorOutline" | ||
| 24 | android:strokeLineCap="round" | ||
| 25 | android:strokeLineJoin="round" /> | ||
| 26 | </group> | ||
| 27 | <group | ||
| 28 | android:name="_R_G_L_0_G_T_1" | ||
| 29 | android:scaleX="5" | ||
| 30 | android:scaleY="5" | ||
| 31 | android:translateX="500" | ||
| 32 | android:translateY="500"> | ||
| 33 | <group | ||
| 34 | android:name="_R_G_L_0_G" | ||
| 35 | android:translateX="-100" | ||
| 36 | android:translateY="-100"> | ||
| 37 | <path | ||
| 38 | android:name="_R_G_L_0_G_D_0_P_0" | ||
| 39 | android:fillAlpha="1" | ||
| 40 | android:fillColor="?attr/colorSecondaryContainer" | ||
| 41 | android:fillType="nonZero" | ||
| 42 | android:pathData=" M100.45 28.02 C140.63,28.02 173.2,60.59 173.2,100.77 C173.2,140.95 140.63,173.52 100.45,173.52 C60.27,173.52 27.7,140.95 27.7,100.77 C27.7,60.59 60.27,28.02 100.45,28.02c " /> | ||
| 43 | <path | ||
| 44 | android:name="_R_G_L_0_G_D_2_P_0" | ||
| 45 | android:fillAlpha="0.8" | ||
| 46 | android:fillColor="?attr/colorOnSecondaryContainer" | ||
| 47 | android:fillType="nonZero" | ||
| 48 | android:pathData=" M100.45 50.26 C128.62,50.26 151.46,73.1 151.46,101.28 C151.46,129.45 128.62,152.29 100.45,152.29 C72.27,152.29 49.43,129.45 49.43,101.28 C49.43,73.1 72.27,50.26 100.45,50.26c " /> | ||
| 49 | </group> | ||
| 50 | </group> | ||
| 51 | </group> | ||
| 52 | <group android:name="time_group" /> | ||
| 53 | </vector> | ||
| 54 | </aapt:attr> | ||
| 55 | <target android:name="_R_G_L_0_G_T_1"> | ||
| 56 | <aapt:attr name="android:animation"> | ||
| 57 | <set android:ordering="together"> | ||
| 58 | <objectAnimator | ||
| 59 | android:duration="267" | ||
| 60 | android:pathData="M 500,500C 500,500 364,500 364,500" | ||
| 61 | android:propertyName="translateXY" | ||
| 62 | android:propertyXName="translateX" | ||
| 63 | android:propertyYName="translateY" | ||
| 64 | android:startOffset="0"> | ||
| 65 | <aapt:attr name="android:interpolator"> | ||
| 66 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> | ||
| 67 | </aapt:attr> | ||
| 68 | </objectAnimator> | ||
| 69 | <objectAnimator | ||
| 70 | android:duration="234" | ||
| 71 | android:pathData="M 364,500C 364,500 364,500 364,500" | ||
| 72 | android:propertyName="translateXY" | ||
| 73 | android:propertyXName="translateX" | ||
| 74 | android:propertyYName="translateY" | ||
| 75 | android:startOffset="267"> | ||
| 76 | <aapt:attr name="android:interpolator"> | ||
| 77 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" /> | ||
| 78 | </aapt:attr> | ||
| 79 | </objectAnimator> | ||
| 80 | <objectAnimator | ||
| 81 | android:duration="133" | ||
| 82 | android:pathData="M 364,500C 364,500 525,500 525,500" | ||
| 83 | android:propertyName="translateXY" | ||
| 84 | android:propertyXName="translateX" | ||
| 85 | android:propertyYName="translateY" | ||
| 86 | android:startOffset="501"> | ||
| 87 | <aapt:attr name="android:interpolator"> | ||
| 88 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> | ||
| 89 | </aapt:attr> | ||
| 90 | </objectAnimator> | ||
| 91 | <objectAnimator | ||
| 92 | android:duration="100" | ||
| 93 | android:pathData="M 525,500C 525,500 500,500 500,500" | ||
| 94 | android:propertyName="translateXY" | ||
| 95 | android:propertyXName="translateX" | ||
| 96 | android:propertyYName="translateY" | ||
| 97 | android:startOffset="634"> | ||
| 98 | <aapt:attr name="android:interpolator"> | ||
| 99 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> | ||
| 100 | </aapt:attr> | ||
| 101 | </objectAnimator> | ||
| 102 | </set> | ||
| 103 | </aapt:attr> | ||
| 104 | </target> | ||
| 105 | <target android:name="time_group"> | ||
| 106 | <aapt:attr name="android:animation"> | ||
| 107 | <set android:ordering="together"> | ||
| 108 | <objectAnimator | ||
| 109 | android:duration="968" | ||
| 110 | android:propertyName="translateX" | ||
| 111 | android:startOffset="0" | ||
| 112 | android:valueFrom="0" | ||
| 113 | android:valueTo="1" | ||
| 114 | android:valueType="floatType" /> | ||
| 115 | </set> | ||
| 116 | </aapt:attr> | ||
| 117 | </target> | ||
| 118 | </animated-vector> | ||
diff --git a/src/android/app/src/main/res/drawable/stick_two_direction_anim.xml b/src/android/app/src/main/res/drawable/stick_two_direction_anim.xml new file mode 100644 index 000000000..bc71adcbd --- /dev/null +++ b/src/android/app/src/main/res/drawable/stick_two_direction_anim.xml | |||
| @@ -0,0 +1,173 @@ | |||
| 1 | <animated-vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | xmlns:aapt="http://schemas.android.com/aapt"> | ||
| 3 | <aapt:attr name="android:drawable"> | ||
| 4 | <vector | ||
| 5 | android:width="1000dp" | ||
| 6 | android:height="1000dp" | ||
| 7 | android:viewportWidth="1000" | ||
| 8 | android:viewportHeight="1000"> | ||
| 9 | <group android:name="_R_G"> | ||
| 10 | <group | ||
| 11 | android:name="_R_G_L_1_G" | ||
| 12 | android:pivotX="100" | ||
| 13 | android:pivotY="100" | ||
| 14 | android:scaleX="5" | ||
| 15 | android:scaleY="5" | ||
| 16 | android:translateX="400" | ||
| 17 | android:translateY="400"> | ||
| 18 | <path | ||
| 19 | android:name="_R_G_L_1_G_D_0_P_0" | ||
| 20 | android:pathData=" M100 199.39 C59.8,199.39 23.56,175.17 8.18,138.04 C-7.2,100.9 1.3,58.15 29.73,29.72 C58.15,1.3 100.9,-7.21 138.04,8.18 C175.18,23.56 199.39,59.8 199.39,100 C199.33,154.87 154.87,199.33 100,199.39c " | ||
| 21 | android:strokeWidth="1" | ||
| 22 | android:strokeAlpha="0.6" | ||
| 23 | android:strokeColor="?attr/colorOutline" | ||
| 24 | android:strokeLineCap="round" | ||
| 25 | android:strokeLineJoin="round" /> | ||
| 26 | </group> | ||
| 27 | <group | ||
| 28 | android:name="_R_G_L_0_G_T_1" | ||
| 29 | android:scaleX="5" | ||
| 30 | android:scaleY="5" | ||
| 31 | android:translateX="500" | ||
| 32 | android:translateY="500"> | ||
| 33 | <group | ||
| 34 | android:name="_R_G_L_0_G" | ||
| 35 | android:translateX="-100" | ||
| 36 | android:translateY="-100"> | ||
| 37 | <path | ||
| 38 | android:name="_R_G_L_0_G_D_0_P_0" | ||
| 39 | android:fillAlpha="1" | ||
| 40 | android:fillColor="?attr/colorSecondaryContainer" | ||
| 41 | android:fillType="nonZero" | ||
| 42 | android:pathData=" M100.45 28.02 C140.63,28.02 173.2,60.59 173.2,100.77 C173.2,140.95 140.63,173.52 100.45,173.52 C60.27,173.52 27.7,140.95 27.7,100.77 C27.7,60.59 60.27,28.02 100.45,28.02c " /> | ||
| 43 | <path | ||
| 44 | android:name="_R_G_L_0_G_D_2_P_0" | ||
| 45 | android:fillAlpha="0.8" | ||
| 46 | android:fillColor="?attr/colorOnSecondaryContainer" | ||
| 47 | android:fillType="nonZero" | ||
| 48 | android:pathData=" M100.45 50.26 C128.62,50.26 151.46,73.1 151.46,101.28 C151.46,129.45 128.62,152.29 100.45,152.29 C72.27,152.29 49.43,129.45 49.43,101.28 C49.43,73.1 72.27,50.26 100.45,50.26c " /> | ||
| 49 | </group> | ||
| 50 | </group> | ||
| 51 | </group> | ||
| 52 | <group android:name="time_group" /> | ||
| 53 | </vector> | ||
| 54 | </aapt:attr> | ||
| 55 | <target android:name="_R_G_L_0_G_T_1"> | ||
| 56 | <aapt:attr name="android:animation"> | ||
| 57 | <set android:ordering="together"> | ||
| 58 | <objectAnimator | ||
| 59 | android:duration="267" | ||
| 60 | android:pathData="M 500,500C 500,500 364,500 364,500" | ||
| 61 | android:propertyName="translateXY" | ||
| 62 | android:propertyXName="translateX" | ||
| 63 | android:propertyYName="translateY" | ||
| 64 | android:startOffset="0"> | ||
| 65 | <aapt:attr name="android:interpolator"> | ||
| 66 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> | ||
| 67 | </aapt:attr> | ||
| 68 | </objectAnimator> | ||
| 69 | <objectAnimator | ||
| 70 | android:duration="234" | ||
| 71 | android:pathData="M 364,500C 364,500 364,500 364,500" | ||
| 72 | android:propertyName="translateXY" | ||
| 73 | android:propertyXName="translateX" | ||
| 74 | android:propertyYName="translateY" | ||
| 75 | android:startOffset="267"> | ||
| 76 | <aapt:attr name="android:interpolator"> | ||
| 77 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" /> | ||
| 78 | </aapt:attr> | ||
| 79 | </objectAnimator> | ||
| 80 | <objectAnimator | ||
| 81 | android:duration="133" | ||
| 82 | android:pathData="M 364,500C 364,500 525,500 525,500" | ||
| 83 | android:propertyName="translateXY" | ||
| 84 | android:propertyXName="translateX" | ||
| 85 | android:propertyYName="translateY" | ||
| 86 | android:startOffset="501"> | ||
| 87 | <aapt:attr name="android:interpolator"> | ||
| 88 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> | ||
| 89 | </aapt:attr> | ||
| 90 | </objectAnimator> | ||
| 91 | <objectAnimator | ||
| 92 | android:duration="100" | ||
| 93 | android:pathData="M 525,500C 525,500 500,500 500,500" | ||
| 94 | android:propertyName="translateXY" | ||
| 95 | android:propertyXName="translateX" | ||
| 96 | android:propertyYName="translateY" | ||
| 97 | android:startOffset="634"> | ||
| 98 | <aapt:attr name="android:interpolator"> | ||
| 99 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> | ||
| 100 | </aapt:attr> | ||
| 101 | </objectAnimator> | ||
| 102 | <objectAnimator | ||
| 103 | android:duration="400" | ||
| 104 | android:pathData="M 500,500C 500,500 500,500 500,500" | ||
| 105 | android:propertyName="translateXY" | ||
| 106 | android:propertyXName="translateX" | ||
| 107 | android:propertyYName="translateY" | ||
| 108 | android:startOffset="734"> | ||
| 109 | <aapt:attr name="android:interpolator"> | ||
| 110 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" /> | ||
| 111 | </aapt:attr> | ||
| 112 | </objectAnimator> | ||
| 113 | <objectAnimator | ||
| 114 | android:duration="267" | ||
| 115 | android:pathData="M 500,500C 500,500 500,364 500,364" | ||
| 116 | android:propertyName="translateXY" | ||
| 117 | android:propertyXName="translateX" | ||
| 118 | android:propertyYName="translateY" | ||
| 119 | android:startOffset="1134"> | ||
| 120 | <aapt:attr name="android:interpolator"> | ||
| 121 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> | ||
| 122 | </aapt:attr> | ||
| 123 | </objectAnimator> | ||
| 124 | <objectAnimator | ||
| 125 | android:duration="234" | ||
| 126 | android:pathData="M 500,364C 500,364 500,364 500,364" | ||
| 127 | android:propertyName="translateXY" | ||
| 128 | android:propertyXName="translateX" | ||
| 129 | android:propertyYName="translateY" | ||
| 130 | android:startOffset="1401"> | ||
| 131 | <aapt:attr name="android:interpolator"> | ||
| 132 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" /> | ||
| 133 | </aapt:attr> | ||
| 134 | </objectAnimator> | ||
| 135 | <objectAnimator | ||
| 136 | android:duration="133" | ||
| 137 | android:pathData="M 500,364C 500,364 500,535 500,535" | ||
| 138 | android:propertyName="translateXY" | ||
| 139 | android:propertyXName="translateX" | ||
| 140 | android:propertyYName="translateY" | ||
| 141 | android:startOffset="1635"> | ||
| 142 | <aapt:attr name="android:interpolator"> | ||
| 143 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> | ||
| 144 | </aapt:attr> | ||
| 145 | </objectAnimator> | ||
| 146 | <objectAnimator | ||
| 147 | android:duration="100" | ||
| 148 | android:pathData="M 500,535C 500,535 500,500 500,500" | ||
| 149 | android:propertyName="translateXY" | ||
| 150 | android:propertyXName="translateX" | ||
| 151 | android:propertyYName="translateY" | ||
| 152 | android:startOffset="1768"> | ||
| 153 | <aapt:attr name="android:interpolator"> | ||
| 154 | <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" /> | ||
| 155 | </aapt:attr> | ||
| 156 | </objectAnimator> | ||
| 157 | </set> | ||
| 158 | </aapt:attr> | ||
| 159 | </target> | ||
| 160 | <target android:name="time_group"> | ||
| 161 | <aapt:attr name="android:animation"> | ||
| 162 | <set android:ordering="together"> | ||
| 163 | <objectAnimator | ||
| 164 | android:duration="2269" | ||
| 165 | android:propertyName="translateX" | ||
| 166 | android:startOffset="0" | ||
| 167 | android:valueFrom="0" | ||
| 168 | android:valueTo="1" | ||
| 169 | android:valueType="floatType" /> | ||
| 170 | </set> | ||
| 171 | </aapt:attr> | ||
| 172 | </target> | ||
| 173 | </animated-vector> | ||
diff --git a/src/android/app/src/main/res/layout-ldrtl/list_item_setting_input.xml b/src/android/app/src/main/res/layout-ldrtl/list_item_setting_input.xml new file mode 100644 index 000000000..583620dc6 --- /dev/null +++ b/src/android/app/src/main/res/layout-ldrtl/list_item_setting_input.xml | |||
| @@ -0,0 +1,63 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| 4 | xmlns:tools="http://schemas.android.com/tools" | ||
| 5 | android:id="@+id/setting_body" | ||
| 6 | android:layout_width="match_parent" | ||
| 7 | android:layout_height="wrap_content" | ||
| 8 | android:background="?android:attr/selectableItemBackground" | ||
| 9 | android:clickable="true" | ||
| 10 | android:focusable="true" | ||
| 11 | android:gravity="center_vertical" | ||
| 12 | android:minHeight="72dp" | ||
| 13 | android:padding="16dp" | ||
| 14 | android:nextFocusLeft="@id/button_options"> | ||
| 15 | |||
| 16 | <LinearLayout | ||
| 17 | android:layout_width="match_parent" | ||
| 18 | android:layout_height="wrap_content" | ||
| 19 | android:gravity="center_vertical" | ||
| 20 | android:orientation="horizontal"> | ||
| 21 | |||
| 22 | <LinearLayout | ||
| 23 | android:layout_width="0dp" | ||
| 24 | android:layout_height="wrap_content" | ||
| 25 | android:orientation="vertical" | ||
| 26 | android:layout_weight="1"> | ||
| 27 | |||
| 28 | <com.google.android.material.textview.MaterialTextView | ||
| 29 | android:id="@+id/text_setting_name" | ||
| 30 | style="@style/TextAppearance.Material3.HeadlineMedium" | ||
| 31 | android:layout_width="match_parent" | ||
| 32 | android:layout_height="wrap_content" | ||
| 33 | android:textAlignment="viewStart" | ||
| 34 | android:textSize="17sp" | ||
| 35 | app:lineHeight="22dp" | ||
| 36 | tools:text="Setting Name" /> | ||
| 37 | |||
| 38 | <com.google.android.material.textview.MaterialTextView | ||
| 39 | android:id="@+id/text_setting_value" | ||
| 40 | style="@style/TextAppearance.Material3.LabelMedium" | ||
| 41 | android:layout_width="match_parent" | ||
| 42 | android:layout_height="wrap_content" | ||
| 43 | android:layout_marginTop="@dimen/spacing_small" | ||
| 44 | android:textAlignment="viewStart" | ||
| 45 | android:textStyle="bold" | ||
| 46 | android:textSize="13sp" | ||
| 47 | tools:text="1x" /> | ||
| 48 | |||
| 49 | </LinearLayout> | ||
| 50 | |||
| 51 | <Button | ||
| 52 | android:id="@+id/button_options" | ||
| 53 | style="?attr/materialIconButtonStyle" | ||
| 54 | android:layout_width="wrap_content" | ||
| 55 | android:layout_height="wrap_content" | ||
| 56 | android:nextFocusRight="@id/setting_body" | ||
| 57 | app:icon="@drawable/ic_more_vert" | ||
| 58 | app:iconSize="24dp" | ||
| 59 | app:iconTint="?attr/colorOnSurface" /> | ||
| 60 | |||
| 61 | </LinearLayout> | ||
| 62 | |||
| 63 | </RelativeLayout> | ||
diff --git a/src/android/app/src/main/res/layout/dialog_input_profiles.xml b/src/android/app/src/main/res/layout/dialog_input_profiles.xml new file mode 100644 index 000000000..6ad76fe41 --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_input_profiles.xml | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | android:id="@+id/list_profiles" | ||
| 4 | android:layout_width="match_parent" | ||
| 5 | android:layout_height="wrap_content" | ||
| 6 | android:fadeScrollbars="false" /> | ||
diff --git a/src/android/app/src/main/res/layout/dialog_mapping.xml b/src/android/app/src/main/res/layout/dialog_mapping.xml new file mode 100644 index 000000000..06190b8d2 --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_mapping.xml | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | android:layout_width="match_parent" | ||
| 4 | android:layout_height="wrap_content" | ||
| 5 | xmlns:tools="http://schemas.android.com/tools" | ||
| 6 | android:defaultFocusHighlightEnabled="false" | ||
| 7 | android:focusable="true" | ||
| 8 | android:focusableInTouchMode="true" | ||
| 9 | android:focusedByDefault="true" | ||
| 10 | android:orientation="horizontal" | ||
| 11 | android:gravity="center"> | ||
| 12 | |||
| 13 | <ImageView | ||
| 14 | android:id="@+id/image_stick_animation" | ||
| 15 | android:layout_width="@dimen/mapping_anim_size" | ||
| 16 | android:layout_height="@dimen/mapping_anim_size" | ||
| 17 | tools:src="@drawable/stick_two_direction_anim" /> | ||
| 18 | |||
| 19 | <ImageView | ||
| 20 | android:id="@+id/image_button_animation" | ||
| 21 | android:layout_width="@dimen/mapping_anim_size" | ||
| 22 | android:layout_height="@dimen/mapping_anim_size" | ||
| 23 | android:layout_marginStart="48dp" | ||
| 24 | tools:src="@drawable/button_anim" /> | ||
| 25 | |||
| 26 | </LinearLayout> | ||
diff --git a/src/android/app/src/main/res/layout/list_item_input_profile.xml b/src/android/app/src/main/res/layout/list_item_input_profile.xml new file mode 100644 index 000000000..a08dccf0c --- /dev/null +++ b/src/android/app/src/main/res/layout/list_item_input_profile.xml | |||
| @@ -0,0 +1,74 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| 4 | xmlns:tools="http://schemas.android.com/tools" | ||
| 5 | android:layout_width="match_parent" | ||
| 6 | android:layout_height="wrap_content" | ||
| 7 | android:focusable="false" | ||
| 8 | android:paddingHorizontal="20dp" | ||
| 9 | android:paddingVertical="16dp"> | ||
| 10 | |||
| 11 | <com.google.android.material.textview.MaterialTextView | ||
| 12 | android:id="@+id/title" | ||
| 13 | style="@style/TextAppearance.Material3.HeadlineMedium" | ||
| 14 | android:layout_width="0dp" | ||
| 15 | android:layout_height="0dp" | ||
| 16 | android:textAlignment="viewStart" | ||
| 17 | android:gravity="start|center_vertical" | ||
| 18 | android:textSize="17sp" | ||
| 19 | android:layout_marginEnd="16dp" | ||
| 20 | app:layout_constraintBottom_toBottomOf="@+id/button_layout" | ||
| 21 | app:layout_constraintEnd_toStartOf="@+id/button_layout" | ||
| 22 | app:layout_constraintStart_toStartOf="parent" | ||
| 23 | app:layout_constraintTop_toTopOf="parent" | ||
| 24 | app:lineHeight="28dp" | ||
| 25 | tools:text="My profile" /> | ||
| 26 | |||
| 27 | <LinearLayout | ||
| 28 | android:id="@+id/button_layout" | ||
| 29 | android:layout_width="wrap_content" | ||
| 30 | android:layout_height="wrap_content" | ||
| 31 | android:gravity="center_vertical" | ||
| 32 | android:orientation="horizontal" | ||
| 33 | app:layout_constraintEnd_toEndOf="parent" | ||
| 34 | app:layout_constraintTop_toTopOf="parent"> | ||
| 35 | |||
| 36 | <Button | ||
| 37 | android:id="@+id/button_new" | ||
| 38 | style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" | ||
| 39 | android:layout_width="wrap_content" | ||
| 40 | android:layout_height="wrap_content" | ||
| 41 | android:contentDescription="@string/create_new_profile" | ||
| 42 | android:tooltipText="@string/create_new_profile" | ||
| 43 | app:icon="@drawable/ic_new_label" /> | ||
| 44 | |||
| 45 | <Button | ||
| 46 | android:id="@+id/button_delete" | ||
| 47 | style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" | ||
| 48 | android:layout_width="wrap_content" | ||
| 49 | android:layout_height="wrap_content" | ||
| 50 | android:contentDescription="@string/delete" | ||
| 51 | android:tooltipText="@string/delete" | ||
| 52 | app:icon="@drawable/ic_delete" /> | ||
| 53 | |||
| 54 | <Button | ||
| 55 | android:id="@+id/button_save" | ||
| 56 | style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" | ||
| 57 | android:layout_width="wrap_content" | ||
| 58 | android:layout_height="wrap_content" | ||
| 59 | android:contentDescription="@string/save" | ||
| 60 | android:tooltipText="@string/save" | ||
| 61 | app:icon="@drawable/ic_save" /> | ||
| 62 | |||
| 63 | <Button | ||
| 64 | android:id="@+id/button_load" | ||
| 65 | style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" | ||
| 66 | android:layout_width="wrap_content" | ||
| 67 | android:layout_height="wrap_content" | ||
| 68 | android:contentDescription="@string/load" | ||
| 69 | android:tooltipText="@string/load" | ||
| 70 | app:icon="@drawable/ic_import" /> | ||
| 71 | |||
| 72 | </LinearLayout> | ||
| 73 | |||
| 74 | </androidx.constraintlayout.widget.ConstraintLayout> | ||
diff --git a/src/android/app/src/main/res/layout/list_item_setting_input.xml b/src/android/app/src/main/res/layout/list_item_setting_input.xml new file mode 100644 index 000000000..d67cbe245 --- /dev/null +++ b/src/android/app/src/main/res/layout/list_item_setting_input.xml | |||
| @@ -0,0 +1,63 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| 4 | xmlns:tools="http://schemas.android.com/tools" | ||
| 5 | android:id="@+id/setting_body" | ||
| 6 | android:layout_width="match_parent" | ||
| 7 | android:layout_height="wrap_content" | ||
| 8 | android:background="?android:attr/selectableItemBackground" | ||
| 9 | android:clickable="true" | ||
| 10 | android:focusable="true" | ||
| 11 | android:gravity="center_vertical" | ||
| 12 | android:minHeight="72dp" | ||
| 13 | android:padding="16dp" | ||
| 14 | android:nextFocusRight="@id/button_options"> | ||
| 15 | |||
| 16 | <LinearLayout | ||
| 17 | android:layout_width="match_parent" | ||
| 18 | android:layout_height="wrap_content" | ||
| 19 | android:gravity="center_vertical" | ||
| 20 | android:orientation="horizontal"> | ||
| 21 | |||
| 22 | <LinearLayout | ||
| 23 | android:layout_width="0dp" | ||
| 24 | android:layout_height="wrap_content" | ||
| 25 | android:orientation="vertical" | ||
| 26 | android:layout_weight="1"> | ||
| 27 | |||
| 28 | <com.google.android.material.textview.MaterialTextView | ||
| 29 | android:id="@+id/text_setting_name" | ||
| 30 | style="@style/TextAppearance.Material3.HeadlineMedium" | ||
| 31 | android:layout_width="match_parent" | ||
| 32 | android:layout_height="wrap_content" | ||
| 33 | android:textAlignment="viewStart" | ||
| 34 | android:textSize="17sp" | ||
| 35 | app:lineHeight="22dp" | ||
| 36 | tools:text="Setting Name" /> | ||
| 37 | |||
| 38 | <com.google.android.material.textview.MaterialTextView | ||
| 39 | android:id="@+id/text_setting_value" | ||
| 40 | style="@style/TextAppearance.Material3.LabelMedium" | ||
| 41 | android:layout_width="match_parent" | ||
| 42 | android:layout_height="wrap_content" | ||
| 43 | android:layout_marginTop="@dimen/spacing_small" | ||
| 44 | android:textAlignment="viewStart" | ||
| 45 | android:textStyle="bold" | ||
| 46 | android:textSize="13sp" | ||
| 47 | tools:text="1x" /> | ||
| 48 | |||
| 49 | </LinearLayout> | ||
| 50 | |||
| 51 | <Button | ||
| 52 | android:id="@+id/button_options" | ||
| 53 | style="?attr/materialIconButtonStyle" | ||
| 54 | android:layout_width="wrap_content" | ||
| 55 | android:layout_height="wrap_content" | ||
| 56 | android:nextFocusLeft="@id/setting_body" | ||
| 57 | app:icon="@drawable/ic_more_vert" | ||
| 58 | app:iconSize="24dp" | ||
| 59 | app:iconTint="?attr/colorOnSurface" /> | ||
| 60 | |||
| 61 | </LinearLayout> | ||
| 62 | |||
| 63 | </RelativeLayout> | ||
diff --git a/src/android/app/src/main/res/menu/menu_in_game.xml b/src/android/app/src/main/res/menu/menu_in_game.xml index eecb0563b..867197ebc 100644 --- a/src/android/app/src/main/res/menu/menu_in_game.xml +++ b/src/android/app/src/main/res/menu/menu_in_game.xml | |||
| @@ -17,8 +17,13 @@ | |||
| 17 | android:title="@string/per_game_settings" /> | 17 | android:title="@string/per_game_settings" /> |
| 18 | 18 | ||
| 19 | <item | 19 | <item |
| 20 | android:id="@+id/menu_overlay_controls" | 20 | android:id="@+id/menu_controls" |
| 21 | android:icon="@drawable/ic_controller" | 21 | android:icon="@drawable/ic_controller" |
| 22 | android:title="@string/preferences_controls" /> | ||
| 23 | |||
| 24 | <item | ||
| 25 | android:id="@+id/menu_overlay_controls" | ||
| 26 | android:icon="@drawable/ic_overlay" | ||
| 22 | android:title="@string/emulation_input_overlay" /> | 27 | android:title="@string/emulation_input_overlay" /> |
| 23 | 28 | ||
| 24 | <item | 29 | <item |
diff --git a/src/android/app/src/main/res/menu/menu_input_options.xml b/src/android/app/src/main/res/menu/menu_input_options.xml new file mode 100644 index 000000000..81ea5043f --- /dev/null +++ b/src/android/app/src/main/res/menu/menu_input_options.xml | |||
| @@ -0,0 +1,34 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <menu xmlns:android="http://schemas.android.com/apk/res/android"> | ||
| 3 | |||
| 4 | <item | ||
| 5 | android:id="@+id/invert_axis" | ||
| 6 | android:title="@string/invert_axis" | ||
| 7 | android:visible="false" /> | ||
| 8 | |||
| 9 | <item | ||
| 10 | android:id="@+id/invert_button" | ||
| 11 | android:title="@string/invert_button" | ||
| 12 | android:visible="false" /> | ||
| 13 | |||
| 14 | <item | ||
| 15 | android:id="@+id/toggle_button" | ||
| 16 | android:title="@string/toggle_button" | ||
| 17 | android:visible="false" /> | ||
| 18 | |||
| 19 | <item | ||
| 20 | android:id="@+id/turbo_button" | ||
| 21 | android:title="@string/turbo_button" | ||
| 22 | android:visible="false" /> | ||
| 23 | |||
| 24 | <item | ||
| 25 | android:id="@+id/set_threshold" | ||
| 26 | android:title="@string/set_threshold" | ||
| 27 | android:visible="false" /> | ||
| 28 | |||
| 29 | <item | ||
| 30 | android:id="@+id/toggle_axis" | ||
| 31 | android:title="@string/toggle_axis" | ||
| 32 | android:visible="false" /> | ||
| 33 | |||
| 34 | </menu> | ||
diff --git a/src/android/app/src/main/res/navigation/settings_navigation.xml b/src/android/app/src/main/res/navigation/settings_navigation.xml index 1d87d36b3..e4c66e7d5 100644 --- a/src/android/app/src/main/res/navigation/settings_navigation.xml +++ b/src/android/app/src/main/res/navigation/settings_navigation.xml | |||
| @@ -26,7 +26,7 @@ | |||
| 26 | 26 | ||
| 27 | <fragment | 27 | <fragment |
| 28 | android:id="@+id/settingsSearchFragment" | 28 | android:id="@+id/settingsSearchFragment" |
| 29 | android:name="org.yuzu.yuzu_emu.fragments.SettingsSearchFragment" | 29 | android:name="org.yuzu.yuzu_emu.features.settings.ui.SettingsSearchFragment" |
| 30 | android:label="SettingsSearchFragment" /> | 30 | android:label="SettingsSearchFragment" /> |
| 31 | 31 | ||
| 32 | </navigation> | 32 | </navigation> |
diff --git a/src/android/app/src/main/res/values-w600dp/dimens.xml b/src/android/app/src/main/res/values-w600dp/dimens.xml index 128319e27..0e2d40876 100644 --- a/src/android/app/src/main/res/values-w600dp/dimens.xml +++ b/src/android/app/src/main/res/values-w600dp/dimens.xml | |||
| @@ -2,4 +2,6 @@ | |||
| 2 | <resources> | 2 | <resources> |
| 3 | <dimen name="spacing_navigation">0dp</dimen> | 3 | <dimen name="spacing_navigation">0dp</dimen> |
| 4 | <dimen name="spacing_navigation_rail">80dp</dimen> | 4 | <dimen name="spacing_navigation_rail">80dp</dimen> |
| 5 | |||
| 6 | <dimen name="mapping_anim_size">100dp</dimen> | ||
| 5 | </resources> | 7 | </resources> |
diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml index 992b5ae44..bf733637f 100644 --- a/src/android/app/src/main/res/values/dimens.xml +++ b/src/android/app/src/main/res/values/dimens.xml | |||
| @@ -18,4 +18,6 @@ | |||
| 18 | 18 | ||
| 19 | <dimen name="dialog_margin">20dp</dimen> | 19 | <dimen name="dialog_margin">20dp</dimen> |
| 20 | <dimen name="elevated_app_bar">3dp</dimen> | 20 | <dimen name="elevated_app_bar">3dp</dimen> |
| 21 | |||
| 22 | <dimen name="mapping_anim_size">75dp</dimen> | ||
| 21 | </resources> | 23 | </resources> |
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 78a4c958a..6a631f664 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml | |||
| @@ -255,6 +255,92 @@ | |||
| 255 | <string name="audio_volume">Volume</string> | 255 | <string name="audio_volume">Volume</string> |
| 256 | <string name="audio_volume_description">Specifies the volume of audio output.</string> | 256 | <string name="audio_volume_description">Specifies the volume of audio output.</string> |
| 257 | 257 | ||
| 258 | <!-- Input strings --> | ||
| 259 | <string name="buttons">Buttons</string> | ||
| 260 | <string name="button_a">A</string> | ||
| 261 | <string name="button_b">B</string> | ||
| 262 | <string name="button_x">X</string> | ||
| 263 | <string name="button_y">Y</string> | ||
| 264 | <string name="button_plus">Plus</string> | ||
| 265 | <string name="button_minus">Minus</string> | ||
| 266 | <string name="button_home">Home</string> | ||
| 267 | <string name="button_capture">Capture</string> | ||
| 268 | <string name="start_pause">Start/Pause</string> | ||
| 269 | <string name="dpad">D-Pad</string> | ||
| 270 | <string name="up">Up</string> | ||
| 271 | <string name="down">Down</string> | ||
| 272 | <string name="left">Left</string> | ||
| 273 | <string name="right">Right</string> | ||
| 274 | <string name="left_stick">Left stick</string> | ||
| 275 | <string name="control_stick">Control stick</string> | ||
| 276 | <string name="right_stick">Right stick</string> | ||
| 277 | <string name="c_stick">C-Stick</string> | ||
| 278 | <string name="pressed">Pressed</string> | ||
| 279 | <string name="range">Range</string> | ||
| 280 | <string name="deadzone">Deadzone</string> | ||
| 281 | <string name="modifier">Modifier</string> | ||
| 282 | <string name="modifier_range">Modifier range</string> | ||
| 283 | <string name="triggers">Triggers</string> | ||
| 284 | <string name="button_l">L</string> | ||
| 285 | <string name="button_r">R</string> | ||
| 286 | <string name="button_zl">ZL</string> | ||
| 287 | <string name="button_zr">ZR</string> | ||
| 288 | <string name="button_sl_left">Left SL</string> | ||
| 289 | <string name="button_sr_left">Left SR</string> | ||
| 290 | <string name="button_sl_right">Right SL</string> | ||
| 291 | <string name="button_sr_right">Right SR</string> | ||
| 292 | <string name="button_z">Z</string> | ||
| 293 | <string name="invalid">Invalid</string> | ||
| 294 | <string name="not_set">Not set</string> | ||
| 295 | <string name="unknown">Unknown</string> | ||
| 296 | <string name="qualified_hat">%1$s%2$s%3$sHat %4$s</string> | ||
| 297 | <string name="qualified_button_stick_axis">%1$s%2$s%3$sAxis %4$s</string> | ||
| 298 | <string name="qualified_button">%1$s%2$s%3$sButton %4$s</string> | ||
| 299 | <string name="qualified_axis">Axis %1$s%2$s</string> | ||
| 300 | <string name="unused">Unused</string> | ||
| 301 | <string name="input_prompt">Move or press an input</string> | ||
| 302 | <string name="unsupported_input">Unsupported input type</string> | ||
| 303 | <string name="input_mapping_filter">Input mapping filter</string> | ||
| 304 | <string name="input_mapping_filter_description">Select a device to filter mapping inputs</string> | ||
| 305 | <string name="auto_map">Auto-map a controller</string> | ||
| 306 | <string name="auto_map_description">Select a device to attempt auto-mapping</string> | ||
| 307 | <string name="attempted_auto_map">Attempted auto-map with %1$s</string> | ||
| 308 | <string name="controller_type">Controller type</string> | ||
| 309 | <string name="pro_controller">Pro Controller</string> | ||
| 310 | <string name="handheld">Handheld</string> | ||
| 311 | <string name="dual_joycons">Dual Joycons</string> | ||
| 312 | <string name="left_joycon">Left Joycon</string> | ||
| 313 | <string name="right_joycon">Right Joycon</string> | ||
| 314 | <string name="gamecube_controller">GameCube Controller</string> | ||
| 315 | <string name="invert_axis">Invert axis</string> | ||
| 316 | <string name="invert_button">Invert button</string> | ||
| 317 | <string name="toggle_button">Toggle button</string> | ||
| 318 | <string name="turbo_button">Turbo button</string> | ||
| 319 | <string name="set_threshold">Set threshold</string> | ||
| 320 | <string name="toggle_axis">Toggle axis</string> | ||
| 321 | <string name="connected">Connected</string> | ||
| 322 | <string name="use_system_vibrator">Use system vibrator</string> | ||
| 323 | <string name="input_overlay">Input overlay</string> | ||
| 324 | <string name="vibration">Vibration</string> | ||
| 325 | <string name="vibration_strength">Vibration strength</string> | ||
| 326 | <string name="profile">Profile</string> | ||
| 327 | <string name="create_new_profile">Create new profile</string> | ||
| 328 | <string name="enter_profile_name">Enter profile name</string> | ||
| 329 | <string name="profile_name_already_exists">Profile name already exists</string> | ||
| 330 | <string name="invalid_profile_name">Invalid profile name</string> | ||
| 331 | <string name="use_global_input_configuration">Use global input configuration</string> | ||
| 332 | <string name="player_num_profile">Player %d profile</string> | ||
| 333 | <string name="delete_input_profile">Delete input profile</string> | ||
| 334 | <string name="delete_input_profile_description">Are you sure that you want to delete this profile? This is not recoverable.</string> | ||
| 335 | <string name="stick_map_description">Move a stick left and then up or press a button</string> | ||
| 336 | <string name="button_map_description">Press a button or move a trigger/stick</string> | ||
| 337 | <string name="map_dpad_direction">Map to D-Pad %1$s</string> | ||
| 338 | <string name="map_control">Map to %1$s</string> | ||
| 339 | <string name="failed_to_load_profile">Failed to load profile</string> | ||
| 340 | <string name="failed_to_save_profile">Failed to save profile</string> | ||
| 341 | <string name="reset_mapping">Reset mappings</string> | ||
| 342 | <string name="reset_mapping_description">Are you sure that you want to reset all mappings for this controller to default? This cannot be undone.</string> | ||
| 343 | |||
| 258 | <!-- Miscellaneous --> | 344 | <!-- Miscellaneous --> |
| 259 | <string name="slider_default">Default</string> | 345 | <string name="slider_default">Default</string> |
| 260 | <string name="ini_saved">Saved settings</string> | 346 | <string name="ini_saved">Saved settings</string> |
| @@ -292,6 +378,10 @@ | |||
| 292 | <string name="more_options">More options</string> | 378 | <string name="more_options">More options</string> |
| 293 | <string name="use_global_setting">Use global setting</string> | 379 | <string name="use_global_setting">Use global setting</string> |
| 294 | <string name="operation_completed_successfully">The operation completed successfully</string> | 380 | <string name="operation_completed_successfully">The operation completed successfully</string> |
| 381 | <string name="retry">Retry</string> | ||
| 382 | <string name="confirm">Confirm</string> | ||
| 383 | <string name="load">Load</string> | ||
| 384 | <string name="save">Save</string> | ||
| 295 | 385 | ||
| 296 | <!-- GPU driver installation --> | 386 | <!-- GPU driver installation --> |
| 297 | <string name="select_gpu_driver">Select GPU driver</string> | 387 | <string name="select_gpu_driver">Select GPU driver</string> |
| @@ -313,6 +403,9 @@ | |||
| 313 | <string name="preferences_graphics_description">Accuracy level, resolution, shader cache</string> | 403 | <string name="preferences_graphics_description">Accuracy level, resolution, shader cache</string> |
| 314 | <string name="preferences_audio">Audio</string> | 404 | <string name="preferences_audio">Audio</string> |
| 315 | <string name="preferences_audio_description">Output engine, volume</string> | 405 | <string name="preferences_audio_description">Output engine, volume</string> |
| 406 | <string name="preferences_controls">Controls</string> | ||
| 407 | <string name="preferences_controls_description">Map controller input</string> | ||
| 408 | <string name="preferences_player">Player %d</string> | ||
| 316 | <string name="preferences_theme">Theme and color</string> | 409 | <string name="preferences_theme">Theme and color</string> |
| 317 | <string name="preferences_debug">Debug</string> | 410 | <string name="preferences_debug">Debug</string> |
| 318 | <string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string> | 411 | <string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string> |
diff --git a/src/common/android/id_cache.cpp b/src/common/android/id_cache.cpp index f39262db9..1145cbdf2 100644 --- a/src/common/android/id_cache.cpp +++ b/src/common/android/id_cache.cpp | |||
| @@ -65,6 +65,30 @@ static jclass s_boolean_class; | |||
| 65 | static jmethodID s_boolean_constructor; | 65 | static jmethodID s_boolean_constructor; |
| 66 | static jfieldID s_boolean_value_field; | 66 | static jfieldID s_boolean_value_field; |
| 67 | 67 | ||
| 68 | static jclass s_player_input_class; | ||
| 69 | static jmethodID s_player_input_constructor; | ||
| 70 | static jfieldID s_player_input_connected_field; | ||
| 71 | static jfieldID s_player_input_buttons_field; | ||
| 72 | static jfieldID s_player_input_analogs_field; | ||
| 73 | static jfieldID s_player_input_motions_field; | ||
| 74 | static jfieldID s_player_input_vibration_enabled_field; | ||
| 75 | static jfieldID s_player_input_vibration_strength_field; | ||
| 76 | static jfieldID s_player_input_body_color_left_field; | ||
| 77 | static jfieldID s_player_input_body_color_right_field; | ||
| 78 | static jfieldID s_player_input_button_color_left_field; | ||
| 79 | static jfieldID s_player_input_button_color_right_field; | ||
| 80 | static jfieldID s_player_input_profile_name_field; | ||
| 81 | static jfieldID s_player_input_use_system_vibrator_field; | ||
| 82 | |||
| 83 | static jclass s_yuzu_input_device_interface; | ||
| 84 | static jmethodID s_yuzu_input_device_get_name; | ||
| 85 | static jmethodID s_yuzu_input_device_get_guid; | ||
| 86 | static jmethodID s_yuzu_input_device_get_port; | ||
| 87 | static jmethodID s_yuzu_input_device_get_supports_vibration; | ||
| 88 | static jmethodID s_yuzu_input_device_vibrate; | ||
| 89 | static jmethodID s_yuzu_input_device_get_axes; | ||
| 90 | static jmethodID s_yuzu_input_device_has_keys; | ||
| 91 | |||
| 68 | static constexpr jint JNI_VERSION = JNI_VERSION_1_6; | 92 | static constexpr jint JNI_VERSION = JNI_VERSION_1_6; |
| 69 | 93 | ||
| 70 | namespace Common::Android { | 94 | namespace Common::Android { |
| @@ -276,6 +300,94 @@ jfieldID GetBooleanValueField() { | |||
| 276 | return s_boolean_value_field; | 300 | return s_boolean_value_field; |
| 277 | } | 301 | } |
| 278 | 302 | ||
| 303 | jclass GetPlayerInputClass() { | ||
| 304 | return s_player_input_class; | ||
| 305 | } | ||
| 306 | |||
| 307 | jmethodID GetPlayerInputConstructor() { | ||
| 308 | return s_player_input_constructor; | ||
| 309 | } | ||
| 310 | |||
| 311 | jfieldID GetPlayerInputConnectedField() { | ||
| 312 | return s_player_input_connected_field; | ||
| 313 | } | ||
| 314 | |||
| 315 | jfieldID GetPlayerInputButtonsField() { | ||
| 316 | return s_player_input_buttons_field; | ||
| 317 | } | ||
| 318 | |||
| 319 | jfieldID GetPlayerInputAnalogsField() { | ||
| 320 | return s_player_input_analogs_field; | ||
| 321 | } | ||
| 322 | |||
| 323 | jfieldID GetPlayerInputMotionsField() { | ||
| 324 | return s_player_input_motions_field; | ||
| 325 | } | ||
| 326 | |||
| 327 | jfieldID GetPlayerInputVibrationEnabledField() { | ||
| 328 | return s_player_input_vibration_enabled_field; | ||
| 329 | } | ||
| 330 | |||
| 331 | jfieldID GetPlayerInputVibrationStrengthField() { | ||
| 332 | return s_player_input_vibration_strength_field; | ||
| 333 | } | ||
| 334 | |||
| 335 | jfieldID GetPlayerInputBodyColorLeftField() { | ||
| 336 | return s_player_input_body_color_left_field; | ||
| 337 | } | ||
| 338 | |||
| 339 | jfieldID GetPlayerInputBodyColorRightField() { | ||
| 340 | return s_player_input_body_color_right_field; | ||
| 341 | } | ||
| 342 | |||
| 343 | jfieldID GetPlayerInputButtonColorLeftField() { | ||
| 344 | return s_player_input_button_color_left_field; | ||
| 345 | } | ||
| 346 | |||
| 347 | jfieldID GetPlayerInputButtonColorRightField() { | ||
| 348 | return s_player_input_button_color_right_field; | ||
| 349 | } | ||
| 350 | |||
| 351 | jfieldID GetPlayerInputProfileNameField() { | ||
| 352 | return s_player_input_profile_name_field; | ||
| 353 | } | ||
| 354 | |||
| 355 | jfieldID GetPlayerInputUseSystemVibratorField() { | ||
| 356 | return s_player_input_use_system_vibrator_field; | ||
| 357 | } | ||
| 358 | |||
| 359 | jclass GetYuzuInputDeviceInterface() { | ||
| 360 | return s_yuzu_input_device_interface; | ||
| 361 | } | ||
| 362 | |||
| 363 | jmethodID GetYuzuDeviceGetName() { | ||
| 364 | return s_yuzu_input_device_get_name; | ||
| 365 | } | ||
| 366 | |||
| 367 | jmethodID GetYuzuDeviceGetGUID() { | ||
| 368 | return s_yuzu_input_device_get_guid; | ||
| 369 | } | ||
| 370 | |||
| 371 | jmethodID GetYuzuDeviceGetPort() { | ||
| 372 | return s_yuzu_input_device_get_port; | ||
| 373 | } | ||
| 374 | |||
| 375 | jmethodID GetYuzuDeviceGetSupportsVibration() { | ||
| 376 | return s_yuzu_input_device_get_supports_vibration; | ||
| 377 | } | ||
| 378 | |||
| 379 | jmethodID GetYuzuDeviceVibrate() { | ||
| 380 | return s_yuzu_input_device_vibrate; | ||
| 381 | } | ||
| 382 | |||
| 383 | jmethodID GetYuzuDeviceGetAxes() { | ||
| 384 | return s_yuzu_input_device_get_axes; | ||
| 385 | } | ||
| 386 | |||
| 387 | jmethodID GetYuzuDeviceHasKeys() { | ||
| 388 | return s_yuzu_input_device_has_keys; | ||
| 389 | } | ||
| 390 | |||
| 279 | #ifdef __cplusplus | 391 | #ifdef __cplusplus |
| 280 | extern "C" { | 392 | extern "C" { |
| 281 | #endif | 393 | #endif |
| @@ -387,6 +499,55 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { | |||
| 387 | s_boolean_value_field = env->GetFieldID(boolean_class, "value", "Z"); | 499 | s_boolean_value_field = env->GetFieldID(boolean_class, "value", "Z"); |
| 388 | env->DeleteLocalRef(boolean_class); | 500 | env->DeleteLocalRef(boolean_class); |
| 389 | 501 | ||
| 502 | const jclass player_input_class = | ||
| 503 | env->FindClass("org/yuzu/yuzu_emu/features/input/model/PlayerInput"); | ||
| 504 | s_player_input_class = reinterpret_cast<jclass>(env->NewGlobalRef(player_input_class)); | ||
| 505 | s_player_input_constructor = env->GetMethodID( | ||
| 506 | player_input_class, "<init>", | ||
| 507 | "(Z[Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;ZIJJJJLjava/lang/String;Z)V"); | ||
| 508 | s_player_input_connected_field = env->GetFieldID(player_input_class, "connected", "Z"); | ||
| 509 | s_player_input_buttons_field = | ||
| 510 | env->GetFieldID(player_input_class, "buttons", "[Ljava/lang/String;"); | ||
| 511 | s_player_input_analogs_field = | ||
| 512 | env->GetFieldID(player_input_class, "analogs", "[Ljava/lang/String;"); | ||
| 513 | s_player_input_motions_field = | ||
| 514 | env->GetFieldID(player_input_class, "motions", "[Ljava/lang/String;"); | ||
| 515 | s_player_input_vibration_enabled_field = | ||
| 516 | env->GetFieldID(player_input_class, "vibrationEnabled", "Z"); | ||
| 517 | s_player_input_vibration_strength_field = | ||
| 518 | env->GetFieldID(player_input_class, "vibrationStrength", "I"); | ||
| 519 | s_player_input_body_color_left_field = | ||
| 520 | env->GetFieldID(player_input_class, "bodyColorLeft", "J"); | ||
| 521 | s_player_input_body_color_right_field = | ||
| 522 | env->GetFieldID(player_input_class, "bodyColorRight", "J"); | ||
| 523 | s_player_input_button_color_left_field = | ||
| 524 | env->GetFieldID(player_input_class, "buttonColorLeft", "J"); | ||
| 525 | s_player_input_button_color_right_field = | ||
| 526 | env->GetFieldID(player_input_class, "buttonColorRight", "J"); | ||
| 527 | s_player_input_profile_name_field = | ||
| 528 | env->GetFieldID(player_input_class, "profileName", "Ljava/lang/String;"); | ||
| 529 | s_player_input_use_system_vibrator_field = | ||
| 530 | env->GetFieldID(player_input_class, "useSystemVibrator", "Z"); | ||
| 531 | env->DeleteLocalRef(player_input_class); | ||
| 532 | |||
| 533 | const jclass yuzu_input_device_interface = | ||
| 534 | env->FindClass("org/yuzu/yuzu_emu/features/input/YuzuInputDevice"); | ||
| 535 | s_yuzu_input_device_interface = | ||
| 536 | reinterpret_cast<jclass>(env->NewGlobalRef(yuzu_input_device_interface)); | ||
| 537 | s_yuzu_input_device_get_name = | ||
| 538 | env->GetMethodID(yuzu_input_device_interface, "getName", "()Ljava/lang/String;"); | ||
| 539 | s_yuzu_input_device_get_guid = | ||
| 540 | env->GetMethodID(yuzu_input_device_interface, "getGUID", "()Ljava/lang/String;"); | ||
| 541 | s_yuzu_input_device_get_port = env->GetMethodID(yuzu_input_device_interface, "getPort", "()I"); | ||
| 542 | s_yuzu_input_device_get_supports_vibration = | ||
| 543 | env->GetMethodID(yuzu_input_device_interface, "getSupportsVibration", "()Z"); | ||
| 544 | s_yuzu_input_device_vibrate = env->GetMethodID(yuzu_input_device_interface, "vibrate", "(F)V"); | ||
| 545 | s_yuzu_input_device_get_axes = | ||
| 546 | env->GetMethodID(yuzu_input_device_interface, "getAxes", "()[Ljava/lang/Integer;"); | ||
| 547 | s_yuzu_input_device_has_keys = | ||
| 548 | env->GetMethodID(yuzu_input_device_interface, "hasKeys", "([I)[Z"); | ||
| 549 | env->DeleteLocalRef(yuzu_input_device_interface); | ||
| 550 | |||
| 390 | // Initialize Android Storage | 551 | // Initialize Android Storage |
| 391 | Common::FS::Android::RegisterCallbacks(env, s_native_library_class); | 552 | Common::FS::Android::RegisterCallbacks(env, s_native_library_class); |
| 392 | 553 | ||
| @@ -416,6 +577,8 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) { | |||
| 416 | env->DeleteGlobalRef(s_double_class); | 577 | env->DeleteGlobalRef(s_double_class); |
| 417 | env->DeleteGlobalRef(s_integer_class); | 578 | env->DeleteGlobalRef(s_integer_class); |
| 418 | env->DeleteGlobalRef(s_boolean_class); | 579 | env->DeleteGlobalRef(s_boolean_class); |
| 580 | env->DeleteGlobalRef(s_player_input_class); | ||
| 581 | env->DeleteGlobalRef(s_yuzu_input_device_interface); | ||
| 419 | 582 | ||
| 420 | // UnInitialize applets | 583 | // UnInitialize applets |
| 421 | SoftwareKeyboard::CleanupJNI(env); | 584 | SoftwareKeyboard::CleanupJNI(env); |
diff --git a/src/common/android/id_cache.h b/src/common/android/id_cache.h index 47802f96c..cd2844dcc 100644 --- a/src/common/android/id_cache.h +++ b/src/common/android/id_cache.h | |||
| @@ -85,4 +85,28 @@ jclass GetBooleanClass(); | |||
| 85 | jmethodID GetBooleanConstructor(); | 85 | jmethodID GetBooleanConstructor(); |
| 86 | jfieldID GetBooleanValueField(); | 86 | jfieldID GetBooleanValueField(); |
| 87 | 87 | ||
| 88 | jclass GetPlayerInputClass(); | ||
| 89 | jmethodID GetPlayerInputConstructor(); | ||
| 90 | jfieldID GetPlayerInputConnectedField(); | ||
| 91 | jfieldID GetPlayerInputButtonsField(); | ||
| 92 | jfieldID GetPlayerInputAnalogsField(); | ||
| 93 | jfieldID GetPlayerInputMotionsField(); | ||
| 94 | jfieldID GetPlayerInputVibrationEnabledField(); | ||
| 95 | jfieldID GetPlayerInputVibrationStrengthField(); | ||
| 96 | jfieldID GetPlayerInputBodyColorLeftField(); | ||
| 97 | jfieldID GetPlayerInputBodyColorRightField(); | ||
| 98 | jfieldID GetPlayerInputButtonColorLeftField(); | ||
| 99 | jfieldID GetPlayerInputButtonColorRightField(); | ||
| 100 | jfieldID GetPlayerInputProfileNameField(); | ||
| 101 | jfieldID GetPlayerInputUseSystemVibratorField(); | ||
| 102 | |||
| 103 | jclass GetYuzuInputDeviceInterface(); | ||
| 104 | jmethodID GetYuzuDeviceGetName(); | ||
| 105 | jmethodID GetYuzuDeviceGetGUID(); | ||
| 106 | jmethodID GetYuzuDeviceGetPort(); | ||
| 107 | jmethodID GetYuzuDeviceGetSupportsVibration(); | ||
| 108 | jmethodID GetYuzuDeviceVibrate(); | ||
| 109 | jmethodID GetYuzuDeviceGetAxes(); | ||
| 110 | jmethodID GetYuzuDeviceHasKeys(); | ||
| 111 | |||
| 88 | } // namespace Common::Android | 112 | } // namespace Common::Android |
diff --git a/src/common/settings_input.h b/src/common/settings_input.h index 53a95ef8f..a99bb0892 100644 --- a/src/common/settings_input.h +++ b/src/common/settings_input.h | |||
| @@ -395,6 +395,10 @@ struct PlayerInput { | |||
| 395 | u32 button_color_left; | 395 | u32 button_color_left; |
| 396 | u32 button_color_right; | 396 | u32 button_color_right; |
| 397 | std::string profile_name; | 397 | std::string profile_name; |
| 398 | |||
| 399 | // This is meant to tell the Android frontend whether to use a device's built-in vibration | ||
| 400 | // motor or a controller's vibrations. | ||
| 401 | bool use_system_vibrator; | ||
| 398 | }; | 402 | }; |
| 399 | 403 | ||
| 400 | struct TouchscreenInput { | 404 | struct TouchscreenInput { |
diff --git a/src/hid_core/frontend/emulated_controller.cpp b/src/hid_core/frontend/emulated_controller.cpp index 8b5d0eec6..3fa06d188 100644 --- a/src/hid_core/frontend/emulated_controller.cpp +++ b/src/hid_core/frontend/emulated_controller.cpp | |||
| @@ -1285,9 +1285,7 @@ bool EmulatedController::SetVibration(DeviceIndex device_index, const VibrationV | |||
| 1285 | }; | 1285 | }; |
| 1286 | 1286 | ||
| 1287 | // Send vibrations to Android's input overlay | 1287 | // Send vibrations to Android's input overlay |
| 1288 | if (npad_id_type == NpadIdType::Handheld || npad_id_type == NpadIdType::Player1) { | 1288 | output_devices[4]->SetVibration(status); |
| 1289 | output_devices[4]->SetVibration(status); | ||
| 1290 | } | ||
| 1291 | 1289 | ||
| 1292 | return output_devices[index]->SetVibration(status) == Common::Input::DriverResult::Success; | 1290 | return output_devices[index]->SetVibration(status) == Common::Input::DriverResult::Success; |
| 1293 | } | 1291 | } |
diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt index d0a71a15b..d455323e0 100644 --- a/src/input_common/CMakeLists.txt +++ b/src/input_common/CMakeLists.txt | |||
| @@ -2,8 +2,6 @@ | |||
| 2 | # SPDX-License-Identifier: GPL-2.0-or-later | 2 | # SPDX-License-Identifier: GPL-2.0-or-later |
| 3 | 3 | ||
| 4 | add_library(input_common STATIC | 4 | add_library(input_common STATIC |
| 5 | drivers/android.cpp | ||
| 6 | drivers/android.h | ||
| 7 | drivers/camera.cpp | 5 | drivers/camera.cpp |
| 8 | drivers/camera.h | 6 | drivers/camera.h |
| 9 | drivers/keyboard.cpp | 7 | drivers/keyboard.cpp |
| @@ -94,3 +92,11 @@ target_link_libraries(input_common PUBLIC hid_core PRIVATE common Boost::headers | |||
| 94 | if (YUZU_USE_PRECOMPILED_HEADERS) | 92 | if (YUZU_USE_PRECOMPILED_HEADERS) |
| 95 | target_precompile_headers(input_common PRIVATE precompiled_headers.h) | 93 | target_precompile_headers(input_common PRIVATE precompiled_headers.h) |
| 96 | endif() | 94 | endif() |
| 95 | |||
| 96 | if (ANDROID) | ||
| 97 | target_sources(input_common PRIVATE | ||
| 98 | drivers/android.cpp | ||
| 99 | drivers/android.h | ||
| 100 | ) | ||
| 101 | target_link_libraries(input_common PRIVATE android) | ||
| 102 | endif() | ||
diff --git a/src/input_common/drivers/android.cpp b/src/input_common/drivers/android.cpp index b6a03fdc0..e859cc538 100644 --- a/src/input_common/drivers/android.cpp +++ b/src/input_common/drivers/android.cpp | |||
| @@ -1,30 +1,47 @@ | |||
| 1 | // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project | 1 | // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project |
| 2 | // SPDX-License-Identifier: GPL-3.0-or-later | 2 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 3 | 3 | ||
| 4 | #include <set> | ||
| 5 | #include <common/settings_input.h> | ||
| 6 | #include <jni.h> | ||
| 7 | #include "common/android/android_common.h" | ||
| 8 | #include "common/android/id_cache.h" | ||
| 4 | #include "input_common/drivers/android.h" | 9 | #include "input_common/drivers/android.h" |
| 5 | 10 | ||
| 6 | namespace InputCommon { | 11 | namespace InputCommon { |
| 7 | 12 | ||
| 8 | Android::Android(std::string input_engine_) : InputEngine(std::move(input_engine_)) {} | 13 | Android::Android(std::string input_engine_) : InputEngine(std::move(input_engine_)) {} |
| 9 | 14 | ||
| 10 | void Android::RegisterController(std::size_t controller_number) { | 15 | void Android::RegisterController(jobject j_input_device) { |
| 11 | PreSetController(GetIdentifier(controller_number)); | 16 | auto env = Common::Android::GetEnvForThread(); |
| 17 | const std::string guid = Common::Android::GetJString( | ||
| 18 | env, static_cast<jstring>( | ||
| 19 | env->CallObjectMethod(j_input_device, Common::Android::GetYuzuDeviceGetGUID()))); | ||
| 20 | const s32 port = env->CallIntMethod(j_input_device, Common::Android::GetYuzuDeviceGetPort()); | ||
| 21 | const auto identifier = GetIdentifier(guid, static_cast<size_t>(port)); | ||
| 22 | PreSetController(identifier); | ||
| 23 | |||
| 24 | if (input_devices.find(identifier) != input_devices.end()) { | ||
| 25 | env->DeleteGlobalRef(input_devices[identifier]); | ||
| 26 | } | ||
| 27 | auto new_device = env->NewGlobalRef(j_input_device); | ||
| 28 | input_devices[identifier] = new_device; | ||
| 12 | } | 29 | } |
| 13 | 30 | ||
| 14 | void Android::SetButtonState(std::size_t controller_number, int button_id, bool value) { | 31 | void Android::SetButtonState(std::string guid, size_t port, int button_id, bool value) { |
| 15 | const auto identifier = GetIdentifier(controller_number); | 32 | const auto identifier = GetIdentifier(guid, port); |
| 16 | SetButton(identifier, button_id, value); | 33 | SetButton(identifier, button_id, value); |
| 17 | } | 34 | } |
| 18 | 35 | ||
| 19 | void Android::SetAxisState(std::size_t controller_number, int axis_id, float value) { | 36 | void Android::SetAxisPosition(std::string guid, size_t port, int axis_id, float value) { |
| 20 | const auto identifier = GetIdentifier(controller_number); | 37 | const auto identifier = GetIdentifier(guid, port); |
| 21 | SetAxis(identifier, axis_id, value); | 38 | SetAxis(identifier, axis_id, value); |
| 22 | } | 39 | } |
| 23 | 40 | ||
| 24 | void Android::SetMotionState(std::size_t controller_number, u64 delta_timestamp, float gyro_x, | 41 | void Android::SetMotionState(std::string guid, size_t port, u64 delta_timestamp, float gyro_x, |
| 25 | float gyro_y, float gyro_z, float accel_x, float accel_y, | 42 | float gyro_y, float gyro_z, float accel_x, float accel_y, |
| 26 | float accel_z) { | 43 | float accel_z) { |
| 27 | const auto identifier = GetIdentifier(controller_number); | 44 | const auto identifier = GetIdentifier(guid, port); |
| 28 | const BasicMotion motion_data{ | 45 | const BasicMotion motion_data{ |
| 29 | .gyro_x = gyro_x, | 46 | .gyro_x = gyro_x, |
| 30 | .gyro_y = gyro_y, | 47 | .gyro_y = gyro_y, |
| @@ -37,10 +54,295 @@ void Android::SetMotionState(std::size_t controller_number, u64 delta_timestamp, | |||
| 37 | SetMotion(identifier, 0, motion_data); | 54 | SetMotion(identifier, 0, motion_data); |
| 38 | } | 55 | } |
| 39 | 56 | ||
| 40 | PadIdentifier Android::GetIdentifier(std::size_t controller_number) const { | 57 | Common::Input::DriverResult Android::SetVibration( |
| 58 | [[maybe_unused]] const PadIdentifier& identifier, | ||
| 59 | [[maybe_unused]] const Common::Input::VibrationStatus& vibration) { | ||
| 60 | auto device = input_devices.find(identifier); | ||
| 61 | if (device != input_devices.end()) { | ||
| 62 | Common::Android::RunJNIOnFiber<void>([&](JNIEnv* env) { | ||
| 63 | float average_intensity = | ||
| 64 | static_cast<float>((vibration.high_amplitude + vibration.low_amplitude) / 2.0); | ||
| 65 | env->CallVoidMethod(device->second, Common::Android::GetYuzuDeviceVibrate(), | ||
| 66 | average_intensity); | ||
| 67 | }); | ||
| 68 | return Common::Input::DriverResult::Success; | ||
| 69 | } | ||
| 70 | return Common::Input::DriverResult::NotSupported; | ||
| 71 | } | ||
| 72 | |||
| 73 | bool Android::IsVibrationEnabled([[maybe_unused]] const PadIdentifier& identifier) { | ||
| 74 | auto device = input_devices.find(identifier); | ||
| 75 | if (device != input_devices.end()) { | ||
| 76 | return Common::Android::RunJNIOnFiber<bool>([&](JNIEnv* env) { | ||
| 77 | return static_cast<bool>(env->CallBooleanMethod( | ||
| 78 | device->second, Common::Android::GetYuzuDeviceGetSupportsVibration())); | ||
| 79 | }); | ||
| 80 | } | ||
| 81 | return false; | ||
| 82 | } | ||
| 83 | |||
| 84 | std::vector<Common::ParamPackage> Android::GetInputDevices() const { | ||
| 85 | std::vector<Common::ParamPackage> devices; | ||
| 86 | auto env = Common::Android::GetEnvForThread(); | ||
| 87 | for (const auto& [key, value] : input_devices) { | ||
| 88 | auto name_object = static_cast<jstring>( | ||
| 89 | env->CallObjectMethod(value, Common::Android::GetYuzuDeviceGetName())); | ||
| 90 | const std::string name = | ||
| 91 | fmt::format("{} {}", Common::Android::GetJString(env, name_object), key.port); | ||
| 92 | devices.emplace_back(Common::ParamPackage{ | ||
| 93 | {"engine", GetEngineName()}, | ||
| 94 | {"display", std::move(name)}, | ||
| 95 | {"guid", key.guid.RawString()}, | ||
| 96 | {"port", std::to_string(key.port)}, | ||
| 97 | }); | ||
| 98 | } | ||
| 99 | return devices; | ||
| 100 | } | ||
| 101 | |||
| 102 | std::set<s32> Android::GetDeviceAxes(JNIEnv* env, jobject& j_device) const { | ||
| 103 | auto j_axes = static_cast<jobjectArray>( | ||
| 104 | env->CallObjectMethod(j_device, Common::Android::GetYuzuDeviceGetAxes())); | ||
| 105 | std::set<s32> axes; | ||
| 106 | for (int i = 0; i < env->GetArrayLength(j_axes); ++i) { | ||
| 107 | jobject axis = env->GetObjectArrayElement(j_axes, i); | ||
| 108 | axes.insert(env->GetIntField(axis, Common::Android::GetIntegerValueField())); | ||
| 109 | } | ||
| 110 | return axes; | ||
| 111 | } | ||
| 112 | |||
| 113 | Common::ParamPackage Android::BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x, | ||
| 114 | int axis_y) const { | ||
| 115 | Common::ParamPackage params; | ||
| 116 | params.Set("engine", GetEngineName()); | ||
| 117 | params.Set("port", static_cast<int>(identifier.port)); | ||
| 118 | params.Set("guid", identifier.guid.RawString()); | ||
| 119 | params.Set("axis_x", axis_x); | ||
| 120 | params.Set("axis_y", axis_y); | ||
| 121 | params.Set("offset_x", 0); | ||
| 122 | params.Set("offset_y", 0); | ||
| 123 | params.Set("invert_x", "+"); | ||
| 124 | |||
| 125 | // Invert Y-Axis by default | ||
| 126 | params.Set("invert_y", "-"); | ||
| 127 | return params; | ||
| 128 | } | ||
| 129 | |||
| 130 | Common::ParamPackage Android::BuildAnalogParamPackageForButton(PadIdentifier identifier, s32 axis, | ||
| 131 | bool invert) const { | ||
| 132 | Common::ParamPackage params{}; | ||
| 133 | params.Set("engine", GetEngineName()); | ||
| 134 | params.Set("port", static_cast<int>(identifier.port)); | ||
| 135 | params.Set("guid", identifier.guid.RawString()); | ||
| 136 | params.Set("axis", axis); | ||
| 137 | params.Set("threshold", "0.5"); | ||
| 138 | params.Set("invert", invert ? "-" : "+"); | ||
| 139 | return params; | ||
| 140 | } | ||
| 141 | |||
| 142 | Common::ParamPackage Android::BuildButtonParamPackageForButton(PadIdentifier identifier, | ||
| 143 | s32 button) const { | ||
| 144 | Common::ParamPackage params{}; | ||
| 145 | params.Set("engine", GetEngineName()); | ||
| 146 | params.Set("port", static_cast<int>(identifier.port)); | ||
| 147 | params.Set("guid", identifier.guid.RawString()); | ||
| 148 | params.Set("button", button); | ||
| 149 | return params; | ||
| 150 | } | ||
| 151 | |||
| 152 | bool Android::MatchVID(Common::UUID device, const std::vector<std::string>& vids) const { | ||
| 153 | for (size_t i = 0; i < vids.size(); ++i) { | ||
| 154 | auto fucker = device.RawString(); | ||
| 155 | if (fucker.find(vids[i]) != std::string::npos) { | ||
| 156 | return true; | ||
| 157 | } | ||
| 158 | } | ||
| 159 | return false; | ||
| 160 | } | ||
| 161 | |||
| 162 | AnalogMapping Android::GetAnalogMappingForDevice(const Common::ParamPackage& params) { | ||
| 163 | if (!params.Has("guid") || !params.Has("port")) { | ||
| 164 | return {}; | ||
| 165 | } | ||
| 166 | |||
| 167 | auto identifier = | ||
| 168 | GetIdentifier(params.Get("guid", ""), static_cast<size_t>(params.Get("port", 0))); | ||
| 169 | auto& j_device = input_devices[identifier]; | ||
| 170 | if (j_device == nullptr) { | ||
| 171 | return {}; | ||
| 172 | } | ||
| 173 | |||
| 174 | auto env = Common::Android::GetEnvForThread(); | ||
| 175 | std::set<s32> axes = GetDeviceAxes(env, j_device); | ||
| 176 | if (axes.size() == 0) { | ||
| 177 | return {}; | ||
| 178 | } | ||
| 179 | |||
| 180 | AnalogMapping mapping = {}; | ||
| 181 | if (axes.find(AXIS_X) != axes.end() && axes.find(AXIS_Y) != axes.end()) { | ||
| 182 | mapping.insert_or_assign(Settings::NativeAnalog::LStick, | ||
| 183 | BuildParamPackageForAnalog(identifier, AXIS_X, AXIS_Y)); | ||
| 184 | } | ||
| 185 | |||
| 186 | if (axes.find(AXIS_RX) != axes.end() && axes.find(AXIS_RY) != axes.end()) { | ||
| 187 | mapping.insert_or_assign(Settings::NativeAnalog::RStick, | ||
| 188 | BuildParamPackageForAnalog(identifier, AXIS_RX, AXIS_RY)); | ||
| 189 | } else if (axes.find(AXIS_Z) != axes.end() && axes.find(AXIS_RZ) != axes.end()) { | ||
| 190 | mapping.insert_or_assign(Settings::NativeAnalog::RStick, | ||
| 191 | BuildParamPackageForAnalog(identifier, AXIS_Z, AXIS_RZ)); | ||
| 192 | } | ||
| 193 | return mapping; | ||
| 194 | } | ||
| 195 | |||
| 196 | ButtonMapping Android::GetButtonMappingForDevice(const Common::ParamPackage& params) { | ||
| 197 | if (!params.Has("guid") || !params.Has("port")) { | ||
| 198 | return {}; | ||
| 199 | } | ||
| 200 | |||
| 201 | auto identifier = | ||
| 202 | GetIdentifier(params.Get("guid", ""), static_cast<size_t>(params.Get("port", 0))); | ||
| 203 | auto& j_device = input_devices[identifier]; | ||
| 204 | if (j_device == nullptr) { | ||
| 205 | return {}; | ||
| 206 | } | ||
| 207 | |||
| 208 | auto env = Common::Android::GetEnvForThread(); | ||
| 209 | jintArray j_keys = env->NewIntArray(static_cast<int>(keycode_ids.size())); | ||
| 210 | env->SetIntArrayRegion(j_keys, 0, static_cast<int>(keycode_ids.size()), keycode_ids.data()); | ||
| 211 | auto j_has_keys_object = static_cast<jbooleanArray>( | ||
| 212 | env->CallObjectMethod(j_device, Common::Android::GetYuzuDeviceHasKeys(), j_keys)); | ||
| 213 | jboolean isCopy = false; | ||
| 214 | jboolean* j_has_keys = env->GetBooleanArrayElements(j_has_keys_object, &isCopy); | ||
| 215 | |||
| 216 | std::set<s32> available_keys; | ||
| 217 | for (size_t i = 0; i < keycode_ids.size(); ++i) { | ||
| 218 | if (j_has_keys[i]) { | ||
| 219 | available_keys.insert(keycode_ids[i]); | ||
| 220 | } | ||
| 221 | } | ||
| 222 | |||
| 223 | // Some devices use axes instead of buttons for certain controls so we need all the axes here | ||
| 224 | std::set<s32> axes = GetDeviceAxes(env, j_device); | ||
| 225 | |||
| 226 | ButtonMapping mapping = {}; | ||
| 227 | if (axes.find(AXIS_HAT_X) != axes.end() && axes.find(AXIS_HAT_Y) != axes.end()) { | ||
| 228 | mapping.insert_or_assign(Settings::NativeButton::DUp, | ||
| 229 | BuildAnalogParamPackageForButton(identifier, AXIS_HAT_Y, true)); | ||
| 230 | mapping.insert_or_assign(Settings::NativeButton::DDown, | ||
| 231 | BuildAnalogParamPackageForButton(identifier, AXIS_HAT_Y, false)); | ||
| 232 | mapping.insert_or_assign(Settings::NativeButton::DLeft, | ||
| 233 | BuildAnalogParamPackageForButton(identifier, AXIS_HAT_X, true)); | ||
| 234 | mapping.insert_or_assign(Settings::NativeButton::DRight, | ||
| 235 | BuildAnalogParamPackageForButton(identifier, AXIS_HAT_X, false)); | ||
| 236 | } else if (available_keys.find(KEYCODE_DPAD_UP) != available_keys.end() && | ||
| 237 | available_keys.find(KEYCODE_DPAD_DOWN) != available_keys.end() && | ||
| 238 | available_keys.find(KEYCODE_DPAD_LEFT) != available_keys.end() && | ||
| 239 | available_keys.find(KEYCODE_DPAD_RIGHT) != available_keys.end()) { | ||
| 240 | mapping.insert_or_assign(Settings::NativeButton::DUp, | ||
| 241 | BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_UP)); | ||
| 242 | mapping.insert_or_assign(Settings::NativeButton::DDown, | ||
| 243 | BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_DOWN)); | ||
| 244 | mapping.insert_or_assign(Settings::NativeButton::DLeft, | ||
| 245 | BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_LEFT)); | ||
| 246 | mapping.insert_or_assign(Settings::NativeButton::DRight, | ||
| 247 | BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_RIGHT)); | ||
| 248 | } | ||
| 249 | |||
| 250 | if (axes.find(AXIS_LTRIGGER) != axes.end()) { | ||
| 251 | mapping.insert_or_assign(Settings::NativeButton::ZL, BuildAnalogParamPackageForButton( | ||
| 252 | identifier, AXIS_LTRIGGER, false)); | ||
| 253 | } else if (available_keys.find(KEYCODE_BUTTON_L2) != available_keys.end()) { | ||
| 254 | mapping.insert_or_assign(Settings::NativeButton::ZL, | ||
| 255 | BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_L2)); | ||
| 256 | } | ||
| 257 | |||
| 258 | if (axes.find(AXIS_RTRIGGER) != axes.end()) { | ||
| 259 | mapping.insert_or_assign(Settings::NativeButton::ZR, BuildAnalogParamPackageForButton( | ||
| 260 | identifier, AXIS_RTRIGGER, false)); | ||
| 261 | } else if (available_keys.find(KEYCODE_BUTTON_R2) != available_keys.end()) { | ||
| 262 | mapping.insert_or_assign(Settings::NativeButton::ZR, | ||
| 263 | BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_R2)); | ||
| 264 | } | ||
| 265 | |||
| 266 | if (available_keys.find(KEYCODE_BUTTON_A) != available_keys.end()) { | ||
| 267 | if (MatchVID(identifier.guid, flipped_ab_vids)) { | ||
| 268 | mapping.insert_or_assign(Settings::NativeButton::B, BuildButtonParamPackageForButton( | ||
| 269 | identifier, KEYCODE_BUTTON_A)); | ||
| 270 | } else { | ||
| 271 | mapping.insert_or_assign(Settings::NativeButton::A, BuildButtonParamPackageForButton( | ||
| 272 | identifier, KEYCODE_BUTTON_A)); | ||
| 273 | } | ||
| 274 | } | ||
| 275 | if (available_keys.find(KEYCODE_BUTTON_B) != available_keys.end()) { | ||
| 276 | if (MatchVID(identifier.guid, flipped_ab_vids)) { | ||
| 277 | mapping.insert_or_assign(Settings::NativeButton::A, BuildButtonParamPackageForButton( | ||
| 278 | identifier, KEYCODE_BUTTON_B)); | ||
| 279 | } else { | ||
| 280 | mapping.insert_or_assign(Settings::NativeButton::B, BuildButtonParamPackageForButton( | ||
| 281 | identifier, KEYCODE_BUTTON_B)); | ||
| 282 | } | ||
| 283 | } | ||
| 284 | if (available_keys.find(KEYCODE_BUTTON_X) != available_keys.end()) { | ||
| 285 | if (MatchVID(identifier.guid, flipped_xy_vids)) { | ||
| 286 | mapping.insert_or_assign(Settings::NativeButton::Y, BuildButtonParamPackageForButton( | ||
| 287 | identifier, KEYCODE_BUTTON_X)); | ||
| 288 | } else { | ||
| 289 | mapping.insert_or_assign(Settings::NativeButton::X, BuildButtonParamPackageForButton( | ||
| 290 | identifier, KEYCODE_BUTTON_X)); | ||
| 291 | } | ||
| 292 | } | ||
| 293 | if (available_keys.find(KEYCODE_BUTTON_Y) != available_keys.end()) { | ||
| 294 | if (MatchVID(identifier.guid, flipped_xy_vids)) { | ||
| 295 | mapping.insert_or_assign(Settings::NativeButton::X, BuildButtonParamPackageForButton( | ||
| 296 | identifier, KEYCODE_BUTTON_Y)); | ||
| 297 | } else { | ||
| 298 | mapping.insert_or_assign(Settings::NativeButton::Y, BuildButtonParamPackageForButton( | ||
| 299 | identifier, KEYCODE_BUTTON_Y)); | ||
| 300 | } | ||
| 301 | } | ||
| 302 | |||
| 303 | if (available_keys.find(KEYCODE_BUTTON_L1) != available_keys.end()) { | ||
| 304 | mapping.insert_or_assign(Settings::NativeButton::L, | ||
| 305 | BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_L1)); | ||
| 306 | } | ||
| 307 | if (available_keys.find(KEYCODE_BUTTON_R1) != available_keys.end()) { | ||
| 308 | mapping.insert_or_assign(Settings::NativeButton::R, | ||
| 309 | BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_R1)); | ||
| 310 | } | ||
| 311 | |||
| 312 | if (available_keys.find(KEYCODE_BUTTON_THUMBL) != available_keys.end()) { | ||
| 313 | mapping.insert_or_assign( | ||
| 314 | Settings::NativeButton::LStick, | ||
| 315 | BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_THUMBL)); | ||
| 316 | } | ||
| 317 | if (available_keys.find(KEYCODE_BUTTON_THUMBR) != available_keys.end()) { | ||
| 318 | mapping.insert_or_assign( | ||
| 319 | Settings::NativeButton::RStick, | ||
| 320 | BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_THUMBR)); | ||
| 321 | } | ||
| 322 | |||
| 323 | if (available_keys.find(KEYCODE_BUTTON_START) != available_keys.end()) { | ||
| 324 | mapping.insert_or_assign( | ||
| 325 | Settings::NativeButton::Plus, | ||
| 326 | BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_START)); | ||
| 327 | } | ||
| 328 | if (available_keys.find(KEYCODE_BUTTON_SELECT) != available_keys.end()) { | ||
| 329 | mapping.insert_or_assign( | ||
| 330 | Settings::NativeButton::Minus, | ||
| 331 | BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_SELECT)); | ||
| 332 | } | ||
| 333 | |||
| 334 | return mapping; | ||
| 335 | } | ||
| 336 | |||
| 337 | Common::Input::ButtonNames Android::GetUIName( | ||
| 338 | [[maybe_unused]] const Common::ParamPackage& params) const { | ||
| 339 | return Common::Input::ButtonNames::Value; | ||
| 340 | } | ||
| 341 | |||
| 342 | PadIdentifier Android::GetIdentifier(const std::string& guid, size_t port) const { | ||
| 41 | return { | 343 | return { |
| 42 | .guid = Common::UUID{}, | 344 | .guid = Common::UUID{guid}, |
| 43 | .port = controller_number, | 345 | .port = port, |
| 44 | .pad = 0, | 346 | .pad = 0, |
| 45 | }; | 347 | }; |
| 46 | } | 348 | } |
diff --git a/src/input_common/drivers/android.h b/src/input_common/drivers/android.h index 3f01817f6..ac60e3598 100644 --- a/src/input_common/drivers/android.h +++ b/src/input_common/drivers/android.h | |||
| @@ -3,6 +3,8 @@ | |||
| 3 | 3 | ||
| 4 | #pragma once | 4 | #pragma once |
| 5 | 5 | ||
| 6 | #include <set> | ||
| 7 | #include <jni.h> | ||
| 6 | #include "input_common/input_engine.h" | 8 | #include "input_common/input_engine.h" |
| 7 | 9 | ||
| 8 | namespace InputCommon { | 10 | namespace InputCommon { |
| @@ -15,40 +17,121 @@ public: | |||
| 15 | explicit Android(std::string input_engine_); | 17 | explicit Android(std::string input_engine_); |
| 16 | 18 | ||
| 17 | /** | 19 | /** |
| 18 | * Registers controller number to accept new inputs | 20 | * Registers controller number to accept new inputs. |
| 19 | * @param controller_number the controller number that will take this action | 21 | * @param j_input_device YuzuInputDevice object from the Android frontend to register. |
| 20 | */ | 22 | */ |
| 21 | void RegisterController(std::size_t controller_number); | 23 | void RegisterController(jobject j_input_device); |
| 22 | 24 | ||
| 23 | /** | 25 | /** |
| 24 | * Sets the status of all buttons bound with the key to pressed | 26 | * Sets the status of a button on a specific controller. |
| 25 | * @param controller_number the controller number that will take this action | 27 | * @param guid 32 character hexadecimal string consisting of the controller's PID+VID. |
| 26 | * @param button_id the id of the button | 28 | * @param port Port determined by controller connection order. |
| 27 | * @param value indicates if the button is pressed or not | 29 | * @param button_id The Android Keycode corresponding to this event. |
| 30 | * @param value Whether the button is pressed or not. | ||
| 28 | */ | 31 | */ |
| 29 | void SetButtonState(std::size_t controller_number, int button_id, bool value); | 32 | void SetButtonState(std::string guid, size_t port, int button_id, bool value); |
| 30 | 33 | ||
| 31 | /** | 34 | /** |
| 32 | * Sets the status of a analog input to a specific player index | 35 | * Sets the status of an axis on a specific controller. |
| 33 | * @param controller_number the controller number that will take this action | 36 | * @param guid 32 character hexadecimal string consisting of the controller's PID+VID. |
| 34 | * @param axis_id the id of the axis to move | 37 | * @param port Port determined by controller connection order. |
| 35 | * @param value the analog position of the axis | 38 | * @param axis_id The Android axis ID corresponding to this event. |
| 39 | * @param value Value along the given axis. | ||
| 36 | */ | 40 | */ |
| 37 | void SetAxisState(std::size_t controller_number, int axis_id, float value); | 41 | void SetAxisPosition(std::string guid, size_t port, int axis_id, float value); |
| 38 | 42 | ||
| 39 | /** | 43 | /** |
| 40 | * Sets the status of the motion sensor to a specific player index | 44 | * Sets the status of the motion sensor on a specific controller |
| 41 | * @param controller_number the controller number that will take this action | 45 | * @param guid 32 character hexadecimal string consisting of the controller's PID+VID. |
| 42 | * @param delta_timestamp time passed since last reading | 46 | * @param port Port determined by controller connection order. |
| 43 | * @param gyro_x,gyro_y,gyro_z the gyro sensor readings | 47 | * @param delta_timestamp Time passed since the last read. |
| 44 | * @param accel_x,accel_y,accel_z the accelerometer reading | 48 | * @param gyro_x,gyro_y,gyro_z Gyro sensor readings. |
| 49 | * @param accel_x,accel_y,accel_z Accelerometer sensor readings. | ||
| 45 | */ | 50 | */ |
| 46 | void SetMotionState(std::size_t controller_number, u64 delta_timestamp, float gyro_x, | 51 | void SetMotionState(std::string guid, size_t port, u64 delta_timestamp, float gyro_x, |
| 47 | float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z); | 52 | float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z); |
| 48 | 53 | ||
| 54 | Common::Input::DriverResult SetVibration( | ||
| 55 | const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override; | ||
| 56 | |||
| 57 | bool IsVibrationEnabled(const PadIdentifier& identifier) override; | ||
| 58 | |||
| 59 | std::vector<Common::ParamPackage> GetInputDevices() const override; | ||
| 60 | |||
| 61 | /** | ||
| 62 | * Gets the axes reported by the YuzuInputDevice. | ||
| 63 | * @param env JNI environment pointer. | ||
| 64 | * @param j_device YuzuInputDevice from the Android frontend. | ||
| 65 | * @return Set of the axes reported by the underlying Android InputDevice | ||
| 66 | */ | ||
| 67 | std::set<s32> GetDeviceAxes(JNIEnv* env, jobject& j_device) const; | ||
| 68 | |||
| 69 | Common::ParamPackage BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x, | ||
| 70 | int axis_y) const; | ||
| 71 | |||
| 72 | Common::ParamPackage BuildAnalogParamPackageForButton(PadIdentifier identifier, s32 axis, | ||
| 73 | bool invert) const; | ||
| 74 | |||
| 75 | Common::ParamPackage BuildButtonParamPackageForButton(PadIdentifier identifier, | ||
| 76 | s32 button) const; | ||
| 77 | |||
| 78 | bool MatchVID(Common::UUID device, const std::vector<std::string>& vids) const; | ||
| 79 | |||
| 80 | AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override; | ||
| 81 | |||
| 82 | ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override; | ||
| 83 | |||
| 84 | Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override; | ||
| 85 | |||
| 49 | private: | 86 | private: |
| 87 | std::unordered_map<PadIdentifier, jobject> input_devices; | ||
| 88 | |||
| 50 | /// Returns the correct identifier corresponding to the player index | 89 | /// Returns the correct identifier corresponding to the player index |
| 51 | PadIdentifier GetIdentifier(std::size_t controller_number) const; | 90 | PadIdentifier GetIdentifier(const std::string& guid, size_t port) const; |
| 91 | |||
| 92 | static constexpr s32 AXIS_X = 0; | ||
| 93 | static constexpr s32 AXIS_Y = 1; | ||
| 94 | static constexpr s32 AXIS_Z = 11; | ||
| 95 | static constexpr s32 AXIS_RX = 12; | ||
| 96 | static constexpr s32 AXIS_RY = 13; | ||
| 97 | static constexpr s32 AXIS_RZ = 14; | ||
| 98 | static constexpr s32 AXIS_HAT_X = 15; | ||
| 99 | static constexpr s32 AXIS_HAT_Y = 16; | ||
| 100 | static constexpr s32 AXIS_LTRIGGER = 17; | ||
| 101 | static constexpr s32 AXIS_RTRIGGER = 18; | ||
| 102 | |||
| 103 | static constexpr s32 KEYCODE_DPAD_UP = 19; | ||
| 104 | static constexpr s32 KEYCODE_DPAD_DOWN = 20; | ||
| 105 | static constexpr s32 KEYCODE_DPAD_LEFT = 21; | ||
| 106 | static constexpr s32 KEYCODE_DPAD_RIGHT = 22; | ||
| 107 | static constexpr s32 KEYCODE_BUTTON_A = 96; | ||
| 108 | static constexpr s32 KEYCODE_BUTTON_B = 97; | ||
| 109 | static constexpr s32 KEYCODE_BUTTON_X = 99; | ||
| 110 | static constexpr s32 KEYCODE_BUTTON_Y = 100; | ||
| 111 | static constexpr s32 KEYCODE_BUTTON_L1 = 102; | ||
| 112 | static constexpr s32 KEYCODE_BUTTON_R1 = 103; | ||
| 113 | static constexpr s32 KEYCODE_BUTTON_L2 = 104; | ||
| 114 | static constexpr s32 KEYCODE_BUTTON_R2 = 105; | ||
| 115 | static constexpr s32 KEYCODE_BUTTON_THUMBL = 106; | ||
| 116 | static constexpr s32 KEYCODE_BUTTON_THUMBR = 107; | ||
| 117 | static constexpr s32 KEYCODE_BUTTON_START = 108; | ||
| 118 | static constexpr s32 KEYCODE_BUTTON_SELECT = 109; | ||
| 119 | const std::vector<s32> keycode_ids{ | ||
| 120 | KEYCODE_DPAD_UP, KEYCODE_DPAD_DOWN, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_RIGHT, | ||
| 121 | KEYCODE_BUTTON_A, KEYCODE_BUTTON_B, KEYCODE_BUTTON_X, KEYCODE_BUTTON_Y, | ||
| 122 | KEYCODE_BUTTON_L1, KEYCODE_BUTTON_R1, KEYCODE_BUTTON_L2, KEYCODE_BUTTON_R2, | ||
| 123 | KEYCODE_BUTTON_THUMBL, KEYCODE_BUTTON_THUMBR, KEYCODE_BUTTON_START, KEYCODE_BUTTON_SELECT, | ||
| 124 | }; | ||
| 125 | |||
| 126 | const std::string sony_vid{"054c"}; | ||
| 127 | const std::string nintendo_vid{"057e"}; | ||
| 128 | const std::string razer_vid{"1532"}; | ||
| 129 | const std::string redmagic_vid{"3537"}; | ||
| 130 | const std::string backbone_labs_vid{"358a"}; | ||
| 131 | const std::vector<std::string> flipped_ab_vids{sony_vid, nintendo_vid, razer_vid, redmagic_vid, | ||
| 132 | backbone_labs_vid}; | ||
| 133 | const std::vector<std::string> flipped_xy_vids{sony_vid, razer_vid, redmagic_vid, | ||
| 134 | backbone_labs_vid}; | ||
| 52 | }; | 135 | }; |
| 53 | 136 | ||
| 54 | } // namespace InputCommon | 137 | } // namespace InputCommon |
diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp index f8749ebbf..62a7ae40f 100644 --- a/src/input_common/main.cpp +++ b/src/input_common/main.cpp | |||
| @@ -4,7 +4,6 @@ | |||
| 4 | #include <memory> | 4 | #include <memory> |
| 5 | #include "common/input.h" | 5 | #include "common/input.h" |
| 6 | #include "common/param_package.h" | 6 | #include "common/param_package.h" |
| 7 | #include "input_common/drivers/android.h" | ||
| 8 | #include "input_common/drivers/camera.h" | 7 | #include "input_common/drivers/camera.h" |
| 9 | #include "input_common/drivers/keyboard.h" | 8 | #include "input_common/drivers/keyboard.h" |
| 10 | #include "input_common/drivers/mouse.h" | 9 | #include "input_common/drivers/mouse.h" |
| @@ -28,6 +27,10 @@ | |||
| 28 | #include "input_common/drivers/sdl_driver.h" | 27 | #include "input_common/drivers/sdl_driver.h" |
| 29 | #endif | 28 | #endif |
| 30 | 29 | ||
| 30 | #ifdef ANDROID | ||
| 31 | #include "input_common/drivers/android.h" | ||
| 32 | #endif | ||
| 33 | |||
| 31 | namespace InputCommon { | 34 | namespace InputCommon { |
| 32 | 35 | ||
| 33 | /// Dummy engine to get periodic updates | 36 | /// Dummy engine to get periodic updates |
| @@ -79,7 +82,9 @@ struct InputSubsystem::Impl { | |||
| 79 | RegisterEngine("cemuhookudp", udp_client); | 82 | RegisterEngine("cemuhookudp", udp_client); |
| 80 | RegisterEngine("tas", tas_input); | 83 | RegisterEngine("tas", tas_input); |
| 81 | RegisterEngine("camera", camera); | 84 | RegisterEngine("camera", camera); |
| 85 | #ifdef ANDROID | ||
| 82 | RegisterEngine("android", android); | 86 | RegisterEngine("android", android); |
| 87 | #endif | ||
| 83 | RegisterEngine("virtual_amiibo", virtual_amiibo); | 88 | RegisterEngine("virtual_amiibo", virtual_amiibo); |
| 84 | RegisterEngine("virtual_gamepad", virtual_gamepad); | 89 | RegisterEngine("virtual_gamepad", virtual_gamepad); |
| 85 | #ifdef HAVE_SDL2 | 90 | #ifdef HAVE_SDL2 |
| @@ -111,7 +116,9 @@ struct InputSubsystem::Impl { | |||
| 111 | UnregisterEngine(udp_client); | 116 | UnregisterEngine(udp_client); |
| 112 | UnregisterEngine(tas_input); | 117 | UnregisterEngine(tas_input); |
| 113 | UnregisterEngine(camera); | 118 | UnregisterEngine(camera); |
| 119 | #ifdef ANDROID | ||
| 114 | UnregisterEngine(android); | 120 | UnregisterEngine(android); |
| 121 | #endif | ||
| 115 | UnregisterEngine(virtual_amiibo); | 122 | UnregisterEngine(virtual_amiibo); |
| 116 | UnregisterEngine(virtual_gamepad); | 123 | UnregisterEngine(virtual_gamepad); |
| 117 | #ifdef HAVE_SDL2 | 124 | #ifdef HAVE_SDL2 |
| @@ -128,12 +135,16 @@ struct InputSubsystem::Impl { | |||
| 128 | Common::ParamPackage{{"display", "Any"}, {"engine", "any"}}, | 135 | Common::ParamPackage{{"display", "Any"}, {"engine", "any"}}, |
| 129 | }; | 136 | }; |
| 130 | 137 | ||
| 138 | #ifndef ANDROID | ||
| 131 | auto keyboard_devices = keyboard->GetInputDevices(); | 139 | auto keyboard_devices = keyboard->GetInputDevices(); |
| 132 | devices.insert(devices.end(), keyboard_devices.begin(), keyboard_devices.end()); | 140 | devices.insert(devices.end(), keyboard_devices.begin(), keyboard_devices.end()); |
| 133 | auto mouse_devices = mouse->GetInputDevices(); | 141 | auto mouse_devices = mouse->GetInputDevices(); |
| 134 | devices.insert(devices.end(), mouse_devices.begin(), mouse_devices.end()); | 142 | devices.insert(devices.end(), mouse_devices.begin(), mouse_devices.end()); |
| 143 | #endif | ||
| 144 | #ifdef ANDROID | ||
| 135 | auto android_devices = android->GetInputDevices(); | 145 | auto android_devices = android->GetInputDevices(); |
| 136 | devices.insert(devices.end(), android_devices.begin(), android_devices.end()); | 146 | devices.insert(devices.end(), android_devices.begin(), android_devices.end()); |
| 147 | #endif | ||
| 137 | #ifdef HAVE_LIBUSB | 148 | #ifdef HAVE_LIBUSB |
| 138 | auto gcadapter_devices = gcadapter->GetInputDevices(); | 149 | auto gcadapter_devices = gcadapter->GetInputDevices(); |
| 139 | devices.insert(devices.end(), gcadapter_devices.begin(), gcadapter_devices.end()); | 150 | devices.insert(devices.end(), gcadapter_devices.begin(), gcadapter_devices.end()); |
| @@ -162,9 +173,11 @@ struct InputSubsystem::Impl { | |||
| 162 | if (engine == mouse->GetEngineName()) { | 173 | if (engine == mouse->GetEngineName()) { |
| 163 | return mouse; | 174 | return mouse; |
| 164 | } | 175 | } |
| 176 | #ifdef ANDROID | ||
| 165 | if (engine == android->GetEngineName()) { | 177 | if (engine == android->GetEngineName()) { |
| 166 | return android; | 178 | return android; |
| 167 | } | 179 | } |
| 180 | #endif | ||
| 168 | #ifdef HAVE_LIBUSB | 181 | #ifdef HAVE_LIBUSB |
| 169 | if (engine == gcadapter->GetEngineName()) { | 182 | if (engine == gcadapter->GetEngineName()) { |
| 170 | return gcadapter; | 183 | return gcadapter; |
| @@ -245,9 +258,11 @@ struct InputSubsystem::Impl { | |||
| 245 | if (engine == mouse->GetEngineName()) { | 258 | if (engine == mouse->GetEngineName()) { |
| 246 | return true; | 259 | return true; |
| 247 | } | 260 | } |
| 261 | #ifdef ANDROID | ||
| 248 | if (engine == android->GetEngineName()) { | 262 | if (engine == android->GetEngineName()) { |
| 249 | return true; | 263 | return true; |
| 250 | } | 264 | } |
| 265 | #endif | ||
| 251 | #ifdef HAVE_LIBUSB | 266 | #ifdef HAVE_LIBUSB |
| 252 | if (engine == gcadapter->GetEngineName()) { | 267 | if (engine == gcadapter->GetEngineName()) { |
| 253 | return true; | 268 | return true; |
| @@ -276,7 +291,9 @@ struct InputSubsystem::Impl { | |||
| 276 | void BeginConfiguration() { | 291 | void BeginConfiguration() { |
| 277 | keyboard->BeginConfiguration(); | 292 | keyboard->BeginConfiguration(); |
| 278 | mouse->BeginConfiguration(); | 293 | mouse->BeginConfiguration(); |
| 294 | #ifdef ANDROID | ||
| 279 | android->BeginConfiguration(); | 295 | android->BeginConfiguration(); |
| 296 | #endif | ||
| 280 | #ifdef HAVE_LIBUSB | 297 | #ifdef HAVE_LIBUSB |
| 281 | gcadapter->BeginConfiguration(); | 298 | gcadapter->BeginConfiguration(); |
| 282 | #endif | 299 | #endif |
| @@ -290,7 +307,9 @@ struct InputSubsystem::Impl { | |||
| 290 | void EndConfiguration() { | 307 | void EndConfiguration() { |
| 291 | keyboard->EndConfiguration(); | 308 | keyboard->EndConfiguration(); |
| 292 | mouse->EndConfiguration(); | 309 | mouse->EndConfiguration(); |
| 310 | #ifdef ANDROID | ||
| 293 | android->EndConfiguration(); | 311 | android->EndConfiguration(); |
| 312 | #endif | ||
| 294 | #ifdef HAVE_LIBUSB | 313 | #ifdef HAVE_LIBUSB |
| 295 | gcadapter->EndConfiguration(); | 314 | gcadapter->EndConfiguration(); |
| 296 | #endif | 315 | #endif |
| @@ -321,7 +340,6 @@ struct InputSubsystem::Impl { | |||
| 321 | std::shared_ptr<TasInput::Tas> tas_input; | 340 | std::shared_ptr<TasInput::Tas> tas_input; |
| 322 | std::shared_ptr<CemuhookUDP::UDPClient> udp_client; | 341 | std::shared_ptr<CemuhookUDP::UDPClient> udp_client; |
| 323 | std::shared_ptr<Camera> camera; | 342 | std::shared_ptr<Camera> camera; |
| 324 | std::shared_ptr<Android> android; | ||
| 325 | std::shared_ptr<VirtualAmiibo> virtual_amiibo; | 343 | std::shared_ptr<VirtualAmiibo> virtual_amiibo; |
| 326 | std::shared_ptr<VirtualGamepad> virtual_gamepad; | 344 | std::shared_ptr<VirtualGamepad> virtual_gamepad; |
| 327 | 345 | ||
| @@ -333,6 +351,10 @@ struct InputSubsystem::Impl { | |||
| 333 | std::shared_ptr<SDLDriver> sdl; | 351 | std::shared_ptr<SDLDriver> sdl; |
| 334 | std::shared_ptr<Joycons> joycon; | 352 | std::shared_ptr<Joycons> joycon; |
| 335 | #endif | 353 | #endif |
| 354 | |||
| 355 | #ifdef ANDROID | ||
| 356 | std::shared_ptr<Android> android; | ||
| 357 | #endif | ||
| 336 | }; | 358 | }; |
| 337 | 359 | ||
| 338 | InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {} | 360 | InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {} |
| @@ -387,6 +409,7 @@ const Camera* InputSubsystem::GetCamera() const { | |||
| 387 | return impl->camera.get(); | 409 | return impl->camera.get(); |
| 388 | } | 410 | } |
| 389 | 411 | ||
| 412 | #ifdef ANDROID | ||
| 390 | Android* InputSubsystem::GetAndroid() { | 413 | Android* InputSubsystem::GetAndroid() { |
| 391 | return impl->android.get(); | 414 | return impl->android.get(); |
| 392 | } | 415 | } |
| @@ -394,6 +417,7 @@ Android* InputSubsystem::GetAndroid() { | |||
| 394 | const Android* InputSubsystem::GetAndroid() const { | 417 | const Android* InputSubsystem::GetAndroid() const { |
| 395 | return impl->android.get(); | 418 | return impl->android.get(); |
| 396 | } | 419 | } |
| 420 | #endif | ||
| 397 | 421 | ||
| 398 | VirtualAmiibo* InputSubsystem::GetVirtualAmiibo() { | 422 | VirtualAmiibo* InputSubsystem::GetVirtualAmiibo() { |
| 399 | return impl->virtual_amiibo.get(); | 423 | return impl->virtual_amiibo.get(); |