diff options
145 files changed, 6688 insertions, 2052 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..02a20dacf 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 | |||
| @@ -3,24 +3,21 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu | 4 | package org.yuzu.yuzu_emu |
| 5 | 5 | ||
| 6 | import android.app.Dialog | ||
| 7 | import android.content.DialogInterface | 6 | import android.content.DialogInterface |
| 8 | import android.net.Uri | 7 | import android.net.Uri |
| 9 | import android.os.Bundle | ||
| 10 | import android.text.Html | 8 | import android.text.Html |
| 11 | import android.text.method.LinkMovementMethod | 9 | import android.text.method.LinkMovementMethod |
| 12 | import android.view.Surface | 10 | import android.view.Surface |
| 13 | import android.view.View | 11 | import android.view.View |
| 14 | import android.widget.TextView | 12 | import android.widget.TextView |
| 15 | import androidx.annotation.Keep | 13 | import androidx.annotation.Keep |
| 16 | import androidx.fragment.app.DialogFragment | ||
| 17 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | 14 | import com.google.android.material.dialog.MaterialAlertDialogBuilder |
| 18 | import java.lang.ref.WeakReference | 15 | import java.lang.ref.WeakReference |
| 19 | import org.yuzu.yuzu_emu.activities.EmulationActivity | 16 | import org.yuzu.yuzu_emu.activities.EmulationActivity |
| 17 | import org.yuzu.yuzu_emu.fragments.CoreErrorDialogFragment | ||
| 20 | import org.yuzu.yuzu_emu.utils.DocumentsTree | 18 | import org.yuzu.yuzu_emu.utils.DocumentsTree |
| 21 | import org.yuzu.yuzu_emu.utils.FileUtil | 19 | import org.yuzu.yuzu_emu.utils.FileUtil |
| 22 | import org.yuzu.yuzu_emu.utils.Log | 20 | import org.yuzu.yuzu_emu.utils.Log |
| 23 | import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable | ||
| 24 | import org.yuzu.yuzu_emu.model.InstallResult | 21 | import org.yuzu.yuzu_emu.model.InstallResult |
| 25 | import org.yuzu.yuzu_emu.model.Patch | 22 | import org.yuzu.yuzu_emu.model.Patch |
| 26 | import org.yuzu.yuzu_emu.model.GameVerificationResult | 23 | import org.yuzu.yuzu_emu.model.GameVerificationResult |
| @@ -30,34 +27,6 @@ import org.yuzu.yuzu_emu.model.GameVerificationResult | |||
| 30 | * with the native side of the Yuzu code. | 27 | * with the native side of the Yuzu code. |
| 31 | */ | 28 | */ |
| 32 | object NativeLibrary { | 29 | 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 | 30 | @JvmField |
| 62 | var sEmulationActivity = WeakReference<EmulationActivity?>(null) | 31 | var sEmulationActivity = WeakReference<EmulationActivity?>(null) |
| 63 | 32 | ||
| @@ -127,112 +96,6 @@ object NativeLibrary { | |||
| 127 | FileUtil.getFilename(Uri.parse(path)) | 96 | FileUtil.getFilename(Uri.parse(path)) |
| 128 | } | 97 | } |
| 129 | 98 | ||
| 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) | 99 | external fun setAppDirectory(directory: String) |
| 237 | 100 | ||
| 238 | /** | 101 | /** |
| @@ -318,46 +181,13 @@ object NativeLibrary { | |||
| 318 | ErrorUnknown | 181 | ErrorUnknown |
| 319 | } | 182 | } |
| 320 | 183 | ||
| 321 | private var coreErrorAlertResult = false | 184 | var coreErrorAlertResult = false |
| 322 | private val coreErrorAlertLock = Object() | 185 | val coreErrorAlertLock = Object() |
| 323 | |||
| 324 | class CoreErrorDialogFragment : DialogFragment() { | ||
| 325 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { | ||
| 326 | val title = requireArguments().serializable<String>("title") | ||
| 327 | val message = requireArguments().serializable<String>("message") | ||
| 328 | |||
| 329 | return MaterialAlertDialogBuilder(requireActivity()) | ||
| 330 | .setTitle(title) | ||
| 331 | .setMessage(message) | ||
| 332 | .setPositiveButton(R.string.continue_button, null) | ||
| 333 | .setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int -> | ||
| 334 | coreErrorAlertResult = false | ||
| 335 | synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() } | ||
| 336 | } | ||
| 337 | .create() | ||
| 338 | } | ||
| 339 | |||
| 340 | override fun onDismiss(dialog: DialogInterface) { | ||
| 341 | coreErrorAlertResult = true | ||
| 342 | synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() } | ||
| 343 | } | ||
| 344 | |||
| 345 | companion object { | ||
| 346 | fun newInstance(title: String?, message: String?): CoreErrorDialogFragment { | ||
| 347 | val frag = CoreErrorDialogFragment() | ||
| 348 | val args = Bundle() | ||
| 349 | args.putString("title", title) | ||
| 350 | args.putString("message", message) | ||
| 351 | frag.arguments = args | ||
| 352 | return frag | ||
| 353 | } | ||
| 354 | } | ||
| 355 | } | ||
| 356 | 186 | ||
| 357 | private fun onCoreErrorImpl(title: String, message: String) { | 187 | private fun onCoreErrorImpl(title: String, message: String) { |
| 358 | val emulationActivity = sEmulationActivity.get() | 188 | val emulationActivity = sEmulationActivity.get() |
| 359 | if (emulationActivity == null) { | 189 | if (emulationActivity == null) { |
| 360 | error("[NativeLibrary] EmulationActivity not present") | 190 | Log.error("[NativeLibrary] EmulationActivity not present") |
| 361 | return | 191 | return |
| 362 | } | 192 | } |
| 363 | 193 | ||
| @@ -373,7 +203,7 @@ object NativeLibrary { | |||
| 373 | fun onCoreError(error: CoreError?, details: String): Boolean { | 203 | fun onCoreError(error: CoreError?, details: String): Boolean { |
| 374 | val emulationActivity = sEmulationActivity.get() | 204 | val emulationActivity = sEmulationActivity.get() |
| 375 | if (emulationActivity == null) { | 205 | if (emulationActivity == null) { |
| 376 | error("[NativeLibrary] EmulationActivity not present") | 206 | Log.error("[NativeLibrary] EmulationActivity not present") |
| 377 | return false | 207 | return false |
| 378 | } | 208 | } |
| 379 | 209 | ||
| @@ -404,7 +234,7 @@ object NativeLibrary { | |||
| 404 | } | 234 | } |
| 405 | 235 | ||
| 406 | // Show the AlertDialog on the main thread. | 236 | // Show the AlertDialog on the main thread. |
| 407 | emulationActivity.runOnUiThread(Runnable { onCoreErrorImpl(title, message) }) | 237 | emulationActivity.runOnUiThread { onCoreErrorImpl(title, message) } |
| 408 | 238 | ||
| 409 | // Wait for the lock to notify that it is complete. | 239 | // Wait for the lock to notify that it is complete. |
| 410 | synchronized(coreErrorAlertLock) { coreErrorAlertLock.wait() } | 240 | synchronized(coreErrorAlertLock) { coreErrorAlertLock.wait() } |
| @@ -629,46 +459,4 @@ object NativeLibrary { | |||
| 629 | * Checks if all necessary keys are present for decryption | 459 | * Checks if all necessary keys are present for decryption |
| 630 | */ | 460 | */ |
| 631 | external fun areKeysPresent(): Boolean | 461 | 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 | } | 462 | } |
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..c962558a7 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,33 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||
| 78 | 79 | ||
| 79 | super.onCreate(savedInstanceState) | 80 | super.onCreate(savedInstanceState) |
| 80 | 81 | ||
| 82 | InputHandler.updateControllerData() | ||
| 83 | val players = NativeConfig.getInputSettings(true) | ||
| 84 | var hasConfiguredControllers = false | ||
| 85 | players.forEach { | ||
| 86 | if (it.hasMapping()) { | ||
| 87 | hasConfiguredControllers = true | ||
| 88 | } | ||
| 89 | } | ||
| 90 | if (!hasConfiguredControllers && InputHandler.androidControllers.isNotEmpty()) { | ||
| 91 | var params: ParamPackage? = null | ||
| 92 | for (controller in InputHandler.registeredControllers) { | ||
| 93 | if (controller.get("port", -1) == 0) { | ||
| 94 | params = controller | ||
| 95 | break | ||
| 96 | } | ||
| 97 | } | ||
| 98 | |||
| 99 | if (params != null) { | ||
| 100 | NativeInput.updateMappingsWithDefault( | ||
| 101 | 0, | ||
| 102 | params, | ||
| 103 | params.get("display", getString(R.string.unknown)) | ||
| 104 | ) | ||
| 105 | NativeConfig.saveGlobalConfig() | ||
| 106 | } | ||
| 107 | } | ||
| 108 | |||
| 81 | binding = ActivityEmulationBinding.inflate(layoutInflater) | 109 | binding = ActivityEmulationBinding.inflate(layoutInflater) |
| 82 | setContentView(binding.root) | 110 | setContentView(binding.root) |
| 83 | 111 | ||
| @@ -95,8 +123,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||
| 95 | nfcReader = NfcReader(this) | 123 | nfcReader = NfcReader(this) |
| 96 | nfcReader.initialize() | 124 | nfcReader.initialize() |
| 97 | 125 | ||
| 98 | InputHandler.initialize() | ||
| 99 | |||
| 100 | val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) | 126 | val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) |
| 101 | if (!preferences.getBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, false)) { | 127 | if (!preferences.getBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, false)) { |
| 102 | if (MemoryUtil.isLessThan(MemoryUtil.REQUIRED_MEMORY, MemoryUtil.totalMemory)) { | 128 | if (MemoryUtil.isLessThan(MemoryUtil.REQUIRED_MEMORY, MemoryUtil.totalMemory)) { |
| @@ -147,7 +173,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||
| 147 | super.onResume() | 173 | super.onResume() |
| 148 | nfcReader.startScanning() | 174 | nfcReader.startScanning() |
| 149 | startMotionSensorListener() | 175 | startMotionSensorListener() |
| 150 | InputHandler.updateControllerIds() | 176 | InputHandler.updateControllerData() |
| 151 | 177 | ||
| 152 | buildPictureInPictureParams() | 178 | buildPictureInPictureParams() |
| 153 | } | 179 | } |
| @@ -172,6 +198,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||
| 172 | super.onNewIntent(intent) | 198 | super.onNewIntent(intent) |
| 173 | setIntent(intent) | 199 | setIntent(intent) |
| 174 | nfcReader.onNewIntent(intent) | 200 | nfcReader.onNewIntent(intent) |
| 201 | InputHandler.updateControllerData() | ||
| 175 | } | 202 | } |
| 176 | 203 | ||
| 177 | override fun dispatchKeyEvent(event: KeyEvent): Boolean { | 204 | override fun dispatchKeyEvent(event: KeyEvent): Boolean { |
| @@ -244,8 +271,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||
| 244 | } | 271 | } |
| 245 | val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000 | 272 | val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000 |
| 246 | motionTimestamp = event.timestamp | 273 | motionTimestamp = event.timestamp |
| 247 | NativeLibrary.onGamePadMotionEvent( | 274 | NativeInput.onDeviceMotionEvent( |
| 248 | NativeLibrary.Player1Device, | 275 | NativeInput.Player1Device, |
| 249 | deltaTimestamp, | 276 | deltaTimestamp, |
| 250 | gyro[0], | 277 | gyro[0], |
| 251 | gyro[1], | 278 | gyro[1], |
| @@ -254,8 +281,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||
| 254 | accel[1], | 281 | accel[1], |
| 255 | accel[2] | 282 | accel[2] |
| 256 | ) | 283 | ) |
| 257 | NativeLibrary.onGamePadMotionEvent( | 284 | NativeInput.onDeviceMotionEvent( |
| 258 | NativeLibrary.ConsoleDevice, | 285 | NativeInput.ConsoleDevice, |
| 259 | deltaTimestamp, | 286 | deltaTimestamp, |
| 260 | gyro[0], | 287 | gyro[0], |
| 261 | gyro[1], | 288 | gyro[1], |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt index f218c76ef..50663ad91 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt | |||
| @@ -3,15 +3,15 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.adapters | 4 | package org.yuzu.yuzu_emu.adapters |
| 5 | 5 | ||
| 6 | import android.text.TextUtils | ||
| 7 | import android.view.LayoutInflater | 6 | import android.view.LayoutInflater |
| 8 | import android.view.View | ||
| 9 | import android.view.ViewGroup | 7 | import android.view.ViewGroup |
| 10 | import org.yuzu.yuzu_emu.R | 8 | import org.yuzu.yuzu_emu.R |
| 11 | import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding | 9 | import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding |
| 12 | import org.yuzu.yuzu_emu.features.settings.model.StringSetting | 10 | import org.yuzu.yuzu_emu.features.settings.model.StringSetting |
| 13 | import org.yuzu.yuzu_emu.model.Driver | 11 | import org.yuzu.yuzu_emu.model.Driver |
| 14 | import org.yuzu.yuzu_emu.model.DriverViewModel | 12 | import org.yuzu.yuzu_emu.model.DriverViewModel |
| 13 | import org.yuzu.yuzu_emu.utils.ViewUtils.marquee | ||
| 14 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 15 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder | 15 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder |
| 16 | 16 | ||
| 17 | class DriverAdapter(private val driverViewModel: DriverViewModel) : | 17 | class DriverAdapter(private val driverViewModel: DriverViewModel) : |
| @@ -44,25 +44,15 @@ class DriverAdapter(private val driverViewModel: DriverViewModel) : | |||
| 44 | } | 44 | } |
| 45 | 45 | ||
| 46 | // Delay marquee by 3s | 46 | // Delay marquee by 3s |
| 47 | title.postDelayed( | 47 | title.marquee() |
| 48 | { | 48 | version.marquee() |
| 49 | title.isSelected = true | 49 | description.marquee() |
| 50 | title.ellipsize = TextUtils.TruncateAt.MARQUEE | ||
| 51 | version.isSelected = true | ||
| 52 | version.ellipsize = TextUtils.TruncateAt.MARQUEE | ||
| 53 | description.isSelected = true | ||
| 54 | description.ellipsize = TextUtils.TruncateAt.MARQUEE | ||
| 55 | }, | ||
| 56 | 3000 | ||
| 57 | ) | ||
| 58 | title.text = model.title | 50 | title.text = model.title |
| 59 | version.text = model.version | 51 | version.text = model.version |
| 60 | description.text = model.description | 52 | description.text = model.description |
| 61 | if (model.title != binding.root.context.getString(R.string.system_gpu_driver)) { | 53 | buttonDelete.setVisible( |
| 62 | buttonDelete.visibility = View.VISIBLE | 54 | model.title != binding.root.context.getString(R.string.system_gpu_driver) |
| 63 | } else { | 55 | ) |
| 64 | buttonDelete.visibility = View.GONE | ||
| 65 | } | ||
| 66 | } | 56 | } |
| 67 | } | 57 | } |
| 68 | } | 58 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt index 3d8f0bda8..5cbd15d2a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt | |||
| @@ -4,7 +4,6 @@ | |||
| 4 | package org.yuzu.yuzu_emu.adapters | 4 | package org.yuzu.yuzu_emu.adapters |
| 5 | 5 | ||
| 6 | import android.net.Uri | 6 | import android.net.Uri |
| 7 | import android.text.TextUtils | ||
| 8 | import android.view.LayoutInflater | 7 | import android.view.LayoutInflater |
| 9 | import android.view.ViewGroup | 8 | import android.view.ViewGroup |
| 10 | import androidx.fragment.app.FragmentActivity | 9 | import androidx.fragment.app.FragmentActivity |
| @@ -12,6 +11,7 @@ import org.yuzu.yuzu_emu.databinding.CardFolderBinding | |||
| 12 | import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment | 11 | import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment |
| 13 | import org.yuzu.yuzu_emu.model.GameDir | 12 | import org.yuzu.yuzu_emu.model.GameDir |
| 14 | import org.yuzu.yuzu_emu.model.GamesViewModel | 13 | import org.yuzu.yuzu_emu.model.GamesViewModel |
| 14 | import org.yuzu.yuzu_emu.utils.ViewUtils.marquee | ||
| 15 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder | 15 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder |
| 16 | 16 | ||
| 17 | class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) : | 17 | class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) : |
| @@ -29,13 +29,7 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie | |||
| 29 | override fun bind(model: GameDir) { | 29 | override fun bind(model: GameDir) { |
| 30 | binding.apply { | 30 | binding.apply { |
| 31 | path.text = Uri.parse(model.uriString).path | 31 | path.text = Uri.parse(model.uriString).path |
| 32 | path.postDelayed( | 32 | path.marquee() |
| 33 | { | ||
| 34 | path.isSelected = true | ||
| 35 | path.ellipsize = TextUtils.TruncateAt.MARQUEE | ||
| 36 | }, | ||
| 37 | 3000 | ||
| 38 | ) | ||
| 39 | 33 | ||
| 40 | buttonEdit.setOnClickListener { | 34 | buttonEdit.setOnClickListener { |
| 41 | GameFolderPropertiesDialogFragment.newInstance(model) | 35 | GameFolderPropertiesDialogFragment.newInstance(model) |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt index 85c8249e6..b1f247ac3 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt | |||
| @@ -4,7 +4,6 @@ | |||
| 4 | package org.yuzu.yuzu_emu.adapters | 4 | package org.yuzu.yuzu_emu.adapters |
| 5 | 5 | ||
| 6 | import android.net.Uri | 6 | import android.net.Uri |
| 7 | import android.text.TextUtils | ||
| 8 | import android.view.LayoutInflater | 7 | import android.view.LayoutInflater |
| 9 | import android.view.ViewGroup | 8 | import android.view.ViewGroup |
| 10 | import android.widget.ImageView | 9 | import android.widget.ImageView |
| @@ -27,6 +26,7 @@ import org.yuzu.yuzu_emu.databinding.CardGameBinding | |||
| 27 | import org.yuzu.yuzu_emu.model.Game | 26 | import org.yuzu.yuzu_emu.model.Game |
| 28 | import org.yuzu.yuzu_emu.model.GamesViewModel | 27 | import org.yuzu.yuzu_emu.model.GamesViewModel |
| 29 | import org.yuzu.yuzu_emu.utils.GameIconUtils | 28 | import org.yuzu.yuzu_emu.utils.GameIconUtils |
| 29 | import org.yuzu.yuzu_emu.utils.ViewUtils.marquee | ||
| 30 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder | 30 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder |
| 31 | 31 | ||
| 32 | class GameAdapter(private val activity: AppCompatActivity) : | 32 | class GameAdapter(private val activity: AppCompatActivity) : |
| @@ -44,14 +44,7 @@ class GameAdapter(private val activity: AppCompatActivity) : | |||
| 44 | 44 | ||
| 45 | binding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ") | 45 | binding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ") |
| 46 | 46 | ||
| 47 | binding.textGameTitle.postDelayed( | 47 | binding.textGameTitle.marquee() |
| 48 | { | ||
| 49 | binding.textGameTitle.ellipsize = TextUtils.TruncateAt.MARQUEE | ||
| 50 | binding.textGameTitle.isSelected = true | ||
| 51 | }, | ||
| 52 | 3000 | ||
| 53 | ) | ||
| 54 | |||
| 55 | binding.cardGame.setOnClickListener { onClick(model) } | 48 | binding.cardGame.setOnClickListener { onClick(model) } |
| 56 | binding.cardGame.setOnLongClickListener { onLongClick(model) } | 49 | binding.cardGame.setOnLongClickListener { onLongClick(model) } |
| 57 | } | 50 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt index 0046d5314..7366e2c77 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt | |||
| @@ -3,21 +3,18 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.adapters | 4 | package org.yuzu.yuzu_emu.adapters |
| 5 | 5 | ||
| 6 | import android.text.TextUtils | ||
| 7 | import android.view.LayoutInflater | 6 | import android.view.LayoutInflater |
| 8 | import android.view.View | ||
| 9 | import android.view.ViewGroup | 7 | import android.view.ViewGroup |
| 10 | import androidx.core.content.res.ResourcesCompat | 8 | import androidx.core.content.res.ResourcesCompat |
| 11 | import androidx.lifecycle.Lifecycle | ||
| 12 | import androidx.lifecycle.LifecycleOwner | 9 | import androidx.lifecycle.LifecycleOwner |
| 13 | import androidx.lifecycle.lifecycleScope | ||
| 14 | import androidx.lifecycle.repeatOnLifecycle | ||
| 15 | import kotlinx.coroutines.launch | ||
| 16 | import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding | 10 | import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding |
| 17 | import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding | 11 | import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding |
| 18 | import org.yuzu.yuzu_emu.model.GameProperty | 12 | import org.yuzu.yuzu_emu.model.GameProperty |
| 19 | import org.yuzu.yuzu_emu.model.InstallableProperty | 13 | import org.yuzu.yuzu_emu.model.InstallableProperty |
| 20 | import org.yuzu.yuzu_emu.model.SubmenuProperty | 14 | import org.yuzu.yuzu_emu.model.SubmenuProperty |
| 15 | import org.yuzu.yuzu_emu.utils.ViewUtils.marquee | ||
| 16 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 17 | import org.yuzu.yuzu_emu.utils.collect | ||
| 21 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder | 18 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder |
| 22 | 19 | ||
| 23 | class GamePropertiesAdapter( | 20 | class GamePropertiesAdapter( |
| @@ -76,23 +73,15 @@ class GamePropertiesAdapter( | |||
| 76 | ) | 73 | ) |
| 77 | ) | 74 | ) |
| 78 | 75 | ||
| 79 | binding.details.postDelayed({ | 76 | binding.details.marquee() |
| 80 | binding.details.isSelected = true | ||
| 81 | binding.details.ellipsize = TextUtils.TruncateAt.MARQUEE | ||
| 82 | }, 3000) | ||
| 83 | |||
| 84 | if (submenuProperty.details != null) { | 77 | if (submenuProperty.details != null) { |
| 85 | binding.details.visibility = View.VISIBLE | 78 | binding.details.setVisible(true) |
| 86 | binding.details.text = submenuProperty.details.invoke() | 79 | binding.details.text = submenuProperty.details.invoke() |
| 87 | } else if (submenuProperty.detailsFlow != null) { | 80 | } else if (submenuProperty.detailsFlow != null) { |
| 88 | binding.details.visibility = View.VISIBLE | 81 | binding.details.setVisible(true) |
| 89 | viewLifecycle.lifecycleScope.launch { | 82 | submenuProperty.detailsFlow.collect(viewLifecycle) { binding.details.text = it } |
| 90 | viewLifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
| 91 | submenuProperty.detailsFlow.collect { binding.details.text = it } | ||
| 92 | } | ||
| 93 | } | ||
| 94 | } else { | 83 | } else { |
| 95 | binding.details.visibility = View.GONE | 84 | binding.details.setVisible(false) |
| 96 | } | 85 | } |
| 97 | } | 86 | } |
| 98 | } | 87 | } |
| @@ -112,14 +101,10 @@ class GamePropertiesAdapter( | |||
| 112 | ) | 101 | ) |
| 113 | ) | 102 | ) |
| 114 | 103 | ||
| 115 | if (installableProperty.install != null) { | 104 | binding.buttonInstall.setVisible(installableProperty.install != null) |
| 116 | binding.buttonInstall.visibility = View.VISIBLE | 105 | binding.buttonInstall.setOnClickListener { installableProperty.install?.invoke() } |
| 117 | binding.buttonInstall.setOnClickListener { installableProperty.install.invoke() } | 106 | binding.buttonExport.setVisible(installableProperty.export != null) |
| 118 | } | 107 | binding.buttonExport.setOnClickListener { installableProperty.export?.invoke() } |
| 119 | if (installableProperty.export != null) { | ||
| 120 | binding.buttonExport.visibility = View.VISIBLE | ||
| 121 | binding.buttonExport.setOnClickListener { installableProperty.export.invoke() } | ||
| 122 | } | ||
| 123 | } | 108 | } |
| 124 | } | 109 | } |
| 125 | 110 | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt index b512845d5..0bd196673 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt | |||
| @@ -3,22 +3,19 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.adapters | 4 | package org.yuzu.yuzu_emu.adapters |
| 5 | 5 | ||
| 6 | import android.text.TextUtils | ||
| 7 | import android.view.LayoutInflater | 6 | import android.view.LayoutInflater |
| 8 | import android.view.View | ||
| 9 | import android.view.ViewGroup | 7 | import android.view.ViewGroup |
| 10 | import androidx.appcompat.app.AppCompatActivity | 8 | import androidx.appcompat.app.AppCompatActivity |
| 11 | import androidx.core.content.ContextCompat | 9 | import androidx.core.content.ContextCompat |
| 12 | import androidx.core.content.res.ResourcesCompat | 10 | import androidx.core.content.res.ResourcesCompat |
| 13 | import androidx.lifecycle.Lifecycle | ||
| 14 | import androidx.lifecycle.LifecycleOwner | 11 | import androidx.lifecycle.LifecycleOwner |
| 15 | import androidx.lifecycle.lifecycleScope | ||
| 16 | import androidx.lifecycle.repeatOnLifecycle | ||
| 17 | import kotlinx.coroutines.launch | ||
| 18 | import org.yuzu.yuzu_emu.R | 12 | import org.yuzu.yuzu_emu.R |
| 19 | import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding | 13 | import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding |
| 20 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment | 14 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment |
| 21 | import org.yuzu.yuzu_emu.model.HomeSetting | 15 | import org.yuzu.yuzu_emu.model.HomeSetting |
| 16 | import org.yuzu.yuzu_emu.utils.ViewUtils.marquee | ||
| 17 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 18 | import org.yuzu.yuzu_emu.utils.collect | ||
| 22 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder | 19 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder |
| 23 | 20 | ||
| 24 | class HomeSettingAdapter( | 21 | class HomeSettingAdapter( |
| @@ -59,18 +56,8 @@ class HomeSettingAdapter( | |||
| 59 | binding.optionIcon.alpha = 0.5f | 56 | binding.optionIcon.alpha = 0.5f |
| 60 | } | 57 | } |
| 61 | 58 | ||
| 62 | viewLifecycle.lifecycleScope.launch { | 59 | model.details.collect(viewLifecycle) { updateOptionDetails(it) } |
| 63 | viewLifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) { | 60 | binding.optionDetail.marquee() |
| 64 | model.details.collect { updateOptionDetails(it) } | ||
| 65 | } | ||
| 66 | } | ||
| 67 | binding.optionDetail.postDelayed( | ||
| 68 | { | ||
| 69 | binding.optionDetail.ellipsize = TextUtils.TruncateAt.MARQUEE | ||
| 70 | binding.optionDetail.isSelected = true | ||
| 71 | }, | ||
| 72 | 3000 | ||
| 73 | ) | ||
| 74 | 61 | ||
| 75 | binding.root.setOnClickListener { onClick(model) } | 62 | binding.root.setOnClickListener { onClick(model) } |
| 76 | } | 63 | } |
| @@ -90,7 +77,7 @@ class HomeSettingAdapter( | |||
| 90 | private fun updateOptionDetails(detailString: String) { | 77 | private fun updateOptionDetails(detailString: String) { |
| 91 | if (detailString.isNotEmpty()) { | 78 | if (detailString.isNotEmpty()) { |
| 92 | binding.optionDetail.text = detailString | 79 | binding.optionDetail.text = detailString |
| 93 | binding.optionDetail.visibility = View.VISIBLE | 80 | binding.optionDetail.setVisible(true) |
| 94 | } | 81 | } |
| 95 | } | 82 | } |
| 96 | } | 83 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt index 4218c4e52..1ba75fa2f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt | |||
| @@ -4,10 +4,10 @@ | |||
| 4 | package org.yuzu.yuzu_emu.adapters | 4 | package org.yuzu.yuzu_emu.adapters |
| 5 | 5 | ||
| 6 | import android.view.LayoutInflater | 6 | import android.view.LayoutInflater |
| 7 | import android.view.View | ||
| 8 | import android.view.ViewGroup | 7 | import android.view.ViewGroup |
| 9 | import org.yuzu.yuzu_emu.databinding.CardInstallableBinding | 8 | import org.yuzu.yuzu_emu.databinding.CardInstallableBinding |
| 10 | import org.yuzu.yuzu_emu.model.Installable | 9 | import org.yuzu.yuzu_emu.model.Installable |
| 10 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 11 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder | 11 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder |
| 12 | 12 | ||
| 13 | class InstallableAdapter(installables: List<Installable>) : | 13 | class InstallableAdapter(installables: List<Installable>) : |
| @@ -26,14 +26,10 @@ class InstallableAdapter(installables: List<Installable>) : | |||
| 26 | binding.title.setText(model.titleId) | 26 | binding.title.setText(model.titleId) |
| 27 | binding.description.setText(model.descriptionId) | 27 | binding.description.setText(model.descriptionId) |
| 28 | 28 | ||
| 29 | if (model.install != null) { | 29 | binding.buttonInstall.setVisible(model.install != null) |
| 30 | binding.buttonInstall.visibility = View.VISIBLE | 30 | binding.buttonInstall.setOnClickListener { model.install?.invoke() } |
| 31 | binding.buttonInstall.setOnClickListener { model.install.invoke() } | 31 | binding.buttonExport.setVisible(model.export != null) |
| 32 | } | 32 | binding.buttonExport.setOnClickListener { model.export?.invoke() } |
| 33 | if (model.export != null) { | ||
| 34 | binding.buttonExport.visibility = View.VISIBLE | ||
| 35 | binding.buttonExport.setOnClickListener { model.export.invoke() } | ||
| 36 | } | ||
| 37 | } | 33 | } |
| 38 | } | 34 | } |
| 39 | } | 35 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt index 38bb1f96f..1379968f9 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/LicenseAdapter.kt | |||
| @@ -4,12 +4,12 @@ | |||
| 4 | package org.yuzu.yuzu_emu.adapters | 4 | package org.yuzu.yuzu_emu.adapters |
| 5 | 5 | ||
| 6 | import android.view.LayoutInflater | 6 | import android.view.LayoutInflater |
| 7 | import android.view.View | ||
| 8 | import android.view.ViewGroup | 7 | import android.view.ViewGroup |
| 9 | import androidx.appcompat.app.AppCompatActivity | 8 | import androidx.appcompat.app.AppCompatActivity |
| 10 | import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding | 9 | import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding |
| 11 | import org.yuzu.yuzu_emu.fragments.LicenseBottomSheetDialogFragment | 10 | import org.yuzu.yuzu_emu.fragments.LicenseBottomSheetDialogFragment |
| 12 | import org.yuzu.yuzu_emu.model.License | 11 | import org.yuzu.yuzu_emu.model.License |
| 12 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 13 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder | 13 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder |
| 14 | 14 | ||
| 15 | class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<License>) : | 15 | class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<License>) : |
| @@ -25,7 +25,7 @@ class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<Lic | |||
| 25 | binding.apply { | 25 | binding.apply { |
| 26 | textSettingName.text = root.context.getString(model.titleId) | 26 | textSettingName.text = root.context.getString(model.titleId) |
| 27 | textSettingDescription.text = root.context.getString(model.descriptionId) | 27 | textSettingDescription.text = root.context.getString(model.descriptionId) |
| 28 | textSettingValue.visibility = View.GONE | 28 | textSettingValue.setVisible(false) |
| 29 | 29 | ||
| 30 | root.setOnClickListener { onClick(model) } | 30 | root.setOnClickListener { onClick(model) } |
| 31 | } | 31 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt index 02118e1a8..a5f610b31 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt | |||
| @@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.adapters | |||
| 5 | 5 | ||
| 6 | import android.text.Html | 6 | import android.text.Html |
| 7 | import android.view.LayoutInflater | 7 | import android.view.LayoutInflater |
| 8 | import android.view.View | ||
| 9 | import android.view.ViewGroup | 8 | import android.view.ViewGroup |
| 10 | import androidx.appcompat.app.AppCompatActivity | 9 | import androidx.appcompat.app.AppCompatActivity |
| 11 | import androidx.core.content.res.ResourcesCompat | 10 | import androidx.core.content.res.ResourcesCompat |
| @@ -17,6 +16,7 @@ import org.yuzu.yuzu_emu.model.SetupCallback | |||
| 17 | import org.yuzu.yuzu_emu.model.SetupPage | 16 | import org.yuzu.yuzu_emu.model.SetupPage |
| 18 | import org.yuzu.yuzu_emu.model.StepState | 17 | import org.yuzu.yuzu_emu.model.StepState |
| 19 | import org.yuzu.yuzu_emu.utils.ViewUtils | 18 | import org.yuzu.yuzu_emu.utils.ViewUtils |
| 19 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 20 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder | 20 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder |
| 21 | 21 | ||
| 22 | class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) : | 22 | class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) : |
| @@ -30,8 +30,8 @@ class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) : | |||
| 30 | AbstractViewHolder<SetupPage>(binding), SetupCallback { | 30 | AbstractViewHolder<SetupPage>(binding), SetupCallback { |
| 31 | override fun bind(model: SetupPage) { | 31 | override fun bind(model: SetupPage) { |
| 32 | if (model.stepCompleted.invoke() == StepState.COMPLETE) { | 32 | if (model.stepCompleted.invoke() == StepState.COMPLETE) { |
| 33 | binding.buttonAction.visibility = View.INVISIBLE | 33 | binding.buttonAction.setVisible(visible = false, gone = false) |
| 34 | binding.textConfirmation.visibility = View.VISIBLE | 34 | binding.textConfirmation.setVisible(true) |
| 35 | } | 35 | } |
| 36 | 36 | ||
| 37 | binding.icon.setImageDrawable( | 37 | binding.icon.setImageDrawable( |
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/DateTimeSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt index 1d81f5f2b..58febff1d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt | |||
| @@ -3,13 +3,16 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.features.settings.model.view | 4 | package org.yuzu.yuzu_emu.features.settings.model.view |
| 5 | 5 | ||
| 6 | import androidx.annotation.StringRes | ||
| 6 | import org.yuzu.yuzu_emu.features.settings.model.AbstractLongSetting | 7 | import org.yuzu.yuzu_emu.features.settings.model.AbstractLongSetting |
| 7 | 8 | ||
| 8 | class DateTimeSetting( | 9 | class DateTimeSetting( |
| 9 | private val longSetting: AbstractLongSetting, | 10 | private val longSetting: AbstractLongSetting, |
| 10 | titleId: Int, | 11 | @StringRes titleId: Int = 0, |
| 11 | descriptionId: Int | 12 | titleString: String = "", |
| 12 | ) : SettingsItem(longSetting, titleId, descriptionId) { | 13 | @StringRes descriptionId: Int = 0, |
| 14 | descriptionString: String = "" | ||
| 15 | ) : SettingsItem(longSetting, titleId, titleString, descriptionId, descriptionString) { | ||
| 13 | override val type = TYPE_DATETIME_SETTING | 16 | override val type = TYPE_DATETIME_SETTING |
| 14 | 17 | ||
| 15 | fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal) | 18 | fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal) |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt index d31ce1c31..8a6a51d5c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt | |||
| @@ -3,8 +3,11 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.features.settings.model.view | 4 | package org.yuzu.yuzu_emu.features.settings.model.view |
| 5 | 5 | ||
| 6 | import androidx.annotation.StringRes | ||
| 7 | |||
| 6 | class HeaderSetting( | 8 | class HeaderSetting( |
| 7 | titleId: Int | 9 | @StringRes titleId: Int = 0, |
| 8 | ) : SettingsItem(emptySetting, titleId, 0) { | 10 | titleString: String = "" |
| 11 | ) : SettingsItem(emptySetting, titleId, titleString, 0, "") { | ||
| 9 | override val type = TYPE_HEADER | 12 | override val type = TYPE_HEADER |
| 10 | } | 13 | } |
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 425160024..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 | |||
| @@ -4,13 +4,16 @@ | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.model.view | 4 | package org.yuzu.yuzu_emu.features.settings.model.view |
| 5 | 5 | ||
| 6 | import androidx.annotation.DrawableRes | 6 | import androidx.annotation.DrawableRes |
| 7 | import androidx.annotation.StringRes | ||
| 7 | 8 | ||
| 8 | class RunnableSetting( | 9 | class RunnableSetting( |
| 9 | titleId: Int, | 10 | @StringRes titleId: Int = 0, |
| 10 | descriptionId: Int, | 11 | titleString: String = "", |
| 11 | val isRuntimeRunnable: Boolean, | 12 | @StringRes descriptionId: Int = 0, |
| 13 | descriptionString: String = "", | ||
| 14 | val isRunnable: Boolean, | ||
| 12 | @DrawableRes val iconId: Int = 0, | 15 | @DrawableRes val iconId: Int = 0, |
| 13 | val runnable: () -> Unit | 16 | val runnable: () -> Unit |
| 14 | ) : SettingsItem(emptySetting, titleId, descriptionId) { | 17 | ) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) { |
| 15 | override val type = TYPE_RUNNABLE | 18 | override val type = TYPE_RUNNABLE |
| 16 | } | 19 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt index 21ca97bc1..8f724835e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt | |||
| @@ -3,8 +3,12 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.features.settings.model.view | 4 | package org.yuzu.yuzu_emu.features.settings.model.view |
| 5 | 5 | ||
| 6 | import androidx.annotation.StringRes | ||
| 6 | import org.yuzu.yuzu_emu.NativeLibrary | 7 | import org.yuzu.yuzu_emu.NativeLibrary |
| 7 | import org.yuzu.yuzu_emu.R | 8 | import org.yuzu.yuzu_emu.R |
| 9 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 10 | import org.yuzu.yuzu_emu.features.input.NativeInput | ||
| 11 | import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex | ||
| 8 | import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting | 12 | import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting |
| 9 | import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting | 13 | import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting |
| 10 | import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting | 14 | import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting |
| @@ -23,13 +27,34 @@ import org.yuzu.yuzu_emu.utils.NativeConfig | |||
| 23 | */ | 27 | */ |
| 24 | abstract class SettingsItem( | 28 | abstract class SettingsItem( |
| 25 | val setting: AbstractSetting, | 29 | val setting: AbstractSetting, |
| 26 | val nameId: Int, | 30 | @StringRes val titleId: Int, |
| 27 | val descriptionId: Int | 31 | val titleString: String, |
| 32 | @StringRes val descriptionId: Int, | ||
| 33 | val descriptionString: String | ||
| 28 | ) { | 34 | ) { |
| 29 | abstract val type: Int | 35 | abstract val type: Int |
| 30 | 36 | ||
| 37 | val title: String by lazy { | ||
| 38 | if (titleId != 0) { | ||
| 39 | return@lazy YuzuApplication.appContext.getString(titleId) | ||
| 40 | } | ||
| 41 | return@lazy titleString | ||
| 42 | } | ||
| 43 | |||
| 44 | val description: String by lazy { | ||
| 45 | if (descriptionId != 0) { | ||
| 46 | return@lazy YuzuApplication.appContext.getString(descriptionId) | ||
| 47 | } | ||
| 48 | return@lazy descriptionString | ||
| 49 | } | ||
| 50 | |||
| 31 | val isEditable: Boolean | 51 | val isEditable: Boolean |
| 32 | get() { | 52 | get() { |
| 53 | // Can't change docked mode toggle when using handheld mode | ||
| 54 | if (setting.key == BooleanSetting.USE_DOCKED_MODE.key) { | ||
| 55 | return NativeInput.getStyleIndex(0) != NpadStyleIndex.Handheld | ||
| 56 | } | ||
| 57 | |||
| 33 | // Can't edit settings that aren't saveable in per-game config even if they are switchable | 58 | // Can't edit settings that aren't saveable in per-game config even if they are switchable |
| 34 | if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) { | 59 | if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) { |
| 35 | return false | 60 | return false |
| @@ -59,6 +84,9 @@ abstract class SettingsItem( | |||
| 59 | const val TYPE_STRING_SINGLE_CHOICE = 5 | 84 | const val TYPE_STRING_SINGLE_CHOICE = 5 |
| 60 | const val TYPE_DATETIME_SETTING = 6 | 85 | const val TYPE_DATETIME_SETTING = 6 |
| 61 | const val TYPE_RUNNABLE = 7 | 86 | const val TYPE_RUNNABLE = 7 |
| 87 | const val TYPE_INPUT = 8 | ||
| 88 | const val TYPE_INT_SINGLE_CHOICE = 9 | ||
| 89 | const val TYPE_INPUT_PROFILE = 10 | ||
| 62 | 90 | ||
| 63 | const val FASTMEM_COMBINED = "fastmem_combined" | 91 | const val FASTMEM_COMBINED = "fastmem_combined" |
| 64 | 92 | ||
| @@ -80,237 +108,242 @@ abstract class SettingsItem( | |||
| 80 | put( | 108 | put( |
| 81 | SwitchSetting( | 109 | SwitchSetting( |
| 82 | BooleanSetting.RENDERER_USE_SPEED_LIMIT, | 110 | BooleanSetting.RENDERER_USE_SPEED_LIMIT, |
| 83 | R.string.frame_limit_enable, | 111 | titleId = R.string.frame_limit_enable, |
| 84 | R.string.frame_limit_enable_description | 112 | descriptionId = R.string.frame_limit_enable_description |
| 85 | ) | 113 | ) |
| 86 | ) | 114 | ) |
| 87 | put( | 115 | put( |
| 88 | SliderSetting( | 116 | SliderSetting( |
| 89 | ShortSetting.RENDERER_SPEED_LIMIT, | 117 | ShortSetting.RENDERER_SPEED_LIMIT, |
| 90 | R.string.frame_limit_slider, | 118 | titleId = R.string.frame_limit_slider, |
| 91 | R.string.frame_limit_slider_description, | 119 | descriptionId = R.string.frame_limit_slider_description, |
| 92 | 1, | 120 | min = 1, |
| 93 | 400, | 121 | max = 400, |
| 94 | "%" | 122 | units = "%" |
| 95 | ) | 123 | ) |
| 96 | ) | 124 | ) |
| 97 | put( | 125 | put( |
| 98 | SingleChoiceSetting( | 126 | SingleChoiceSetting( |
| 99 | IntSetting.CPU_BACKEND, | 127 | IntSetting.CPU_BACKEND, |
| 100 | R.string.cpu_backend, | 128 | titleId = R.string.cpu_backend, |
| 101 | 0, | 129 | choicesId = R.array.cpuBackendArm64Names, |
| 102 | R.array.cpuBackendArm64Names, | 130 | valuesId = R.array.cpuBackendArm64Values |
| 103 | R.array.cpuBackendArm64Values | ||
| 104 | ) | 131 | ) |
| 105 | ) | 132 | ) |
| 106 | put( | 133 | put( |
| 107 | SingleChoiceSetting( | 134 | SingleChoiceSetting( |
| 108 | IntSetting.CPU_ACCURACY, | 135 | IntSetting.CPU_ACCURACY, |
| 109 | R.string.cpu_accuracy, | 136 | titleId = R.string.cpu_accuracy, |
| 110 | 0, | 137 | choicesId = R.array.cpuAccuracyNames, |
| 111 | R.array.cpuAccuracyNames, | 138 | valuesId = R.array.cpuAccuracyValues |
| 112 | R.array.cpuAccuracyValues | ||
| 113 | ) | 139 | ) |
| 114 | ) | 140 | ) |
| 115 | put( | 141 | put( |
| 116 | SwitchSetting( | 142 | SwitchSetting( |
| 117 | BooleanSetting.PICTURE_IN_PICTURE, | 143 | BooleanSetting.PICTURE_IN_PICTURE, |
| 118 | R.string.picture_in_picture, | 144 | titleId = R.string.picture_in_picture, |
| 119 | R.string.picture_in_picture_description | 145 | descriptionId = R.string.picture_in_picture_description |
| 120 | ) | 146 | ) |
| 121 | ) | 147 | ) |
| 148 | |||
| 149 | val dockedModeSetting = object : AbstractBooleanSetting { | ||
| 150 | override val key = BooleanSetting.USE_DOCKED_MODE.key | ||
| 151 | |||
| 152 | override fun getBoolean(needsGlobal: Boolean): Boolean { | ||
| 153 | if (NativeInput.getStyleIndex(0) == NpadStyleIndex.Handheld) { | ||
| 154 | return false | ||
| 155 | } | ||
| 156 | return BooleanSetting.USE_DOCKED_MODE.getBoolean(needsGlobal) | ||
| 157 | } | ||
| 158 | |||
| 159 | override fun setBoolean(value: Boolean) = | ||
| 160 | BooleanSetting.USE_DOCKED_MODE.setBoolean(value) | ||
| 161 | |||
| 162 | override val defaultValue = BooleanSetting.USE_DOCKED_MODE.defaultValue | ||
| 163 | |||
| 164 | override fun getValueAsString(needsGlobal: Boolean): String = | ||
| 165 | BooleanSetting.USE_DOCKED_MODE.getValueAsString(needsGlobal) | ||
| 166 | |||
| 167 | override fun reset() = BooleanSetting.USE_DOCKED_MODE.reset() | ||
| 168 | } | ||
| 122 | put( | 169 | put( |
| 123 | SwitchSetting( | 170 | SwitchSetting( |
| 124 | BooleanSetting.USE_DOCKED_MODE, | 171 | dockedModeSetting, |
| 125 | R.string.use_docked_mode, | 172 | titleId = R.string.use_docked_mode, |
| 126 | R.string.use_docked_mode_description | 173 | descriptionId = R.string.use_docked_mode_description |
| 127 | ) | 174 | ) |
| 128 | ) | 175 | ) |
| 176 | |||
| 129 | put( | 177 | put( |
| 130 | SingleChoiceSetting( | 178 | SingleChoiceSetting( |
| 131 | IntSetting.REGION_INDEX, | 179 | IntSetting.REGION_INDEX, |
| 132 | R.string.emulated_region, | 180 | titleId = R.string.emulated_region, |
| 133 | 0, | 181 | choicesId = R.array.regionNames, |
| 134 | R.array.regionNames, | 182 | valuesId = R.array.regionValues |
| 135 | R.array.regionValues | ||
| 136 | ) | 183 | ) |
| 137 | ) | 184 | ) |
| 138 | put( | 185 | put( |
| 139 | SingleChoiceSetting( | 186 | SingleChoiceSetting( |
| 140 | IntSetting.LANGUAGE_INDEX, | 187 | IntSetting.LANGUAGE_INDEX, |
| 141 | R.string.emulated_language, | 188 | titleId = R.string.emulated_language, |
| 142 | 0, | 189 | choicesId = R.array.languageNames, |
| 143 | R.array.languageNames, | 190 | valuesId = R.array.languageValues |
| 144 | R.array.languageValues | ||
| 145 | ) | 191 | ) |
| 146 | ) | 192 | ) |
| 147 | put( | 193 | put( |
| 148 | SwitchSetting( | 194 | SwitchSetting( |
| 149 | BooleanSetting.USE_CUSTOM_RTC, | 195 | BooleanSetting.USE_CUSTOM_RTC, |
| 150 | R.string.use_custom_rtc, | 196 | titleId = R.string.use_custom_rtc, |
| 151 | R.string.use_custom_rtc_description | 197 | descriptionId = R.string.use_custom_rtc_description |
| 152 | ) | 198 | ) |
| 153 | ) | 199 | ) |
| 154 | put(DateTimeSetting(LongSetting.CUSTOM_RTC, R.string.set_custom_rtc, 0)) | 200 | put(DateTimeSetting(LongSetting.CUSTOM_RTC, titleId = R.string.set_custom_rtc)) |
| 155 | put( | 201 | put( |
| 156 | SingleChoiceSetting( | 202 | SingleChoiceSetting( |
| 157 | IntSetting.RENDERER_ACCURACY, | 203 | IntSetting.RENDERER_ACCURACY, |
| 158 | R.string.renderer_accuracy, | 204 | titleId = R.string.renderer_accuracy, |
| 159 | 0, | 205 | choicesId = R.array.rendererAccuracyNames, |
| 160 | R.array.rendererAccuracyNames, | 206 | valuesId = R.array.rendererAccuracyValues |
| 161 | R.array.rendererAccuracyValues | ||
| 162 | ) | 207 | ) |
| 163 | ) | 208 | ) |
| 164 | put( | 209 | put( |
| 165 | SingleChoiceSetting( | 210 | SingleChoiceSetting( |
| 166 | IntSetting.RENDERER_RESOLUTION, | 211 | IntSetting.RENDERER_RESOLUTION, |
| 167 | R.string.renderer_resolution, | 212 | titleId = R.string.renderer_resolution, |
| 168 | 0, | 213 | choicesId = R.array.rendererResolutionNames, |
| 169 | R.array.rendererResolutionNames, | 214 | valuesId = R.array.rendererResolutionValues |
| 170 | R.array.rendererResolutionValues | ||
| 171 | ) | 215 | ) |
| 172 | ) | 216 | ) |
| 173 | put( | 217 | put( |
| 174 | SingleChoiceSetting( | 218 | SingleChoiceSetting( |
| 175 | IntSetting.RENDERER_VSYNC, | 219 | IntSetting.RENDERER_VSYNC, |
| 176 | R.string.renderer_vsync, | 220 | titleId = R.string.renderer_vsync, |
| 177 | 0, | 221 | choicesId = R.array.rendererVSyncNames, |
| 178 | R.array.rendererVSyncNames, | 222 | valuesId = R.array.rendererVSyncValues |
| 179 | R.array.rendererVSyncValues | ||
| 180 | ) | 223 | ) |
| 181 | ) | 224 | ) |
| 182 | put( | 225 | put( |
| 183 | SingleChoiceSetting( | 226 | SingleChoiceSetting( |
| 184 | IntSetting.RENDERER_SCALING_FILTER, | 227 | IntSetting.RENDERER_SCALING_FILTER, |
| 185 | R.string.renderer_scaling_filter, | 228 | titleId = R.string.renderer_scaling_filter, |
| 186 | 0, | 229 | choicesId = R.array.rendererScalingFilterNames, |
| 187 | R.array.rendererScalingFilterNames, | 230 | valuesId = R.array.rendererScalingFilterValues |
| 188 | R.array.rendererScalingFilterValues | ||
| 189 | ) | 231 | ) |
| 190 | ) | 232 | ) |
| 191 | put( | 233 | put( |
| 192 | SliderSetting( | 234 | SliderSetting( |
| 193 | IntSetting.FSR_SHARPENING_SLIDER, | 235 | IntSetting.FSR_SHARPENING_SLIDER, |
| 194 | R.string.fsr_sharpness, | 236 | titleId = R.string.fsr_sharpness, |
| 195 | R.string.fsr_sharpness_description, | 237 | descriptionId = R.string.fsr_sharpness_description, |
| 196 | 0, | 238 | units = "%" |
| 197 | 100, | ||
| 198 | "%" | ||
| 199 | ) | 239 | ) |
| 200 | ) | 240 | ) |
| 201 | put( | 241 | put( |
| 202 | SingleChoiceSetting( | 242 | SingleChoiceSetting( |
| 203 | IntSetting.RENDERER_ANTI_ALIASING, | 243 | IntSetting.RENDERER_ANTI_ALIASING, |
| 204 | R.string.renderer_anti_aliasing, | 244 | titleId = R.string.renderer_anti_aliasing, |
| 205 | 0, | 245 | choicesId = R.array.rendererAntiAliasingNames, |
| 206 | R.array.rendererAntiAliasingNames, | 246 | valuesId = R.array.rendererAntiAliasingValues |
| 207 | R.array.rendererAntiAliasingValues | ||
| 208 | ) | 247 | ) |
| 209 | ) | 248 | ) |
| 210 | put( | 249 | put( |
| 211 | SingleChoiceSetting( | 250 | SingleChoiceSetting( |
| 212 | IntSetting.RENDERER_SCREEN_LAYOUT, | 251 | IntSetting.RENDERER_SCREEN_LAYOUT, |
| 213 | R.string.renderer_screen_layout, | 252 | titleId = R.string.renderer_screen_layout, |
| 214 | 0, | 253 | choicesId = R.array.rendererScreenLayoutNames, |
| 215 | R.array.rendererScreenLayoutNames, | 254 | valuesId = R.array.rendererScreenLayoutValues |
| 216 | R.array.rendererScreenLayoutValues | ||
| 217 | ) | 255 | ) |
| 218 | ) | 256 | ) |
| 219 | put( | 257 | put( |
| 220 | SingleChoiceSetting( | 258 | SingleChoiceSetting( |
| 221 | IntSetting.RENDERER_ASPECT_RATIO, | 259 | IntSetting.RENDERER_ASPECT_RATIO, |
| 222 | R.string.renderer_aspect_ratio, | 260 | titleId = R.string.renderer_aspect_ratio, |
| 223 | 0, | 261 | choicesId = R.array.rendererAspectRatioNames, |
| 224 | R.array.rendererAspectRatioNames, | 262 | valuesId = R.array.rendererAspectRatioValues |
| 225 | R.array.rendererAspectRatioValues | ||
| 226 | ) | 263 | ) |
| 227 | ) | 264 | ) |
| 228 | put( | 265 | put( |
| 229 | SingleChoiceSetting( | 266 | SingleChoiceSetting( |
| 230 | IntSetting.VERTICAL_ALIGNMENT, | 267 | IntSetting.VERTICAL_ALIGNMENT, |
| 231 | R.string.vertical_alignment, | 268 | titleId = R.string.vertical_alignment, |
| 232 | 0, | 269 | descriptionId = 0, |
| 233 | R.array.verticalAlignmentEntries, | 270 | choicesId = R.array.verticalAlignmentEntries, |
| 234 | R.array.verticalAlignmentValues | 271 | valuesId = R.array.verticalAlignmentValues |
| 235 | ) | 272 | ) |
| 236 | ) | 273 | ) |
| 237 | put( | 274 | put( |
| 238 | SwitchSetting( | 275 | SwitchSetting( |
| 239 | BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE, | 276 | BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE, |
| 240 | R.string.use_disk_shader_cache, | 277 | titleId = R.string.use_disk_shader_cache, |
| 241 | R.string.use_disk_shader_cache_description | 278 | descriptionId = R.string.use_disk_shader_cache_description |
| 242 | ) | 279 | ) |
| 243 | ) | 280 | ) |
| 244 | put( | 281 | put( |
| 245 | SwitchSetting( | 282 | SwitchSetting( |
| 246 | BooleanSetting.RENDERER_FORCE_MAX_CLOCK, | 283 | BooleanSetting.RENDERER_FORCE_MAX_CLOCK, |
| 247 | R.string.renderer_force_max_clock, | 284 | titleId = R.string.renderer_force_max_clock, |
| 248 | R.string.renderer_force_max_clock_description | 285 | descriptionId = R.string.renderer_force_max_clock_description |
| 249 | ) | 286 | ) |
| 250 | ) | 287 | ) |
| 251 | put( | 288 | put( |
| 252 | SwitchSetting( | 289 | SwitchSetting( |
| 253 | BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS, | 290 | BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS, |
| 254 | R.string.renderer_asynchronous_shaders, | 291 | titleId = R.string.renderer_asynchronous_shaders, |
| 255 | R.string.renderer_asynchronous_shaders_description | 292 | descriptionId = R.string.renderer_asynchronous_shaders_description |
| 256 | ) | 293 | ) |
| 257 | ) | 294 | ) |
| 258 | put( | 295 | put( |
| 259 | SwitchSetting( | 296 | SwitchSetting( |
| 260 | BooleanSetting.RENDERER_REACTIVE_FLUSHING, | 297 | BooleanSetting.RENDERER_REACTIVE_FLUSHING, |
| 261 | R.string.renderer_reactive_flushing, | 298 | titleId = R.string.renderer_reactive_flushing, |
| 262 | R.string.renderer_reactive_flushing_description | 299 | descriptionId = R.string.renderer_reactive_flushing_description |
| 263 | ) | 300 | ) |
| 264 | ) | 301 | ) |
| 265 | put( | 302 | put( |
| 266 | SingleChoiceSetting( | 303 | SingleChoiceSetting( |
| 267 | IntSetting.MAX_ANISOTROPY, | 304 | IntSetting.MAX_ANISOTROPY, |
| 268 | R.string.anisotropic_filtering, | 305 | titleId = R.string.anisotropic_filtering, |
| 269 | R.string.anisotropic_filtering_description, | 306 | descriptionId = R.string.anisotropic_filtering_description, |
| 270 | R.array.anisoEntries, | 307 | choicesId = R.array.anisoEntries, |
| 271 | R.array.anisoValues | 308 | valuesId = R.array.anisoValues |
| 272 | ) | 309 | ) |
| 273 | ) | 310 | ) |
| 274 | put( | 311 | put( |
| 275 | SingleChoiceSetting( | 312 | SingleChoiceSetting( |
| 276 | IntSetting.AUDIO_OUTPUT_ENGINE, | 313 | IntSetting.AUDIO_OUTPUT_ENGINE, |
| 277 | R.string.audio_output_engine, | 314 | titleId = R.string.audio_output_engine, |
| 278 | 0, | 315 | choicesId = R.array.outputEngineEntries, |
| 279 | R.array.outputEngineEntries, | 316 | valuesId = R.array.outputEngineValues |
| 280 | R.array.outputEngineValues | ||
| 281 | ) | 317 | ) |
| 282 | ) | 318 | ) |
| 283 | put( | 319 | put( |
| 284 | SliderSetting( | 320 | SliderSetting( |
| 285 | ByteSetting.AUDIO_VOLUME, | 321 | ByteSetting.AUDIO_VOLUME, |
| 286 | R.string.audio_volume, | 322 | titleId = R.string.audio_volume, |
| 287 | R.string.audio_volume_description, | 323 | descriptionId = R.string.audio_volume_description, |
| 288 | 0, | 324 | units = "%" |
| 289 | 100, | ||
| 290 | "%" | ||
| 291 | ) | 325 | ) |
| 292 | ) | 326 | ) |
| 293 | put( | 327 | put( |
| 294 | SingleChoiceSetting( | 328 | SingleChoiceSetting( |
| 295 | IntSetting.RENDERER_BACKEND, | 329 | IntSetting.RENDERER_BACKEND, |
| 296 | R.string.renderer_api, | 330 | titleId = R.string.renderer_api, |
| 297 | 0, | 331 | choicesId = R.array.rendererApiNames, |
| 298 | R.array.rendererApiNames, | 332 | valuesId = R.array.rendererApiValues |
| 299 | R.array.rendererApiValues | ||
| 300 | ) | 333 | ) |
| 301 | ) | 334 | ) |
| 302 | put( | 335 | put( |
| 303 | SwitchSetting( | 336 | SwitchSetting( |
| 304 | BooleanSetting.RENDERER_DEBUG, | 337 | BooleanSetting.RENDERER_DEBUG, |
| 305 | R.string.renderer_debug, | 338 | titleId = R.string.renderer_debug, |
| 306 | R.string.renderer_debug_description | 339 | descriptionId = R.string.renderer_debug_description |
| 307 | ) | 340 | ) |
| 308 | ) | 341 | ) |
| 309 | put( | 342 | put( |
| 310 | SwitchSetting( | 343 | SwitchSetting( |
| 311 | BooleanSetting.CPU_DEBUG_MODE, | 344 | BooleanSetting.CPU_DEBUG_MODE, |
| 312 | R.string.cpu_debug_mode, | 345 | titleId = R.string.cpu_debug_mode, |
| 313 | R.string.cpu_debug_mode_description | 346 | descriptionId = R.string.cpu_debug_mode_description |
| 314 | ) | 347 | ) |
| 315 | ) | 348 | ) |
| 316 | 349 | ||
| @@ -346,7 +379,7 @@ abstract class SettingsItem( | |||
| 346 | 379 | ||
| 347 | override fun reset() = setBoolean(defaultValue) | 380 | override fun reset() = setBoolean(defaultValue) |
| 348 | } | 381 | } |
| 349 | put(SwitchSetting(fastmem, R.string.fastmem, 0)) | 382 | put(SwitchSetting(fastmem, R.string.fastmem)) |
| 350 | } | 383 | } |
| 351 | } | 384 | } |
| 352 | } | 385 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt index 97a5a9e59..ea5e099ed 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt | |||
| @@ -3,16 +3,20 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.features.settings.model.view | 4 | package org.yuzu.yuzu_emu.features.settings.model.view |
| 5 | 5 | ||
| 6 | import androidx.annotation.ArrayRes | ||
| 7 | import androidx.annotation.StringRes | ||
| 6 | import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting | 8 | import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting |
| 7 | import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting | 9 | import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting |
| 8 | 10 | ||
| 9 | class SingleChoiceSetting( | 11 | class SingleChoiceSetting( |
| 10 | setting: AbstractSetting, | 12 | setting: AbstractSetting, |
| 11 | titleId: Int, | 13 | @StringRes titleId: Int = 0, |
| 12 | descriptionId: Int, | 14 | titleString: String = "", |
| 13 | val choicesId: Int, | 15 | @StringRes descriptionId: Int = 0, |
| 14 | val valuesId: Int | 16 | descriptionString: String = "", |
| 15 | ) : SettingsItem(setting, titleId, descriptionId) { | 17 | @ArrayRes val choicesId: Int, |
| 18 | @ArrayRes val valuesId: Int | ||
| 19 | ) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) { | ||
| 16 | override val type = TYPE_SINGLE_CHOICE | 20 | override val type = TYPE_SINGLE_CHOICE |
| 17 | 21 | ||
| 18 | fun getSelectedValue(needsGlobal: Boolean = false) = | 22 | fun getSelectedValue(needsGlobal: Boolean = false) = |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt index b9b709bf7..6a5cdf48b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt | |||
| @@ -3,6 +3,7 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.features.settings.model.view | 4 | package org.yuzu.yuzu_emu.features.settings.model.view |
| 5 | 5 | ||
| 6 | import androidx.annotation.StringRes | ||
| 6 | import org.yuzu.yuzu_emu.features.settings.model.AbstractByteSetting | 7 | import org.yuzu.yuzu_emu.features.settings.model.AbstractByteSetting |
| 7 | import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting | 8 | import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting |
| 8 | import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting | 9 | import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting |
| @@ -12,12 +13,14 @@ import kotlin.math.roundToInt | |||
| 12 | 13 | ||
| 13 | class SliderSetting( | 14 | class SliderSetting( |
| 14 | setting: AbstractSetting, | 15 | setting: AbstractSetting, |
| 15 | titleId: Int, | 16 | @StringRes titleId: Int = 0, |
| 16 | descriptionId: Int, | 17 | titleString: String = "", |
| 17 | val min: Int, | 18 | @StringRes descriptionId: Int = 0, |
| 18 | val max: Int, | 19 | descriptionString: String = "", |
| 19 | val units: String | 20 | val min: Int = 0, |
| 20 | ) : SettingsItem(setting, titleId, descriptionId) { | 21 | val max: Int = 100, |
| 22 | val units: String = "" | ||
| 23 | ) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) { | ||
| 21 | override val type = TYPE_SLIDER | 24 | override val type = TYPE_SLIDER |
| 22 | 25 | ||
| 23 | fun getSelectedValue(needsGlobal: Boolean = false) = | 26 | fun getSelectedValue(needsGlobal: Boolean = false) = |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt index ba7920f50..5260ff4dc 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt | |||
| @@ -3,15 +3,18 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.features.settings.model.view | 4 | package org.yuzu.yuzu_emu.features.settings.model.view |
| 5 | 5 | ||
| 6 | import androidx.annotation.StringRes | ||
| 6 | import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting | 7 | import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting |
| 7 | 8 | ||
| 8 | class StringSingleChoiceSetting( | 9 | class StringSingleChoiceSetting( |
| 9 | private val stringSetting: AbstractStringSetting, | 10 | private val stringSetting: AbstractStringSetting, |
| 10 | titleId: Int, | 11 | @StringRes titleId: Int = 0, |
| 11 | descriptionId: Int, | 12 | titleString: String = "", |
| 13 | @StringRes descriptionId: Int = 0, | ||
| 14 | descriptionString: String = "", | ||
| 12 | val choices: Array<String>, | 15 | val choices: Array<String>, |
| 13 | val values: Array<String> | 16 | val values: Array<String> |
| 14 | ) : SettingsItem(stringSetting, titleId, descriptionId) { | 17 | ) : SettingsItem(stringSetting, titleId, titleString, descriptionId, descriptionString) { |
| 15 | override val type = TYPE_STRING_SINGLE_CHOICE | 18 | override val type = TYPE_STRING_SINGLE_CHOICE |
| 16 | 19 | ||
| 17 | fun getValueAt(index: Int): String = | 20 | fun getValueAt(index: Int): String = |
| @@ -20,7 +23,7 @@ class StringSingleChoiceSetting( | |||
| 20 | fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal) | 23 | fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal) |
| 21 | fun setSelectedValue(value: String) = stringSetting.setString(value) | 24 | fun setSelectedValue(value: String) = stringSetting.setString(value) |
| 22 | 25 | ||
| 23 | val selectValueIndex: Int | 26 | val selectedValueIndex: Int |
| 24 | get() { | 27 | get() { |
| 25 | for (i in values.indices) { | 28 | for (i in values.indices) { |
| 26 | if (values[i] == getSelectedValue()) { | 29 | if (values[i] == getSelectedValue()) { |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt index 94953b18a..c722393dd 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt | |||
| @@ -8,10 +8,12 @@ import androidx.annotation.StringRes | |||
| 8 | import org.yuzu.yuzu_emu.features.settings.model.Settings | 8 | import org.yuzu.yuzu_emu.features.settings.model.Settings |
| 9 | 9 | ||
| 10 | class SubmenuSetting( | 10 | class SubmenuSetting( |
| 11 | @StringRes titleId: Int, | 11 | @StringRes titleId: Int = 0, |
| 12 | @StringRes descriptionId: Int, | 12 | titleString: String = "", |
| 13 | @DrawableRes val iconId: Int, | 13 | @StringRes descriptionId: Int = 0, |
| 14 | descriptionString: String = "", | ||
| 15 | @DrawableRes val iconId: Int = 0, | ||
| 14 | val menuKey: Settings.MenuTag | 16 | val menuKey: Settings.MenuTag |
| 15 | ) : SettingsItem(emptySetting, titleId, descriptionId) { | 17 | ) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) { |
| 16 | override val type = TYPE_SUBMENU | 18 | override val type = TYPE_SUBMENU |
| 17 | } | 19 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt index 44d47dd69..4984bf52e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt | |||
| @@ -3,15 +3,18 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.features.settings.model.view | 4 | package org.yuzu.yuzu_emu.features.settings.model.view |
| 5 | 5 | ||
| 6 | import androidx.annotation.StringRes | ||
| 6 | import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting | 7 | import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting |
| 7 | import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting | 8 | import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting |
| 8 | import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting | 9 | import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting |
| 9 | 10 | ||
| 10 | class SwitchSetting( | 11 | class SwitchSetting( |
| 11 | setting: AbstractSetting, | 12 | setting: AbstractSetting, |
| 12 | titleId: Int, | 13 | @StringRes titleId: Int = 0, |
| 13 | descriptionId: Int | 14 | titleString: String = "", |
| 14 | ) : SettingsItem(setting, titleId, descriptionId) { | 15 | @StringRes descriptionId: Int = 0, |
| 16 | descriptionString: String = "" | ||
| 17 | ) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) { | ||
| 15 | override val type = TYPE_SWITCH | 18 | override val type = TYPE_SWITCH |
| 16 | 19 | ||
| 17 | fun getIsChecked(needsGlobal: Boolean = false): Boolean { | 20 | fun getIsChecked(needsGlobal: Boolean = false): Boolean { |
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..1bae593ae --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt | |||
| @@ -0,0 +1,148 @@ | |||
| 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.recyclerview.widget.LinearLayoutManager | ||
| 15 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||
| 16 | import org.yuzu.yuzu_emu.R | ||
| 17 | import org.yuzu.yuzu_emu.databinding.DialogInputProfilesBinding | ||
| 18 | import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting | ||
| 19 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment | ||
| 20 | import org.yuzu.yuzu_emu.utils.collect | ||
| 21 | |||
| 22 | class InputProfileDialogFragment : DialogFragment() { | ||
| 23 | private var position = 0 | ||
| 24 | |||
| 25 | private val settingsViewModel: SettingsViewModel by activityViewModels() | ||
| 26 | |||
| 27 | private lateinit var binding: DialogInputProfilesBinding | ||
| 28 | |||
| 29 | private lateinit var setting: InputProfileSetting | ||
| 30 | |||
| 31 | override fun onCreate(savedInstanceState: Bundle?) { | ||
| 32 | super.onCreate(savedInstanceState) | ||
| 33 | position = requireArguments().getInt(POSITION) | ||
| 34 | } | ||
| 35 | |||
| 36 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { | ||
| 37 | binding = DialogInputProfilesBinding.inflate(layoutInflater) | ||
| 38 | |||
| 39 | setting = settingsViewModel.clickedItem as InputProfileSetting | ||
| 40 | val options = mutableListOf<ProfileItem>().apply { | ||
| 41 | add( | ||
| 42 | NewProfileItem( | ||
| 43 | createNewProfile = { | ||
| 44 | NewInputProfileDialogFragment.newInstance( | ||
| 45 | settingsViewModel, | ||
| 46 | setting, | ||
| 47 | position | ||
| 48 | ).show(parentFragmentManager, NewInputProfileDialogFragment.TAG) | ||
| 49 | dismiss() | ||
| 50 | } | ||
| 51 | ) | ||
| 52 | ) | ||
| 53 | |||
| 54 | val onActionDismiss = { | ||
| 55 | settingsViewModel.setReloadListAndNotifyDataset(true) | ||
| 56 | dismiss() | ||
| 57 | } | ||
| 58 | setting.getProfileNames().forEach { | ||
| 59 | add( | ||
| 60 | ExistingProfileItem( | ||
| 61 | it, | ||
| 62 | deleteProfile = { | ||
| 63 | settingsViewModel.setShouldShowDeleteProfileDialog(it) | ||
| 64 | }, | ||
| 65 | saveProfile = { | ||
| 66 | if (!setting.saveProfile(it)) { | ||
| 67 | Toast.makeText( | ||
| 68 | requireContext(), | ||
| 69 | R.string.failed_to_save_profile, | ||
| 70 | Toast.LENGTH_SHORT | ||
| 71 | ).show() | ||
| 72 | } | ||
| 73 | onActionDismiss.invoke() | ||
| 74 | }, | ||
| 75 | loadProfile = { | ||
| 76 | if (!setting.loadProfile(it)) { | ||
| 77 | Toast.makeText( | ||
| 78 | requireContext(), | ||
| 79 | R.string.failed_to_load_profile, | ||
| 80 | Toast.LENGTH_SHORT | ||
| 81 | ).show() | ||
| 82 | } | ||
| 83 | onActionDismiss.invoke() | ||
| 84 | } | ||
| 85 | ) | ||
| 86 | ) | ||
| 87 | } | ||
| 88 | } | ||
| 89 | binding.listProfiles.apply { | ||
| 90 | layoutManager = LinearLayoutManager(requireContext()) | ||
| 91 | adapter = InputProfileAdapter(options) | ||
| 92 | } | ||
| 93 | |||
| 94 | return MaterialAlertDialogBuilder(requireContext()) | ||
| 95 | .setView(binding.root) | ||
| 96 | .create() | ||
| 97 | } | ||
| 98 | |||
| 99 | override fun onCreateView( | ||
| 100 | inflater: LayoutInflater, | ||
| 101 | container: ViewGroup?, | ||
| 102 | savedInstanceState: Bundle? | ||
| 103 | ): View { | ||
| 104 | return binding.root | ||
| 105 | } | ||
| 106 | |||
| 107 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| 108 | super.onViewCreated(view, savedInstanceState) | ||
| 109 | |||
| 110 | settingsViewModel.shouldShowDeleteProfileDialog.collect(viewLifecycleOwner) { | ||
| 111 | if (it.isNotEmpty()) { | ||
| 112 | MessageDialogFragment.newInstance( | ||
| 113 | activity = requireActivity(), | ||
| 114 | titleId = R.string.delete_input_profile, | ||
| 115 | descriptionId = R.string.delete_input_profile_description, | ||
| 116 | positiveAction = { | ||
| 117 | setting.deleteProfile(it) | ||
| 118 | settingsViewModel.setReloadListAndNotifyDataset(true) | ||
| 119 | }, | ||
| 120 | negativeAction = {}, | ||
| 121 | negativeButtonTitleId = android.R.string.cancel | ||
| 122 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 123 | settingsViewModel.setShouldShowDeleteProfileDialog("") | ||
| 124 | dismiss() | ||
| 125 | } | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 129 | companion object { | ||
| 130 | const val TAG = "InputProfileDialogFragment" | ||
| 131 | |||
| 132 | const val POSITION = "Position" | ||
| 133 | |||
| 134 | fun newInstance( | ||
| 135 | settingsViewModel: SettingsViewModel, | ||
| 136 | profileSetting: InputProfileSetting, | ||
| 137 | position: Int | ||
| 138 | ): InputProfileDialogFragment { | ||
| 139 | settingsViewModel.clickedItem = profileSetting | ||
| 140 | |||
| 141 | val args = Bundle() | ||
| 142 | args.putInt(POSITION, position) | ||
| 143 | val fragment = InputProfileDialogFragment() | ||
| 144 | fragment.arguments = args | ||
| 145 | return fragment | ||
| 146 | } | ||
| 147 | } | ||
| 148 | } | ||
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..455b3b5ff 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 | |||
| @@ -13,21 +13,16 @@ import androidx.appcompat.app.AppCompatActivity | |||
| 13 | import androidx.core.view.ViewCompat | 13 | import androidx.core.view.ViewCompat |
| 14 | import androidx.core.view.WindowCompat | 14 | import androidx.core.view.WindowCompat |
| 15 | import androidx.core.view.WindowInsetsCompat | 15 | import androidx.core.view.WindowInsetsCompat |
| 16 | import androidx.lifecycle.Lifecycle | ||
| 17 | import androidx.lifecycle.lifecycleScope | ||
| 18 | import androidx.lifecycle.repeatOnLifecycle | ||
| 19 | import androidx.navigation.fragment.NavHostFragment | 16 | import androidx.navigation.fragment.NavHostFragment |
| 20 | import androidx.navigation.navArgs | 17 | import androidx.navigation.navArgs |
| 21 | import com.google.android.material.color.MaterialColors | 18 | import com.google.android.material.color.MaterialColors |
| 22 | import kotlinx.coroutines.flow.collectLatest | ||
| 23 | import kotlinx.coroutines.launch | ||
| 24 | import org.yuzu.yuzu_emu.NativeLibrary | 19 | import org.yuzu.yuzu_emu.NativeLibrary |
| 25 | import java.io.IOException | 20 | import java.io.IOException |
| 26 | import org.yuzu.yuzu_emu.R | 21 | import org.yuzu.yuzu_emu.R |
| 27 | import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding | 22 | import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding |
| 23 | import org.yuzu.yuzu_emu.features.input.NativeInput | ||
| 28 | import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile | 24 | import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile |
| 29 | import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment | 25 | import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment |
| 30 | import org.yuzu.yuzu_emu.model.SettingsViewModel | ||
| 31 | import org.yuzu.yuzu_emu.utils.* | 26 | import org.yuzu.yuzu_emu.utils.* |
| 32 | 27 | ||
| 33 | class SettingsActivity : AppCompatActivity() { | 28 | class SettingsActivity : AppCompatActivity() { |
| @@ -70,39 +65,23 @@ class SettingsActivity : AppCompatActivity() { | |||
| 70 | ) | 65 | ) |
| 71 | } | 66 | } |
| 72 | 67 | ||
| 73 | lifecycleScope.apply { | 68 | settingsViewModel.shouldRecreate.collect( |
| 74 | launch { | 69 | this, |
| 75 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 70 | resetState = { settingsViewModel.setShouldRecreate(false) } |
| 76 | settingsViewModel.shouldRecreate.collectLatest { | 71 | ) { if (it) recreate() } |
| 77 | if (it) { | 72 | settingsViewModel.shouldNavigateBack.collect( |
| 78 | settingsViewModel.setShouldRecreate(false) | 73 | this, |
| 79 | recreate() | 74 | resetState = { settingsViewModel.setShouldNavigateBack(false) } |
| 80 | } | 75 | ) { if (it) navigateBack() } |
| 81 | } | 76 | settingsViewModel.shouldShowResetSettingsDialog.collect( |
| 82 | } | 77 | this, |
| 83 | } | 78 | resetState = { settingsViewModel.setShouldShowResetSettingsDialog(false) } |
| 84 | launch { | 79 | ) { |
| 85 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 80 | if (it) { |
| 86 | settingsViewModel.shouldNavigateBack.collectLatest { | 81 | ResetSettingsDialogFragment().show( |
| 87 | if (it) { | 82 | supportFragmentManager, |
| 88 | settingsViewModel.setShouldNavigateBack(false) | 83 | ResetSettingsDialogFragment.TAG |
| 89 | navigateBack() | 84 | ) |
| 90 | } | ||
| 91 | } | ||
| 92 | } | ||
| 93 | } | ||
| 94 | launch { | ||
| 95 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 96 | settingsViewModel.shouldShowResetSettingsDialog.collectLatest { | ||
| 97 | if (it) { | ||
| 98 | settingsViewModel.setShouldShowResetSettingsDialog(false) | ||
| 99 | ResetSettingsDialogFragment().show( | ||
| 100 | supportFragmentManager, | ||
| 101 | ResetSettingsDialogFragment.TAG | ||
| 102 | ) | ||
| 103 | } | ||
| 104 | } | ||
| 105 | } | ||
| 106 | } | 85 | } |
| 107 | } | 86 | } |
| 108 | 87 | ||
| @@ -137,6 +116,7 @@ class SettingsActivity : AppCompatActivity() { | |||
| 137 | super.onStop() | 116 | super.onStop() |
| 138 | Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...") | 117 | Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...") |
| 139 | if (isFinishing) { | 118 | if (isFinishing) { |
| 119 | NativeInput.reloadInputDevices() | ||
| 140 | NativeLibrary.applySettings() | 120 | NativeLibrary.applySettings() |
| 141 | if (args.game == null) { | 121 | if (args.game == null) { |
| 142 | NativeConfig.saveGlobalConfig() | 122 | 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..a81ff6b1a 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 |
| @@ -11,19 +11,21 @@ import android.view.View | |||
| 11 | import android.view.ViewGroup | 11 | import android.view.ViewGroup |
| 12 | import androidx.fragment.app.DialogFragment | 12 | import androidx.fragment.app.DialogFragment |
| 13 | import androidx.fragment.app.activityViewModels | 13 | import androidx.fragment.app.activityViewModels |
| 14 | import androidx.lifecycle.Lifecycle | ||
| 15 | import androidx.lifecycle.lifecycleScope | ||
| 16 | import androidx.lifecycle.repeatOnLifecycle | ||
| 17 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | 14 | import com.google.android.material.dialog.MaterialAlertDialogBuilder |
| 18 | import com.google.android.material.slider.Slider | 15 | import com.google.android.material.slider.Slider |
| 19 | import kotlinx.coroutines.launch | ||
| 20 | import org.yuzu.yuzu_emu.R | 16 | import org.yuzu.yuzu_emu.R |
| 21 | import org.yuzu.yuzu_emu.databinding.DialogSliderBinding | 17 | import org.yuzu.yuzu_emu.databinding.DialogSliderBinding |
| 18 | import org.yuzu.yuzu_emu.features.input.NativeInput | ||
| 19 | import org.yuzu.yuzu_emu.features.input.model.AnalogDirection | ||
| 20 | import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting | ||
| 21 | import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting | ||
| 22 | import org.yuzu.yuzu_emu.features.settings.model.view.IntSingleChoiceSetting | ||
| 22 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | 23 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem |
| 23 | import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting | 24 | import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting |
| 24 | import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting | 25 | import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting |
| 25 | import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting | 26 | import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting |
| 26 | import org.yuzu.yuzu_emu.model.SettingsViewModel | 27 | import org.yuzu.yuzu_emu.utils.ParamPackage |
| 28 | import org.yuzu.yuzu_emu.utils.collect | ||
| 27 | 29 | ||
| 28 | class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener { | 30 | class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener { |
| 29 | private var type = 0 | 31 | private var type = 0 |
| @@ -50,8 +52,49 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener | |||
| 50 | MaterialAlertDialogBuilder(requireContext()) | 52 | MaterialAlertDialogBuilder(requireContext()) |
| 51 | .setMessage(R.string.reset_setting_confirmation) | 53 | .setMessage(R.string.reset_setting_confirmation) |
| 52 | .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> | 54 | .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> |
| 53 | settingsViewModel.clickedItem!!.setting.reset() | 55 | when (val item = settingsViewModel.clickedItem) { |
| 54 | settingsViewModel.setAdapterItemChanged(position) | 56 | is AnalogInputSetting -> { |
| 57 | val stickParam = NativeInput.getStickParam( | ||
| 58 | item.playerIndex, | ||
| 59 | item.nativeAnalog | ||
| 60 | ) | ||
| 61 | if (stickParam.get("engine", "") == "analog_from_button") { | ||
| 62 | when (item.analogDirection) { | ||
| 63 | AnalogDirection.Up -> stickParam.erase("up") | ||
| 64 | AnalogDirection.Down -> stickParam.erase("down") | ||
| 65 | AnalogDirection.Left -> stickParam.erase("left") | ||
| 66 | AnalogDirection.Right -> stickParam.erase("right") | ||
| 67 | } | ||
| 68 | NativeInput.setStickParam( | ||
| 69 | item.playerIndex, | ||
| 70 | item.nativeAnalog, | ||
| 71 | stickParam | ||
| 72 | ) | ||
| 73 | settingsViewModel.setAdapterItemChanged(position) | ||
| 74 | } else { | ||
| 75 | NativeInput.setStickParam( | ||
| 76 | item.playerIndex, | ||
| 77 | item.nativeAnalog, | ||
| 78 | ParamPackage() | ||
| 79 | ) | ||
| 80 | settingsViewModel.setDatasetChanged(true) | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | is ButtonInputSetting -> { | ||
| 85 | NativeInput.setButtonParam( | ||
| 86 | item.playerIndex, | ||
| 87 | item.nativeButton, | ||
| 88 | ParamPackage() | ||
| 89 | ) | ||
| 90 | settingsViewModel.setAdapterItemChanged(position) | ||
| 91 | } | ||
| 92 | |||
| 93 | else -> { | ||
| 94 | settingsViewModel.clickedItem!!.setting.reset() | ||
| 95 | settingsViewModel.setAdapterItemChanged(position) | ||
| 96 | } | ||
| 97 | } | ||
| 55 | } | 98 | } |
| 56 | .setNegativeButton(android.R.string.cancel, null) | 99 | .setNegativeButton(android.R.string.cancel, null) |
| 57 | .create() | 100 | .create() |
| @@ -61,7 +104,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener | |||
| 61 | val item = settingsViewModel.clickedItem as SingleChoiceSetting | 104 | val item = settingsViewModel.clickedItem as SingleChoiceSetting |
| 62 | val value = getSelectionForSingleChoiceValue(item) | 105 | val value = getSelectionForSingleChoiceValue(item) |
| 63 | MaterialAlertDialogBuilder(requireContext()) | 106 | MaterialAlertDialogBuilder(requireContext()) |
| 64 | .setTitle(item.nameId) | 107 | .setTitle(item.title) |
| 65 | .setSingleChoiceItems(item.choicesId, value, this) | 108 | .setSingleChoiceItems(item.choicesId, value, this) |
| 66 | .create() | 109 | .create() |
| 67 | } | 110 | } |
| @@ -81,7 +124,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener | |||
| 81 | } | 124 | } |
| 82 | 125 | ||
| 83 | MaterialAlertDialogBuilder(requireContext()) | 126 | MaterialAlertDialogBuilder(requireContext()) |
| 84 | .setTitle(item.nameId) | 127 | .setTitle(item.title) |
| 85 | .setView(sliderBinding.root) | 128 | .setView(sliderBinding.root) |
| 86 | .setPositiveButton(android.R.string.ok, this) | 129 | .setPositiveButton(android.R.string.ok, this) |
| 87 | .setNegativeButton(android.R.string.cancel, defaultCancelListener) | 130 | .setNegativeButton(android.R.string.cancel, defaultCancelListener) |
| @@ -91,8 +134,16 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener | |||
| 91 | SettingsItem.TYPE_STRING_SINGLE_CHOICE -> { | 134 | SettingsItem.TYPE_STRING_SINGLE_CHOICE -> { |
| 92 | val item = settingsViewModel.clickedItem as StringSingleChoiceSetting | 135 | val item = settingsViewModel.clickedItem as StringSingleChoiceSetting |
| 93 | MaterialAlertDialogBuilder(requireContext()) | 136 | MaterialAlertDialogBuilder(requireContext()) |
| 94 | .setTitle(item.nameId) | 137 | .setTitle(item.title) |
| 95 | .setSingleChoiceItems(item.choices, item.selectValueIndex, this) | 138 | .setSingleChoiceItems(item.choices, item.selectedValueIndex, this) |
| 139 | .create() | ||
| 140 | } | ||
| 141 | |||
| 142 | SettingsItem.TYPE_INT_SINGLE_CHOICE -> { | ||
| 143 | val item = settingsViewModel.clickedItem as IntSingleChoiceSetting | ||
| 144 | MaterialAlertDialogBuilder(requireContext()) | ||
| 145 | .setTitle(item.title) | ||
| 146 | .setSingleChoiceItems(item.choices, item.selectedValueIndex, this) | ||
| 96 | .create() | 147 | .create() |
| 97 | } | 148 | } |
| 98 | 149 | ||
| @@ -115,17 +166,11 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener | |||
| 115 | super.onViewCreated(view, savedInstanceState) | 166 | super.onViewCreated(view, savedInstanceState) |
| 116 | when (type) { | 167 | when (type) { |
| 117 | SettingsItem.TYPE_SLIDER -> { | 168 | SettingsItem.TYPE_SLIDER -> { |
| 118 | viewLifecycleOwner.lifecycleScope.launch { | 169 | settingsViewModel.sliderTextValue.collect(viewLifecycleOwner) { |
| 119 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 170 | sliderBinding.textValue.text = it |
| 120 | settingsViewModel.sliderTextValue.collect { | 171 | } |
| 121 | sliderBinding.textValue.text = it | 172 | settingsViewModel.sliderProgress.collect(viewLifecycleOwner) { |
| 122 | } | 173 | sliderBinding.slider.value = it.toFloat() |
| 123 | } | ||
| 124 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 125 | settingsViewModel.sliderProgress.collect { | ||
| 126 | sliderBinding.slider.value = it.toFloat() | ||
| 127 | } | ||
| 128 | } | ||
| 129 | } | 174 | } |
| 130 | } | 175 | } |
| 131 | } | 176 | } |
| @@ -145,6 +190,12 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener | |||
| 145 | scSetting.setSelectedValue(value) | 190 | scSetting.setSelectedValue(value) |
| 146 | } | 191 | } |
| 147 | 192 | ||
| 193 | is IntSingleChoiceSetting -> { | ||
| 194 | val scSetting = settingsViewModel.clickedItem as IntSingleChoiceSetting | ||
| 195 | val value = scSetting.getValueAt(which) | ||
| 196 | scSetting.setSelectedValue(value) | ||
| 197 | } | ||
| 198 | |||
| 148 | is SliderSetting -> { | 199 | is SliderSetting -> { |
| 149 | val sliderSetting = settingsViewModel.clickedItem as SliderSetting | 200 | val sliderSetting = settingsViewModel.clickedItem as SliderSetting |
| 150 | sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value) | 201 | 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..ec16f16c4 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 | |||
| @@ -13,20 +13,17 @@ import androidx.core.view.WindowInsetsCompat | |||
| 13 | import androidx.core.view.updatePadding | 13 | import androidx.core.view.updatePadding |
| 14 | import androidx.fragment.app.Fragment | 14 | import androidx.fragment.app.Fragment |
| 15 | import androidx.fragment.app.activityViewModels | 15 | import androidx.fragment.app.activityViewModels |
| 16 | import androidx.lifecycle.Lifecycle | ||
| 17 | import androidx.lifecycle.lifecycleScope | ||
| 18 | import androidx.lifecycle.repeatOnLifecycle | ||
| 19 | import androidx.navigation.findNavController | 16 | import androidx.navigation.findNavController |
| 20 | import androidx.navigation.fragment.navArgs | 17 | import androidx.navigation.fragment.navArgs |
| 21 | import androidx.recyclerview.widget.LinearLayoutManager | 18 | import androidx.recyclerview.widget.LinearLayoutManager |
| 22 | import com.google.android.material.transition.MaterialSharedAxis | 19 | import com.google.android.material.transition.MaterialSharedAxis |
| 23 | import kotlinx.coroutines.flow.collectLatest | ||
| 24 | import kotlinx.coroutines.launch | ||
| 25 | import org.yuzu.yuzu_emu.R | 20 | import org.yuzu.yuzu_emu.R |
| 26 | import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding | 21 | import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding |
| 22 | import org.yuzu.yuzu_emu.features.input.NativeInput | ||
| 27 | import org.yuzu.yuzu_emu.features.settings.model.Settings | 23 | import org.yuzu.yuzu_emu.features.settings.model.Settings |
| 28 | import org.yuzu.yuzu_emu.model.SettingsViewModel | 24 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment |
| 29 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins | 25 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins |
| 26 | import org.yuzu.yuzu_emu.utils.collect | ||
| 30 | 27 | ||
| 31 | class SettingsFragment : Fragment() { | 28 | class SettingsFragment : Fragment() { |
| 32 | private lateinit var presenter: SettingsFragmentPresenter | 29 | private lateinit var presenter: SettingsFragmentPresenter |
| @@ -45,6 +42,12 @@ class SettingsFragment : Fragment() { | |||
| 45 | returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) | 42 | returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) |
| 46 | reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) | 43 | reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) |
| 47 | exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) | 44 | exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) |
| 45 | |||
| 46 | val playerIndex = getPlayerIndex() | ||
| 47 | if (playerIndex != -1) { | ||
| 48 | NativeInput.loadInputProfiles() | ||
| 49 | NativeInput.reloadInputDevices() | ||
| 50 | } | ||
| 48 | } | 51 | } |
| 49 | 52 | ||
| 50 | override fun onCreateView( | 53 | override fun onCreateView( |
| @@ -56,9 +59,9 @@ class SettingsFragment : Fragment() { | |||
| 56 | return binding.root | 59 | return binding.root |
| 57 | } | 60 | } |
| 58 | 61 | ||
| 59 | // This is using the correct scope, lint is just acting up | 62 | @SuppressLint("NotifyDataSetChanged") |
| 60 | @SuppressLint("UnsafeRepeatOnLifecycleDetector") | ||
| 61 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | 63 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| 64 | super.onViewCreated(view, savedInstanceState) | ||
| 62 | settingsAdapter = SettingsAdapter(this, requireContext()) | 65 | settingsAdapter = SettingsAdapter(this, requireContext()) |
| 63 | presenter = SettingsFragmentPresenter( | 66 | presenter = SettingsFragmentPresenter( |
| 64 | settingsViewModel, | 67 | settingsViewModel, |
| @@ -71,7 +74,17 @@ class SettingsFragment : Fragment() { | |||
| 71 | ) { | 74 | ) { |
| 72 | args.game!!.title | 75 | args.game!!.title |
| 73 | } else { | 76 | } else { |
| 74 | getString(args.menuTag.titleId) | 77 | when (args.menuTag) { |
| 78 | Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> Settings.getPlayerString(1) | ||
| 79 | Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> Settings.getPlayerString(2) | ||
| 80 | Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> Settings.getPlayerString(3) | ||
| 81 | Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> Settings.getPlayerString(4) | ||
| 82 | Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> Settings.getPlayerString(5) | ||
| 83 | Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> Settings.getPlayerString(6) | ||
| 84 | Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> Settings.getPlayerString(7) | ||
| 85 | Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> Settings.getPlayerString(8) | ||
| 86 | else -> getString(args.menuTag.titleId) | ||
| 87 | } | ||
| 75 | } | 88 | } |
| 76 | binding.listSettings.apply { | 89 | binding.listSettings.apply { |
| 77 | adapter = settingsAdapter | 90 | adapter = settingsAdapter |
| @@ -82,16 +95,37 @@ class SettingsFragment : Fragment() { | |||
| 82 | settingsViewModel.setShouldNavigateBack(true) | 95 | settingsViewModel.setShouldNavigateBack(true) |
| 83 | } | 96 | } |
| 84 | 97 | ||
| 85 | viewLifecycleOwner.lifecycleScope.apply { | 98 | settingsViewModel.shouldReloadSettingsList.collect( |
| 86 | launch { | 99 | viewLifecycleOwner, |
| 87 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 100 | resetState = { settingsViewModel.setShouldReloadSettingsList(false) } |
| 88 | settingsViewModel.shouldReloadSettingsList.collectLatest { | 101 | ) { if (it) presenter.loadSettingsList() } |
| 89 | if (it) { | 102 | settingsViewModel.adapterItemChanged.collect( |
| 90 | settingsViewModel.setShouldReloadSettingsList(false) | 103 | viewLifecycleOwner, |
| 91 | presenter.loadSettingsList() | 104 | resetState = { settingsViewModel.setAdapterItemChanged(-1) } |
| 92 | } | 105 | ) { if (it != -1) settingsAdapter?.notifyItemChanged(it) } |
| 93 | } | 106 | settingsViewModel.datasetChanged.collect( |
| 94 | } | 107 | viewLifecycleOwner, |
| 108 | resetState = { settingsViewModel.setDatasetChanged(false) } | ||
| 109 | ) { if (it) settingsAdapter?.notifyDataSetChanged() } | ||
| 110 | settingsViewModel.reloadListAndNotifyDataset.collect( | ||
| 111 | viewLifecycleOwner, | ||
| 112 | resetState = { settingsViewModel.setReloadListAndNotifyDataset(false) } | ||
| 113 | ) { if (it) presenter.loadSettingsList(true) } | ||
| 114 | settingsViewModel.shouldShowResetInputDialog.collect( | ||
| 115 | viewLifecycleOwner, | ||
| 116 | resetState = { settingsViewModel.setShouldShowResetInputDialog(false) } | ||
| 117 | ) { | ||
| 118 | if (it) { | ||
| 119 | MessageDialogFragment.newInstance( | ||
| 120 | activity = requireActivity(), | ||
| 121 | titleId = R.string.reset_mapping, | ||
| 122 | descriptionId = R.string.reset_mapping_description, | ||
| 123 | positiveAction = { | ||
| 124 | NativeInput.resetControllerMappings(getPlayerIndex()) | ||
| 125 | settingsViewModel.setReloadListAndNotifyDataset(true) | ||
| 126 | }, | ||
| 127 | negativeAction = {} | ||
| 128 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 95 | } | 129 | } |
| 96 | } | 130 | } |
| 97 | 131 | ||
| @@ -115,6 +149,19 @@ class SettingsFragment : Fragment() { | |||
| 115 | setInsets() | 149 | setInsets() |
| 116 | } | 150 | } |
| 117 | 151 | ||
| 152 | private fun getPlayerIndex(): Int = | ||
| 153 | when (args.menuTag) { | ||
| 154 | Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> 0 | ||
| 155 | Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> 1 | ||
| 156 | Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> 2 | ||
| 157 | Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> 3 | ||
| 158 | Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> 4 | ||
| 159 | Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> 5 | ||
| 160 | Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> 6 | ||
| 161 | Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> 7 | ||
| 162 | else -> -1 | ||
| 163 | } | ||
| 164 | |||
| 118 | private fun setInsets() { | 165 | private fun setInsets() { |
| 119 | ViewCompat.setOnApplyWindowInsetsListener( | 166 | ViewCompat.setOnApplyWindowInsetsListener( |
| 120 | binding.root | 167 | 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 db1a58147..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,73 +62,90 @@ 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>) { |
| 84 | sl.apply { | 110 | sl.apply { |
| 85 | add( | 111 | add( |
| 86 | SubmenuSetting( | 112 | SubmenuSetting( |
| 87 | R.string.preferences_system, | 113 | titleId = R.string.preferences_system, |
| 88 | R.string.preferences_system_description, | 114 | descriptionId = R.string.preferences_system_description, |
| 89 | R.drawable.ic_system_settings, | 115 | iconId = R.drawable.ic_system_settings, |
| 90 | Settings.MenuTag.SECTION_SYSTEM | 116 | menuKey = MenuTag.SECTION_SYSTEM |
| 91 | ) | 117 | ) |
| 92 | ) | 118 | ) |
| 93 | add( | 119 | add( |
| 94 | SubmenuSetting( | 120 | SubmenuSetting( |
| 95 | R.string.preferences_graphics, | 121 | titleId = R.string.preferences_graphics, |
| 96 | R.string.preferences_graphics_description, | 122 | descriptionId = R.string.preferences_graphics_description, |
| 97 | R.drawable.ic_graphics, | 123 | iconId = R.drawable.ic_graphics, |
| 98 | Settings.MenuTag.SECTION_RENDERER | 124 | menuKey = MenuTag.SECTION_RENDERER |
| 99 | ) | 125 | ) |
| 100 | ) | 126 | ) |
| 101 | add( | 127 | add( |
| 102 | SubmenuSetting( | 128 | SubmenuSetting( |
| 103 | R.string.preferences_audio, | 129 | titleId = R.string.preferences_audio, |
| 104 | R.string.preferences_audio_description, | 130 | descriptionId = R.string.preferences_audio_description, |
| 105 | R.drawable.ic_audio, | 131 | iconId = R.drawable.ic_audio, |
| 106 | Settings.MenuTag.SECTION_AUDIO | 132 | menuKey = MenuTag.SECTION_AUDIO |
| 107 | ) | 133 | ) |
| 108 | ) | 134 | ) |
| 109 | add( | 135 | add( |
| 110 | SubmenuSetting( | 136 | SubmenuSetting( |
| 111 | R.string.preferences_debug, | 137 | titleId = R.string.preferences_debug, |
| 112 | R.string.preferences_debug_description, | 138 | descriptionId = R.string.preferences_debug_description, |
| 113 | R.drawable.ic_code, | 139 | iconId = R.drawable.ic_code, |
| 114 | Settings.MenuTag.SECTION_DEBUG | 140 | menuKey = MenuTag.SECTION_DEBUG |
| 115 | ) | 141 | ) |
| 116 | ) | 142 | ) |
| 117 | add( | 143 | add( |
| 118 | RunnableSetting( | 144 | RunnableSetting( |
| 119 | R.string.reset_to_default, | 145 | titleId = R.string.reset_to_default, |
| 120 | R.string.reset_to_default_description, | 146 | descriptionId = R.string.reset_to_default_description, |
| 121 | false, | 147 | isRunnable = !NativeLibrary.isRunning(), |
| 122 | R.drawable.ic_restore | 148 | iconId = R.drawable.ic_restore |
| 123 | ) { settingsViewModel.setShouldShowResetSettingsDialog(true) } | 149 | ) { settingsViewModel.setShouldShowResetSettingsDialog(true) } |
| 124 | ) | 150 | ) |
| 125 | } | 151 | } |
| @@ -164,6 +190,671 @@ class SettingsFragmentPresenter( | |||
| 164 | } | 190 | } |
| 165 | } | 191 | } |
| 166 | 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 | |||
| 167 | private fun addThemeSettings(sl: ArrayList<SettingsItem>) { | 858 | private fun addThemeSettings(sl: ArrayList<SettingsItem>) { |
| 168 | sl.apply { | 859 | sl.apply { |
| 169 | val theme: AbstractIntSetting = object : AbstractIntSetting { | 860 | val theme: AbstractIntSetting = object : AbstractIntSetting { |
| @@ -186,20 +877,18 @@ class SettingsFragmentPresenter( | |||
| 186 | add( | 877 | add( |
| 187 | SingleChoiceSetting( | 878 | SingleChoiceSetting( |
| 188 | theme, | 879 | theme, |
| 189 | R.string.change_app_theme, | 880 | titleId = R.string.change_app_theme, |
| 190 | 0, | 881 | choicesId = R.array.themeEntriesA12, |
| 191 | R.array.themeEntriesA12, | 882 | valuesId = R.array.themeValuesA12 |
| 192 | R.array.themeValuesA12 | ||
| 193 | ) | 883 | ) |
| 194 | ) | 884 | ) |
| 195 | } else { | 885 | } else { |
| 196 | add( | 886 | add( |
| 197 | SingleChoiceSetting( | 887 | SingleChoiceSetting( |
| 198 | theme, | 888 | theme, |
| 199 | R.string.change_app_theme, | 889 | titleId = R.string.change_app_theme, |
| 200 | 0, | 890 | choicesId = R.array.themeEntries, |
| 201 | R.array.themeEntries, | 891 | valuesId = R.array.themeValues |
| 202 | R.array.themeValues | ||
| 203 | ) | 892 | ) |
| 204 | ) | 893 | ) |
| 205 | } | 894 | } |
| @@ -228,10 +917,9 @@ class SettingsFragmentPresenter( | |||
| 228 | add( | 917 | add( |
| 229 | SingleChoiceSetting( | 918 | SingleChoiceSetting( |
| 230 | themeMode, | 919 | themeMode, |
| 231 | R.string.change_theme_mode, | 920 | titleId = R.string.change_theme_mode, |
| 232 | 0, | 921 | choicesId = R.array.themeModeEntries, |
| 233 | R.array.themeModeEntries, | 922 | valuesId = R.array.themeModeValues |
| 234 | R.array.themeModeValues | ||
| 235 | ) | 923 | ) |
| 236 | ) | 924 | ) |
| 237 | 925 | ||
| @@ -262,8 +950,8 @@ class SettingsFragmentPresenter( | |||
| 262 | add( | 950 | add( |
| 263 | SwitchSetting( | 951 | SwitchSetting( |
| 264 | blackBackgrounds, | 952 | blackBackgrounds, |
| 265 | R.string.use_black_backgrounds, | 953 | titleId = R.string.use_black_backgrounds, |
| 266 | R.string.use_black_backgrounds_description | 954 | descriptionId = R.string.use_black_backgrounds_description |
| 267 | ) | 955 | ) |
| 268 | ) | 956 | ) |
| 269 | } | 957 | } |
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..ed60cf34f 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 |
| @@ -15,21 +15,17 @@ import androidx.core.view.updatePadding | |||
| 15 | import androidx.core.widget.doOnTextChanged | 15 | import androidx.core.widget.doOnTextChanged |
| 16 | import androidx.fragment.app.Fragment | 16 | import androidx.fragment.app.Fragment |
| 17 | import androidx.fragment.app.activityViewModels | 17 | import androidx.fragment.app.activityViewModels |
| 18 | import androidx.lifecycle.Lifecycle | ||
| 19 | import androidx.lifecycle.lifecycleScope | ||
| 20 | import androidx.lifecycle.repeatOnLifecycle | ||
| 21 | import androidx.recyclerview.widget.LinearLayoutManager | 18 | import androidx.recyclerview.widget.LinearLayoutManager |
| 22 | import com.google.android.material.divider.MaterialDividerItemDecoration | 19 | import com.google.android.material.divider.MaterialDividerItemDecoration |
| 23 | import com.google.android.material.transition.MaterialSharedAxis | 20 | import com.google.android.material.transition.MaterialSharedAxis |
| 24 | import info.debatty.java.stringsimilarity.Cosine | 21 | import info.debatty.java.stringsimilarity.Cosine |
| 25 | import kotlinx.coroutines.launch | ||
| 26 | import org.yuzu.yuzu_emu.R | 22 | import org.yuzu.yuzu_emu.R |
| 27 | import org.yuzu.yuzu_emu.databinding.FragmentSettingsSearchBinding | 23 | import org.yuzu.yuzu_emu.databinding.FragmentSettingsSearchBinding |
| 28 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | 24 | 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 | 25 | import org.yuzu.yuzu_emu.utils.NativeConfig |
| 26 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 32 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins | 27 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins |
| 28 | import org.yuzu.yuzu_emu.utils.collect | ||
| 33 | 29 | ||
| 34 | class SettingsSearchFragment : Fragment() { | 30 | class SettingsSearchFragment : Fragment() { |
| 35 | private var _binding: FragmentSettingsSearchBinding? = null | 31 | private var _binding: FragmentSettingsSearchBinding? = null |
| @@ -85,14 +81,10 @@ class SettingsSearchFragment : Fragment() { | |||
| 85 | search() | 81 | search() |
| 86 | binding.settingsList.smoothScrollToPosition(0) | 82 | binding.settingsList.smoothScrollToPosition(0) |
| 87 | } | 83 | } |
| 88 | viewLifecycleOwner.lifecycleScope.launch { | 84 | settingsViewModel.shouldReloadSettingsList.collect(viewLifecycleOwner) { |
| 89 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 85 | if (it) { |
| 90 | settingsViewModel.shouldReloadSettingsList.collect { | 86 | settingsViewModel.setShouldReloadSettingsList(false) |
| 91 | if (it) { | 87 | search() |
| 92 | settingsViewModel.setShouldReloadSettingsList(false) | ||
| 93 | search() | ||
| 94 | } | ||
| 95 | } | ||
| 96 | } | 88 | } |
| 97 | } | 89 | } |
| 98 | 90 | ||
| @@ -108,10 +100,9 @@ class SettingsSearchFragment : Fragment() { | |||
| 108 | 100 | ||
| 109 | private fun search() { | 101 | private fun search() { |
| 110 | val searchTerm = binding.searchText.text.toString().lowercase() | 102 | val searchTerm = binding.searchText.text.toString().lowercase() |
| 111 | binding.clearButton.visibility = | 103 | binding.clearButton.setVisible(visible = searchTerm.isNotEmpty(), gone = false) |
| 112 | if (searchTerm.isEmpty()) View.INVISIBLE else View.VISIBLE | ||
| 113 | if (searchTerm.isEmpty()) { | 104 | if (searchTerm.isEmpty()) { |
| 114 | binding.noResultsView.visibility = View.VISIBLE | 105 | binding.noResultsView.setVisible(visible = false, gone = false) |
| 115 | settingsAdapter?.submitList(emptyList()) | 106 | settingsAdapter?.submitList(emptyList()) |
| 116 | return | 107 | return |
| 117 | } | 108 | } |
| @@ -119,7 +110,7 @@ class SettingsSearchFragment : Fragment() { | |||
| 119 | val baseList = SettingsItem.settingsItems | 110 | val baseList = SettingsItem.settingsItems |
| 120 | val similarityAlgorithm = if (searchTerm.length > 2) Cosine() else Cosine(1) | 111 | val similarityAlgorithm = if (searchTerm.length > 2) Cosine() else Cosine(1) |
| 121 | val sortedList: List<SettingsItem> = baseList.mapNotNull { item -> | 112 | val sortedList: List<SettingsItem> = baseList.mapNotNull { item -> |
| 122 | val title = getString(item.value.nameId).lowercase() | 113 | val title = item.value.title.lowercase() |
| 123 | val similarity = similarityAlgorithm.similarity(searchTerm, title) | 114 | val similarity = similarityAlgorithm.similarity(searchTerm, title) |
| 124 | if (similarity > 0.08) { | 115 | if (similarity > 0.08) { |
| 125 | Pair(similarity, item) | 116 | Pair(similarity, item) |
| @@ -138,8 +129,7 @@ class SettingsSearchFragment : Fragment() { | |||
| 138 | optionalSetting | 129 | optionalSetting |
| 139 | } | 130 | } |
| 140 | settingsAdapter?.submitList(sortedList) | 131 | settingsAdapter?.submitList(sortedList) |
| 141 | binding.noResultsView.visibility = | 132 | binding.noResultsView.setVisible(visible = sortedList.isEmpty(), gone = false) |
| 142 | if (sortedList.isEmpty()) View.VISIBLE else View.INVISIBLE | ||
| 143 | } | 133 | } |
| 144 | 134 | ||
| 145 | private fun focusSearch() { | 135 | private fun focusSearch() { |
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/DateTimeViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt index 5ad0899dd..367db7fd2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt | |||
| @@ -14,6 +14,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting | |||
| 14 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | 14 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem |
| 15 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | 15 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter |
| 16 | import org.yuzu.yuzu_emu.utils.NativeConfig | 16 | import org.yuzu.yuzu_emu.utils.NativeConfig |
| 17 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 17 | 18 | ||
| 18 | class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : | 19 | class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : |
| 19 | SettingViewHolder(binding.root, adapter) { | 20 | SettingViewHolder(binding.root, adapter) { |
| @@ -21,28 +22,19 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA | |||
| 21 | 22 | ||
| 22 | override fun bind(item: SettingsItem) { | 23 | override fun bind(item: SettingsItem) { |
| 23 | setting = item as DateTimeSetting | 24 | setting = item as DateTimeSetting |
| 24 | binding.textSettingName.setText(item.nameId) | 25 | binding.textSettingName.text = item.title |
| 25 | if (item.descriptionId != 0) { | 26 | binding.textSettingDescription.setVisible(item.description.isNotEmpty()) |
| 26 | binding.textSettingDescription.setText(item.descriptionId) | 27 | binding.textSettingDescription.text = item.description |
| 27 | binding.textSettingDescription.visibility = View.VISIBLE | 28 | binding.textSettingValue.setVisible(true) |
| 28 | } else { | ||
| 29 | binding.textSettingDescription.visibility = View.GONE | ||
| 30 | } | ||
| 31 | |||
| 32 | binding.textSettingValue.visibility = View.VISIBLE | ||
| 33 | val epochTime = setting.getValue() | 29 | val epochTime = setting.getValue() |
| 34 | val instant = Instant.ofEpochMilli(epochTime * 1000) | 30 | val instant = Instant.ofEpochMilli(epochTime * 1000) |
| 35 | val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")) | 31 | val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")) |
| 36 | val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) | 32 | val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) |
| 37 | binding.textSettingValue.text = dateFormatter.format(zonedTime) | 33 | binding.textSettingValue.text = dateFormatter.format(zonedTime) |
| 38 | 34 | ||
| 39 | binding.buttonClear.visibility = if (setting.setting.global || | 35 | binding.buttonClear.setVisible( |
| 40 | !NativeConfig.isPerGameConfigLoaded() | 36 | !setting.setting.global || NativeConfig.isPerGameConfigLoaded() |
| 41 | ) { | 37 | ) |
| 42 | View.GONE | ||
| 43 | } else { | ||
| 44 | View.VISIBLE | ||
| 45 | } | ||
| 46 | binding.buttonClear.setOnClickListener { | 38 | binding.buttonClear.setOnClickListener { |
| 47 | adapter.onClearClick(setting, bindingAdapterPosition) | 39 | adapter.onClearClick(setting, bindingAdapterPosition) |
| 48 | } | 40 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt index f5bcf705c..0815c36e2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt | |||
| @@ -16,7 +16,7 @@ class HeaderViewHolder(val binding: ListItemSettingsHeaderBinding, adapter: Sett | |||
| 16 | } | 16 | } |
| 17 | 17 | ||
| 18 | override fun bind(item: SettingsItem) { | 18 | override fun bind(item: SettingsItem) { |
| 19 | binding.textHeaderName.setText(item.nameId) | 19 | binding.textHeaderName.text = item.title |
| 20 | } | 20 | } |
| 21 | 21 | ||
| 22 | override fun onClick(clicked: View) { | 22 | override fun onClick(clicked: View) { |
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..86af3a226 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputProfileViewHolder.kt | |||
| @@ -0,0 +1,34 @@ | |||
| 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 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 13 | |||
| 14 | class InputProfileViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : | ||
| 15 | SettingViewHolder(binding.root, adapter) { | ||
| 16 | private lateinit var setting: InputProfileSetting | ||
| 17 | |||
| 18 | override fun bind(item: SettingsItem) { | ||
| 19 | setting = item as InputProfileSetting | ||
| 20 | binding.textSettingName.text = setting.title | ||
| 21 | binding.textSettingValue.text = | ||
| 22 | setting.getCurrentProfile().ifEmpty { binding.root.context.getString(R.string.not_set) } | ||
| 23 | |||
| 24 | binding.textSettingDescription.setVisible(false) | ||
| 25 | binding.buttonClear.setVisible(false) | ||
| 26 | binding.icon.setVisible(false) | ||
| 27 | binding.buttonClear.setVisible(false) | ||
| 28 | } | ||
| 29 | |||
| 30 | override fun onClick(clicked: View) = | ||
| 31 | adapter.onInputProfileClick(setting, bindingAdapterPosition) | ||
| 32 | |||
| 33 | override fun onLongClick(clicked: View): Boolean = false | ||
| 34 | } | ||
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..9d9047804 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt | |||
| @@ -0,0 +1,60 @@ | |||
| 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 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 16 | |||
| 17 | class InputViewHolder(val binding: ListItemSettingInputBinding, adapter: SettingsAdapter) : | ||
| 18 | SettingViewHolder(binding.root, adapter) { | ||
| 19 | private lateinit var setting: InputSetting | ||
| 20 | |||
| 21 | override fun bind(item: SettingsItem) { | ||
| 22 | setting = item as InputSetting | ||
| 23 | binding.textSettingName.text = setting.title | ||
| 24 | binding.textSettingValue.text = setting.getSelectedValue() | ||
| 25 | |||
| 26 | when (item) { | ||
| 27 | is AnalogInputSetting -> { | ||
| 28 | val param = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog) | ||
| 29 | binding.buttonOptions.setVisible( | ||
| 30 | param.get("engine", "") == "analog_from_button" || | ||
| 31 | param.has("axis_x") || param.has("axis_y") | ||
| 32 | ) | ||
| 33 | } | ||
| 34 | |||
| 35 | is ButtonInputSetting -> { | ||
| 36 | val param = NativeInput.getButtonParam(item.playerIndex, item.nativeButton) | ||
| 37 | binding.buttonOptions.setVisible( | ||
| 38 | param.has("code") || param.has("button") || param.has("hat") || | ||
| 39 | param.has("axis") | ||
| 40 | ) | ||
| 41 | } | ||
| 42 | |||
| 43 | is ModifierInputSetting -> { | ||
| 44 | val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog) | ||
| 45 | binding.buttonOptions.setVisible(params.has("modifier")) | ||
| 46 | } | ||
| 47 | } | ||
| 48 | |||
| 49 | binding.buttonOptions.setOnClickListener(null) | ||
| 50 | binding.buttonOptions.setOnClickListener { | ||
| 51 | adapter.onInputOptionsClick(binding.buttonOptions, setting, bindingAdapterPosition) | ||
| 52 | } | ||
| 53 | } | ||
| 54 | |||
| 55 | override fun onClick(clicked: View) = | ||
| 56 | adapter.onInputClick(setting, bindingAdapterPosition) | ||
| 57 | |||
| 58 | override fun onLongClick(clicked: View): Boolean = | ||
| 59 | adapter.onLongClick(setting, bindingAdapterPosition) | ||
| 60 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt index 507184238..fc2ffb515 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt | |||
| @@ -5,11 +5,11 @@ package org.yuzu.yuzu_emu.features.settings.ui.viewholder | |||
| 5 | 5 | ||
| 6 | import android.view.View | 6 | import android.view.View |
| 7 | import androidx.core.content.res.ResourcesCompat | 7 | import androidx.core.content.res.ResourcesCompat |
| 8 | import org.yuzu.yuzu_emu.NativeLibrary | ||
| 9 | import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding | 8 | import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding |
| 10 | import org.yuzu.yuzu_emu.features.settings.model.view.RunnableSetting | 9 | import org.yuzu.yuzu_emu.features.settings.model.view.RunnableSetting |
| 11 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | 10 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem |
| 12 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | 11 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter |
| 12 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 13 | 13 | ||
| 14 | class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : | 14 | class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : |
| 15 | SettingViewHolder(binding.root, adapter) { | 15 | SettingViewHolder(binding.root, adapter) { |
| @@ -17,34 +17,28 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA | |||
| 17 | 17 | ||
| 18 | override fun bind(item: SettingsItem) { | 18 | override fun bind(item: SettingsItem) { |
| 19 | setting = item as RunnableSetting | 19 | setting = item as RunnableSetting |
| 20 | if (item.iconId != 0) { | 20 | binding.icon.setVisible(setting.iconId != 0) |
| 21 | binding.icon.visibility = View.VISIBLE | 21 | if (setting.iconId != 0) { |
| 22 | binding.icon.setImageDrawable( | 22 | binding.icon.setImageDrawable( |
| 23 | ResourcesCompat.getDrawable( | 23 | ResourcesCompat.getDrawable( |
| 24 | binding.icon.resources, | 24 | binding.icon.resources, |
| 25 | item.iconId, | 25 | setting.iconId, |
| 26 | binding.icon.context.theme | 26 | binding.icon.context.theme |
| 27 | ) | 27 | ) |
| 28 | ) | 28 | ) |
| 29 | } else { | ||
| 30 | binding.icon.visibility = View.GONE | ||
| 31 | } | 29 | } |
| 32 | 30 | ||
| 33 | binding.textSettingName.setText(item.nameId) | 31 | binding.textSettingName.text = setting.title |
| 34 | if (item.descriptionId != 0) { | 32 | binding.textSettingDescription.setVisible(setting.description.isNotEmpty()) |
| 35 | binding.textSettingDescription.setText(item.descriptionId) | 33 | binding.textSettingDescription.text = item.description |
| 36 | binding.textSettingDescription.visibility = View.VISIBLE | 34 | binding.textSettingValue.setVisible(false) |
| 37 | } else { | 35 | binding.buttonClear.setVisible(false) |
| 38 | binding.textSettingDescription.visibility = View.GONE | ||
| 39 | } | ||
| 40 | binding.textSettingValue.visibility = View.GONE | ||
| 41 | binding.buttonClear.visibility = View.GONE | ||
| 42 | 36 | ||
| 43 | setStyle(setting.isEditable, binding) | 37 | setStyle(setting.isEditable, binding) |
| 44 | } | 38 | } |
| 45 | 39 | ||
| 46 | override fun onClick(clicked: View) { | 40 | override fun onClick(clicked: View) { |
| 47 | if (!setting.isRuntimeRunnable && !NativeLibrary.isRunning()) { | 41 | if (setting.isRunnable) { |
| 48 | setting.runnable.invoke() | 42 | setting.runnable.invoke() |
| 49 | } | 43 | } |
| 50 | } | 44 | } |
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 02dab3785..e2fe0b072 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 | |||
| @@ -5,11 +5,13 @@ package org.yuzu.yuzu_emu.features.settings.ui.viewholder | |||
| 5 | 5 | ||
| 6 | import android.view.View | 6 | import android.view.View |
| 7 | import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding | 7 | import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding |
| 8 | import org.yuzu.yuzu_emu.features.settings.model.view.IntSingleChoiceSetting | ||
| 8 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | 9 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem |
| 9 | import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting | 10 | import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting |
| 10 | import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting | 11 | import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting |
| 11 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | 12 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter |
| 12 | import org.yuzu.yuzu_emu.utils.NativeConfig | 13 | import org.yuzu.yuzu_emu.utils.NativeConfig |
| 14 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 13 | 15 | ||
| 14 | class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : | 16 | class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : |
| 15 | SettingViewHolder(binding.root, adapter) { | 17 | SettingViewHolder(binding.root, adapter) { |
| @@ -17,40 +19,38 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti | |||
| 17 | 19 | ||
| 18 | override fun bind(item: SettingsItem) { | 20 | override fun bind(item: SettingsItem) { |
| 19 | setting = item | 21 | setting = item |
| 20 | binding.textSettingName.setText(item.nameId) | 22 | binding.textSettingName.text = setting.title |
| 21 | if (item.descriptionId != 0) { | 23 | binding.textSettingDescription.setVisible(item.description.isNotEmpty()) |
| 22 | binding.textSettingDescription.setText(item.descriptionId) | 24 | binding.textSettingDescription.text = item.description |
| 23 | binding.textSettingDescription.visibility = View.VISIBLE | ||
| 24 | } else { | ||
| 25 | binding.textSettingDescription.visibility = View.GONE | ||
| 26 | } | ||
| 27 | 25 | ||
| 28 | binding.textSettingValue.visibility = View.VISIBLE | 26 | binding.textSettingValue.setVisible(true) |
| 29 | if (item is SingleChoiceSetting) { | 27 | when (item) { |
| 30 | val resMgr = binding.textSettingValue.context.resources | 28 | is SingleChoiceSetting -> { |
| 31 | val values = resMgr.getIntArray(item.valuesId) | 29 | val resMgr = binding.textSettingValue.context.resources |
| 32 | for (i in values.indices) { | 30 | val values = resMgr.getIntArray(item.valuesId) |
| 33 | if (values[i] == item.getSelectedValue()) { | 31 | for (i in values.indices) { |
| 34 | binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i] | 32 | if (values[i] == item.getSelectedValue()) { |
| 35 | break | 33 | binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i] |
| 34 | break | ||
| 35 | } | ||
| 36 | } | 36 | } |
| 37 | } | 37 | } |
| 38 | } else if (item is StringSingleChoiceSetting) { | 38 | |
| 39 | for (i in item.values.indices) { | 39 | is StringSingleChoiceSetting -> { |
| 40 | if (item.values[i] == item.getSelectedValue()) { | 40 | binding.textSettingValue.text = item.getSelectedValue() |
| 41 | binding.textSettingValue.text = item.choices[i] | ||
| 42 | break | ||
| 43 | } | ||
| 44 | } | 41 | } |
| 45 | } | ||
| 46 | 42 | ||
| 47 | binding.buttonClear.visibility = if (setting.setting.global || | 43 | is IntSingleChoiceSetting -> { |
| 48 | !NativeConfig.isPerGameConfigLoaded() | 44 | binding.textSettingValue.text = item.getChoiceAt(item.getSelectedValue()) |
| 49 | ) { | 45 | } |
| 50 | View.GONE | 46 | } |
| 51 | } else { | 47 | if (binding.textSettingValue.text.isEmpty()) { |
| 52 | View.VISIBLE | 48 | binding.textSettingValue.setVisible(false) |
| 53 | } | 49 | } |
| 50 | |||
| 51 | binding.buttonClear.setVisible( | ||
| 52 | !setting.setting.global || NativeConfig.isPerGameConfigLoaded() | ||
| 53 | ) | ||
| 54 | binding.buttonClear.setOnClickListener { | 54 | binding.buttonClear.setOnClickListener { |
| 55 | adapter.onClearClick(setting, bindingAdapterPosition) | 55 | adapter.onClearClick(setting, bindingAdapterPosition) |
| 56 | } | 56 | } |
| @@ -63,16 +63,25 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti | |||
| 63 | return | 63 | return |
| 64 | } | 64 | } |
| 65 | 65 | ||
| 66 | if (setting is SingleChoiceSetting) { | 66 | when (setting) { |
| 67 | adapter.onSingleChoiceClick( | 67 | is SingleChoiceSetting -> adapter.onSingleChoiceClick( |
| 68 | (setting as SingleChoiceSetting), | 68 | setting as SingleChoiceSetting, |
| 69 | bindingAdapterPosition | ||
| 70 | ) | ||
| 71 | } else if (setting is StringSingleChoiceSetting) { | ||
| 72 | adapter.onStringSingleChoiceClick( | ||
| 73 | (setting as StringSingleChoiceSetting), | ||
| 74 | bindingAdapterPosition | 69 | bindingAdapterPosition |
| 75 | ) | 70 | ) |
| 71 | |||
| 72 | is StringSingleChoiceSetting -> { | ||
| 73 | adapter.onStringSingleChoiceClick( | ||
| 74 | setting as StringSingleChoiceSetting, | ||
| 75 | bindingAdapterPosition | ||
| 76 | ) | ||
| 77 | } | ||
| 78 | |||
| 79 | is IntSingleChoiceSetting -> { | ||
| 80 | adapter.onIntSingleChoiceClick( | ||
| 81 | setting as IntSingleChoiceSetting, | ||
| 82 | bindingAdapterPosition | ||
| 83 | ) | ||
| 84 | } | ||
| 76 | } | 85 | } |
| 77 | } | 86 | } |
| 78 | 87 | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt index 596c18012..a37b59b44 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt | |||
| @@ -10,6 +10,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | |||
| 10 | import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting | 10 | import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting |
| 11 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | 11 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter |
| 12 | import org.yuzu.yuzu_emu.utils.NativeConfig | 12 | import org.yuzu.yuzu_emu.utils.NativeConfig |
| 13 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 13 | 14 | ||
| 14 | class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : | 15 | class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : |
| 15 | SettingViewHolder(binding.root, adapter) { | 16 | SettingViewHolder(binding.root, adapter) { |
| @@ -17,27 +18,19 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda | |||
| 17 | 18 | ||
| 18 | override fun bind(item: SettingsItem) { | 19 | override fun bind(item: SettingsItem) { |
| 19 | setting = item as SliderSetting | 20 | setting = item as SliderSetting |
| 20 | binding.textSettingName.setText(item.nameId) | 21 | binding.textSettingName.text = setting.title |
| 21 | if (item.descriptionId != 0) { | 22 | binding.textSettingDescription.setVisible(item.description.isNotEmpty()) |
| 22 | binding.textSettingDescription.setText(item.descriptionId) | 23 | binding.textSettingDescription.text = setting.description |
| 23 | binding.textSettingDescription.visibility = View.VISIBLE | 24 | binding.textSettingValue.setVisible(true) |
| 24 | } else { | ||
| 25 | binding.textSettingDescription.visibility = View.GONE | ||
| 26 | } | ||
| 27 | binding.textSettingValue.visibility = View.VISIBLE | ||
| 28 | binding.textSettingValue.text = String.format( | 25 | binding.textSettingValue.text = String.format( |
| 29 | binding.textSettingValue.context.getString(R.string.value_with_units), | 26 | binding.textSettingValue.context.getString(R.string.value_with_units), |
| 30 | setting.getSelectedValue(), | 27 | setting.getSelectedValue(), |
| 31 | setting.units | 28 | setting.units |
| 32 | ) | 29 | ) |
| 33 | 30 | ||
| 34 | binding.buttonClear.visibility = if (setting.setting.global || | 31 | binding.buttonClear.setVisible( |
| 35 | !NativeConfig.isPerGameConfigLoaded() | 32 | !setting.setting.global || NativeConfig.isPerGameConfigLoaded() |
| 36 | ) { | 33 | ) |
| 37 | View.GONE | ||
| 38 | } else { | ||
| 39 | View.VISIBLE | ||
| 40 | } | ||
| 41 | binding.buttonClear.setOnClickListener { | 34 | binding.buttonClear.setOnClickListener { |
| 42 | adapter.onClearClick(setting, bindingAdapterPosition) | 35 | adapter.onClearClick(setting, bindingAdapterPosition) |
| 43 | } | 36 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt index 20d35a17d..f7a9c08c3 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt | |||
| @@ -9,39 +9,34 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding | |||
| 9 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | 9 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem |
| 10 | import org.yuzu.yuzu_emu.features.settings.model.view.SubmenuSetting | 10 | import org.yuzu.yuzu_emu.features.settings.model.view.SubmenuSetting |
| 11 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | 11 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter |
| 12 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 12 | 13 | ||
| 13 | class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : | 14 | class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : |
| 14 | SettingViewHolder(binding.root, adapter) { | 15 | SettingViewHolder(binding.root, adapter) { |
| 15 | private lateinit var item: SubmenuSetting | 16 | private lateinit var setting: SubmenuSetting |
| 16 | 17 | ||
| 17 | override fun bind(item: SettingsItem) { | 18 | override fun bind(item: SettingsItem) { |
| 18 | this.item = item as SubmenuSetting | 19 | setting = item as SubmenuSetting |
| 19 | if (item.iconId != 0) { | 20 | binding.icon.setVisible(setting.iconId != 0) |
| 20 | binding.icon.visibility = View.VISIBLE | 21 | if (setting.iconId != 0) { |
| 21 | binding.icon.setImageDrawable( | 22 | binding.icon.setImageDrawable( |
| 22 | ResourcesCompat.getDrawable( | 23 | ResourcesCompat.getDrawable( |
| 23 | binding.icon.resources, | 24 | binding.icon.resources, |
| 24 | item.iconId, | 25 | setting.iconId, |
| 25 | binding.icon.context.theme | 26 | binding.icon.context.theme |
| 26 | ) | 27 | ) |
| 27 | ) | 28 | ) |
| 28 | } else { | ||
| 29 | binding.icon.visibility = View.GONE | ||
| 30 | } | 29 | } |
| 31 | 30 | ||
| 32 | binding.textSettingName.setText(item.nameId) | 31 | binding.textSettingName.text = setting.title |
| 33 | if (item.descriptionId != 0) { | 32 | binding.textSettingDescription.setVisible(setting.description.isNotEmpty()) |
| 34 | binding.textSettingDescription.setText(item.descriptionId) | 33 | binding.textSettingDescription.text = setting.description |
| 35 | binding.textSettingDescription.visibility = View.VISIBLE | 34 | binding.textSettingValue.setVisible(false) |
| 36 | } else { | 35 | binding.buttonClear.setVisible(false) |
| 37 | binding.textSettingDescription.visibility = View.GONE | ||
| 38 | } | ||
| 39 | binding.textSettingValue.visibility = View.GONE | ||
| 40 | binding.buttonClear.visibility = View.GONE | ||
| 41 | } | 36 | } |
| 42 | 37 | ||
| 43 | override fun onClick(clicked: View) { | 38 | override fun onClick(clicked: View) { |
| 44 | adapter.onSubmenuClick(item) | 39 | adapter.onSubmenuClick(setting) |
| 45 | } | 40 | } |
| 46 | 41 | ||
| 47 | override fun onLongClick(clicked: View): Boolean { | 42 | override fun onLongClick(clicked: View): Boolean { |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt index d26bf9374..53f7b301f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt | |||
| @@ -10,6 +10,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | |||
| 10 | import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting | 10 | import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting |
| 11 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | 11 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter |
| 12 | import org.yuzu.yuzu_emu.utils.NativeConfig | 12 | import org.yuzu.yuzu_emu.utils.NativeConfig |
| 13 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 13 | 14 | ||
| 14 | class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) : | 15 | class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) : |
| 15 | SettingViewHolder(binding.root, adapter) { | 16 | SettingViewHolder(binding.root, adapter) { |
| @@ -18,28 +19,19 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter | |||
| 18 | 19 | ||
| 19 | override fun bind(item: SettingsItem) { | 20 | override fun bind(item: SettingsItem) { |
| 20 | setting = item as SwitchSetting | 21 | setting = item as SwitchSetting |
| 21 | binding.textSettingName.setText(item.nameId) | 22 | binding.textSettingName.text = setting.title |
| 22 | if (item.descriptionId != 0) { | 23 | binding.textSettingDescription.setVisible(setting.description.isNotEmpty()) |
| 23 | binding.textSettingDescription.setText(item.descriptionId) | 24 | binding.textSettingDescription.text = setting.description |
| 24 | binding.textSettingDescription.visibility = View.VISIBLE | ||
| 25 | } else { | ||
| 26 | binding.textSettingDescription.text = "" | ||
| 27 | binding.textSettingDescription.visibility = View.GONE | ||
| 28 | } | ||
| 29 | 25 | ||
| 30 | binding.switchWidget.setOnCheckedChangeListener(null) | 26 | binding.switchWidget.setOnCheckedChangeListener(null) |
| 31 | binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal) | 27 | binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal) |
| 32 | binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> | 28 | binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> |
| 33 | adapter.onBooleanClick(item, binding.switchWidget.isChecked, bindingAdapterPosition) | 29 | adapter.onBooleanClick(setting, binding.switchWidget.isChecked, bindingAdapterPosition) |
| 34 | } | 30 | } |
| 35 | 31 | ||
| 36 | binding.buttonClear.visibility = if (setting.setting.global || | 32 | binding.buttonClear.setVisible( |
| 37 | !NativeConfig.isPerGameConfigLoaded() | 33 | !setting.setting.global || NativeConfig.isPerGameConfigLoaded() |
| 38 | ) { | 34 | ) |
| 39 | View.GONE | ||
| 40 | } else { | ||
| 41 | View.VISIBLE | ||
| 42 | } | ||
| 43 | binding.buttonClear.setOnClickListener { | 35 | binding.buttonClear.setOnClickListener { |
| 44 | adapter.onClearClick(setting, bindingAdapterPosition) | 36 | adapter.onClearClick(setting, bindingAdapterPosition) |
| 45 | } | 37 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt index 872553ac4..110aa2960 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt | |||
| @@ -3,7 +3,6 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.fragments | 4 | package org.yuzu.yuzu_emu.fragments |
| 5 | 5 | ||
| 6 | import android.annotation.SuppressLint | ||
| 7 | import android.content.Intent | 6 | import android.content.Intent |
| 8 | import android.os.Bundle | 7 | import android.os.Bundle |
| 9 | import android.view.LayoutInflater | 8 | import android.view.LayoutInflater |
| @@ -16,9 +15,6 @@ import androidx.core.view.updatePadding | |||
| 16 | import androidx.documentfile.provider.DocumentFile | 15 | import androidx.documentfile.provider.DocumentFile |
| 17 | import androidx.fragment.app.Fragment | 16 | import androidx.fragment.app.Fragment |
| 18 | import androidx.fragment.app.activityViewModels | 17 | import androidx.fragment.app.activityViewModels |
| 19 | import androidx.lifecycle.Lifecycle | ||
| 20 | import androidx.lifecycle.lifecycleScope | ||
| 21 | import androidx.lifecycle.repeatOnLifecycle | ||
| 22 | import androidx.navigation.findNavController | 18 | import androidx.navigation.findNavController |
| 23 | import androidx.navigation.fragment.navArgs | 19 | import androidx.navigation.fragment.navArgs |
| 24 | import androidx.recyclerview.widget.LinearLayoutManager | 20 | import androidx.recyclerview.widget.LinearLayoutManager |
| @@ -32,6 +28,7 @@ import org.yuzu.yuzu_emu.model.HomeViewModel | |||
| 32 | import org.yuzu.yuzu_emu.utils.AddonUtil | 28 | import org.yuzu.yuzu_emu.utils.AddonUtil |
| 33 | import org.yuzu.yuzu_emu.utils.FileUtil.copyFilesTo | 29 | import org.yuzu.yuzu_emu.utils.FileUtil.copyFilesTo |
| 34 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins | 30 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins |
| 31 | import org.yuzu.yuzu_emu.utils.collect | ||
| 35 | import java.io.File | 32 | import java.io.File |
| 36 | 33 | ||
| 37 | class AddonsFragment : Fragment() { | 34 | class AddonsFragment : Fragment() { |
| @@ -60,8 +57,6 @@ class AddonsFragment : Fragment() { | |||
| 60 | return binding.root | 57 | return binding.root |
| 61 | } | 58 | } |
| 62 | 59 | ||
| 63 | // This is using the correct scope, lint is just acting up | ||
| 64 | @SuppressLint("UnsafeRepeatOnLifecycleDetector") | ||
| 65 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | 60 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| 66 | super.onViewCreated(view, savedInstanceState) | 61 | super.onViewCreated(view, savedInstanceState) |
| 67 | homeViewModel.setNavigationVisibility(visible = false, animated = false) | 62 | homeViewModel.setNavigationVisibility(visible = false, animated = false) |
| @@ -78,57 +73,41 @@ class AddonsFragment : Fragment() { | |||
| 78 | adapter = AddonAdapter(addonViewModel) | 73 | adapter = AddonAdapter(addonViewModel) |
| 79 | } | 74 | } |
| 80 | 75 | ||
| 81 | viewLifecycleOwner.lifecycleScope.apply { | 76 | addonViewModel.addonList.collect(viewLifecycleOwner) { |
| 82 | launch { | 77 | (binding.listAddons.adapter as AddonAdapter).submitList(it) |
| 83 | repeatOnLifecycle(Lifecycle.State.STARTED) { | 78 | } |
| 84 | addonViewModel.addonList.collect { | 79 | addonViewModel.showModInstallPicker.collect( |
| 85 | (binding.listAddons.adapter as AddonAdapter).submitList(it) | 80 | viewLifecycleOwner, |
| 86 | } | 81 | resetState = { addonViewModel.showModInstallPicker(false) } |
| 87 | } | 82 | ) { if (it) installAddon.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) } |
| 88 | } | 83 | addonViewModel.showModNoticeDialog.collect( |
| 89 | launch { | 84 | viewLifecycleOwner, |
| 90 | repeatOnLifecycle(Lifecycle.State.STARTED) { | 85 | resetState = { addonViewModel.showModNoticeDialog(false) } |
| 91 | addonViewModel.showModInstallPicker.collect { | 86 | ) { |
| 92 | if (it) { | 87 | if (it) { |
| 93 | installAddon.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) | 88 | MessageDialogFragment.newInstance( |
| 94 | addonViewModel.showModInstallPicker(false) | 89 | requireActivity(), |
| 95 | } | 90 | titleId = R.string.addon_notice, |
| 96 | } | 91 | descriptionId = R.string.addon_notice_description, |
| 97 | } | 92 | dismissible = false, |
| 98 | } | 93 | positiveAction = { addonViewModel.showModInstallPicker(true) }, |
| 99 | launch { | 94 | negativeAction = {}, |
| 100 | repeatOnLifecycle(Lifecycle.State.STARTED) { | 95 | negativeButtonTitleId = R.string.close |
| 101 | addonViewModel.showModNoticeDialog.collect { | 96 | ).show(parentFragmentManager, MessageDialogFragment.TAG) |
| 102 | if (it) { | ||
| 103 | MessageDialogFragment.newInstance( | ||
| 104 | requireActivity(), | ||
| 105 | titleId = R.string.addon_notice, | ||
| 106 | descriptionId = R.string.addon_notice_description, | ||
| 107 | dismissible = false, | ||
| 108 | positiveAction = { addonViewModel.showModInstallPicker(true) }, | ||
| 109 | negativeAction = {}, | ||
| 110 | negativeButtonTitleId = R.string.close | ||
| 111 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 112 | addonViewModel.showModNoticeDialog(false) | ||
| 113 | } | ||
| 114 | } | ||
| 115 | } | ||
| 116 | } | 97 | } |
| 117 | launch { | 98 | } |
| 118 | repeatOnLifecycle(Lifecycle.State.STARTED) { | 99 | addonViewModel.addonToDelete.collect( |
| 119 | addonViewModel.addonToDelete.collect { | 100 | viewLifecycleOwner, |
| 120 | if (it != null) { | 101 | resetState = { addonViewModel.setAddonToDelete(null) } |
| 121 | MessageDialogFragment.newInstance( | 102 | ) { |
| 122 | requireActivity(), | 103 | if (it != null) { |
| 123 | titleId = R.string.confirm_uninstall, | 104 | MessageDialogFragment.newInstance( |
| 124 | descriptionId = R.string.confirm_uninstall_description, | 105 | requireActivity(), |
| 125 | positiveAction = { addonViewModel.onDeleteAddon(it) }, | 106 | titleId = R.string.confirm_uninstall, |
| 126 | negativeAction = {} | 107 | descriptionId = R.string.confirm_uninstall_description, |
| 127 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | 108 | positiveAction = { addonViewModel.onDeleteAddon(it) }, |
| 128 | addonViewModel.setAddonToDelete(null) | 109 | negativeAction = {} |
| 129 | } | 110 | ).show(parentFragmentManager, MessageDialogFragment.TAG) |
| 130 | } | ||
| 131 | } | ||
| 132 | } | 111 | } |
| 133 | } | 112 | } |
| 134 | 113 | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CoreErrorDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CoreErrorDialogFragment.kt new file mode 100644 index 000000000..299f8da71 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CoreErrorDialogFragment.kt | |||
| @@ -0,0 +1,47 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.fragments | ||
| 5 | |||
| 6 | import android.app.Dialog | ||
| 7 | import android.content.DialogInterface | ||
| 8 | import android.os.Bundle | ||
| 9 | import androidx.fragment.app.DialogFragment | ||
| 10 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||
| 11 | import org.yuzu.yuzu_emu.NativeLibrary | ||
| 12 | import org.yuzu.yuzu_emu.R | ||
| 13 | |||
| 14 | class CoreErrorDialogFragment : DialogFragment() { | ||
| 15 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = | ||
| 16 | MaterialAlertDialogBuilder(requireActivity()) | ||
| 17 | .setTitle(requireArguments().getString(TITLE)) | ||
| 18 | .setMessage(requireArguments().getString(MESSAGE)) | ||
| 19 | .setPositiveButton(R.string.continue_button, null) | ||
| 20 | .setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int -> | ||
| 21 | NativeLibrary.coreErrorAlertResult = false | ||
| 22 | synchronized(NativeLibrary.coreErrorAlertLock) { | ||
| 23 | NativeLibrary.coreErrorAlertLock.notify() | ||
| 24 | } | ||
| 25 | } | ||
| 26 | .create() | ||
| 27 | |||
| 28 | override fun onDismiss(dialog: DialogInterface) { | ||
| 29 | super.onDismiss(dialog) | ||
| 30 | NativeLibrary.coreErrorAlertResult = true | ||
| 31 | synchronized(NativeLibrary.coreErrorAlertLock) { NativeLibrary.coreErrorAlertLock.notify() } | ||
| 32 | } | ||
| 33 | |||
| 34 | companion object { | ||
| 35 | const val TITLE = "Title" | ||
| 36 | const val MESSAGE = "Message" | ||
| 37 | |||
| 38 | fun newInstance(title: String, message: String): CoreErrorDialogFragment { | ||
| 39 | val frag = CoreErrorDialogFragment() | ||
| 40 | val args = Bundle() | ||
| 41 | args.putString(TITLE, title) | ||
| 42 | args.putString(MESSAGE, message) | ||
| 43 | frag.arguments = args | ||
| 44 | return frag | ||
| 45 | } | ||
| 46 | } | ||
| 47 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt index 41cff46c1..8b23a1021 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt | |||
| @@ -3,7 +3,6 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.fragments | 4 | package org.yuzu.yuzu_emu.fragments |
| 5 | 5 | ||
| 6 | import android.annotation.SuppressLint | ||
| 7 | import android.os.Bundle | 6 | import android.os.Bundle |
| 8 | import android.view.LayoutInflater | 7 | import android.view.LayoutInflater |
| 9 | import android.view.View | 8 | import android.view.View |
| @@ -14,9 +13,6 @@ import androidx.core.view.WindowInsetsCompat | |||
| 14 | import androidx.core.view.updatePadding | 13 | import androidx.core.view.updatePadding |
| 15 | import androidx.fragment.app.Fragment | 14 | import androidx.fragment.app.Fragment |
| 16 | import androidx.fragment.app.activityViewModels | 15 | import androidx.fragment.app.activityViewModels |
| 17 | import androidx.lifecycle.Lifecycle | ||
| 18 | import androidx.lifecycle.lifecycleScope | ||
| 19 | import androidx.lifecycle.repeatOnLifecycle | ||
| 20 | import androidx.navigation.findNavController | 16 | import androidx.navigation.findNavController |
| 21 | import androidx.navigation.fragment.navArgs | 17 | import androidx.navigation.fragment.navArgs |
| 22 | import androidx.recyclerview.widget.GridLayoutManager | 18 | import androidx.recyclerview.widget.GridLayoutManager |
| @@ -35,6 +31,7 @@ import org.yuzu.yuzu_emu.utils.FileUtil | |||
| 35 | import org.yuzu.yuzu_emu.utils.GpuDriverHelper | 31 | import org.yuzu.yuzu_emu.utils.GpuDriverHelper |
| 36 | import org.yuzu.yuzu_emu.utils.NativeConfig | 32 | import org.yuzu.yuzu_emu.utils.NativeConfig |
| 37 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins | 33 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins |
| 34 | import org.yuzu.yuzu_emu.utils.collect | ||
| 38 | import java.io.File | 35 | import java.io.File |
| 39 | import java.io.IOException | 36 | import java.io.IOException |
| 40 | 37 | ||
| @@ -63,8 +60,6 @@ class DriverManagerFragment : Fragment() { | |||
| 63 | return binding.root | 60 | return binding.root |
| 64 | } | 61 | } |
| 65 | 62 | ||
| 66 | // This is using the correct scope, lint is just acting up | ||
| 67 | @SuppressLint("UnsafeRepeatOnLifecycleDetector") | ||
| 68 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | 63 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| 69 | super.onViewCreated(view, savedInstanceState) | 64 | super.onViewCreated(view, savedInstanceState) |
| 70 | homeViewModel.setNavigationVisibility(visible = false, animated = true) | 65 | homeViewModel.setNavigationVisibility(visible = false, animated = true) |
| @@ -89,15 +84,8 @@ class DriverManagerFragment : Fragment() { | |||
| 89 | } | 84 | } |
| 90 | } | 85 | } |
| 91 | 86 | ||
| 92 | viewLifecycleOwner.lifecycleScope.apply { | 87 | driverViewModel.showClearButton.collect(viewLifecycleOwner) { |
| 93 | launch { | 88 | binding.toolbarDrivers.menu.findItem(R.id.menu_driver_use_global).isVisible = it |
| 94 | repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
| 95 | driverViewModel.showClearButton.collect { | ||
| 96 | binding.toolbarDrivers.menu | ||
| 97 | .findItem(R.id.menu_driver_use_global).isVisible = it | ||
| 98 | } | ||
| 99 | } | ||
| 100 | } | ||
| 101 | } | 89 | } |
| 102 | } | 90 | } |
| 103 | 91 | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt index 6a47b29f0..bad56e434 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt | |||
| @@ -10,14 +10,11 @@ import android.view.View | |||
| 10 | import android.view.ViewGroup | 10 | import android.view.ViewGroup |
| 11 | import androidx.fragment.app.DialogFragment | 11 | import androidx.fragment.app.DialogFragment |
| 12 | import androidx.fragment.app.activityViewModels | 12 | import androidx.fragment.app.activityViewModels |
| 13 | import androidx.lifecycle.Lifecycle | ||
| 14 | import androidx.lifecycle.lifecycleScope | ||
| 15 | import androidx.lifecycle.repeatOnLifecycle | ||
| 16 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | 13 | import com.google.android.material.dialog.MaterialAlertDialogBuilder |
| 17 | import kotlinx.coroutines.launch | ||
| 18 | import org.yuzu.yuzu_emu.R | 14 | import org.yuzu.yuzu_emu.R |
| 19 | import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding | 15 | import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding |
| 20 | import org.yuzu.yuzu_emu.model.DriverViewModel | 16 | import org.yuzu.yuzu_emu.model.DriverViewModel |
| 17 | import org.yuzu.yuzu_emu.utils.collect | ||
| 21 | 18 | ||
| 22 | class DriversLoadingDialogFragment : DialogFragment() { | 19 | class DriversLoadingDialogFragment : DialogFragment() { |
| 23 | private val driverViewModel: DriverViewModel by activityViewModels() | 20 | private val driverViewModel: DriverViewModel by activityViewModels() |
| @@ -44,13 +41,7 @@ class DriversLoadingDialogFragment : DialogFragment() { | |||
| 44 | 41 | ||
| 45 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | 42 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| 46 | super.onViewCreated(view, savedInstanceState) | 43 | super.onViewCreated(view, savedInstanceState) |
| 47 | viewLifecycleOwner.lifecycleScope.apply { | 44 | driverViewModel.isInteractionAllowed.collect(viewLifecycleOwner) { if (it) dismiss() } |
| 48 | launch { | ||
| 49 | repeatOnLifecycle(Lifecycle.State.RESUMED) { | ||
| 50 | driverViewModel.isInteractionAllowed.collect { if (it) dismiss() } | ||
| 51 | } | ||
| 52 | } | ||
| 53 | } | ||
| 54 | } | 45 | } |
| 55 | 46 | ||
| 56 | companion object { | 47 | companion object { |
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..c3b2b11f8 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 | |||
| @@ -32,9 +32,6 @@ import androidx.drawerlayout.widget.DrawerLayout | |||
| 32 | import androidx.drawerlayout.widget.DrawerLayout.DrawerListener | 32 | import androidx.drawerlayout.widget.DrawerLayout.DrawerListener |
| 33 | import androidx.fragment.app.Fragment | 33 | import androidx.fragment.app.Fragment |
| 34 | import androidx.fragment.app.activityViewModels | 34 | import androidx.fragment.app.activityViewModels |
| 35 | import androidx.lifecycle.Lifecycle | ||
| 36 | import androidx.lifecycle.lifecycleScope | ||
| 37 | import androidx.lifecycle.repeatOnLifecycle | ||
| 38 | import androidx.navigation.findNavController | 35 | import androidx.navigation.findNavController |
| 39 | import androidx.navigation.fragment.navArgs | 36 | import androidx.navigation.fragment.navArgs |
| 40 | import androidx.window.layout.FoldingFeature | 37 | import androidx.window.layout.FoldingFeature |
| @@ -42,9 +39,6 @@ import androidx.window.layout.WindowInfoTracker | |||
| 42 | import androidx.window.layout.WindowLayoutInfo | 39 | import androidx.window.layout.WindowLayoutInfo |
| 43 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | 40 | import com.google.android.material.dialog.MaterialAlertDialogBuilder |
| 44 | import com.google.android.material.slider.Slider | 41 | import com.google.android.material.slider.Slider |
| 45 | import kotlinx.coroutines.Dispatchers | ||
| 46 | import kotlinx.coroutines.flow.collectLatest | ||
| 47 | import kotlinx.coroutines.launch | ||
| 48 | import org.yuzu.yuzu_emu.HomeNavigationDirections | 42 | import org.yuzu.yuzu_emu.HomeNavigationDirections |
| 49 | import org.yuzu.yuzu_emu.NativeLibrary | 43 | import org.yuzu.yuzu_emu.NativeLibrary |
| 50 | import org.yuzu.yuzu_emu.R | 44 | import org.yuzu.yuzu_emu.R |
| @@ -63,6 +57,7 @@ import org.yuzu.yuzu_emu.model.EmulationViewModel | |||
| 63 | import org.yuzu.yuzu_emu.overlay.model.OverlayControl | 57 | import org.yuzu.yuzu_emu.overlay.model.OverlayControl |
| 64 | import org.yuzu.yuzu_emu.overlay.model.OverlayLayout | 58 | import org.yuzu.yuzu_emu.overlay.model.OverlayLayout |
| 65 | import org.yuzu.yuzu_emu.utils.* | 59 | import org.yuzu.yuzu_emu.utils.* |
| 60 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 66 | import java.lang.NullPointerException | 61 | import java.lang.NullPointerException |
| 67 | 62 | ||
| 68 | class EmulationFragment : Fragment(), SurfaceHolder.Callback { | 63 | class EmulationFragment : Fragment(), SurfaceHolder.Callback { |
| @@ -90,14 +85,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 90 | if (context is EmulationActivity) { | 85 | if (context is EmulationActivity) { |
| 91 | emulationActivity = context | 86 | emulationActivity = context |
| 92 | NativeLibrary.setEmulationActivity(context) | 87 | NativeLibrary.setEmulationActivity(context) |
| 93 | |||
| 94 | lifecycleScope.launch(Dispatchers.Main) { | ||
| 95 | lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
| 96 | WindowInfoTracker.getOrCreate(context) | ||
| 97 | .windowLayoutInfo(context) | ||
| 98 | .collect { updateFoldableLayout(context, it) } | ||
| 99 | } | ||
| 100 | } | ||
| 101 | } else { | 88 | } else { |
| 102 | throw IllegalStateException("EmulationFragment must have EmulationActivity parent") | 89 | throw IllegalStateException("EmulationFragment must have EmulationActivity parent") |
| 103 | } | 90 | } |
| @@ -168,8 +155,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 168 | return binding.root | 155 | return binding.root |
| 169 | } | 156 | } |
| 170 | 157 | ||
| 171 | // This is using the correct scope, lint is just acting up | ||
| 172 | @SuppressLint("UnsafeRepeatOnLifecycleDetector") | ||
| 173 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | 158 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| 174 | super.onViewCreated(view, savedInstanceState) | 159 | super.onViewCreated(view, savedInstanceState) |
| 175 | if (requireActivity().isFinishing) { | 160 | if (requireActivity().isFinishing) { |
| @@ -277,6 +262,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 277 | true | 262 | true |
| 278 | } | 263 | } |
| 279 | 264 | ||
| 265 | R.id.menu_controls -> { | ||
| 266 | val action = HomeNavigationDirections.actionGlobalSettingsActivity( | ||
| 267 | null, | ||
| 268 | Settings.MenuTag.SECTION_INPUT | ||
| 269 | ) | ||
| 270 | binding.root.findNavController().navigate(action) | ||
| 271 | true | ||
| 272 | } | ||
| 273 | |||
| 280 | R.id.menu_overlay_controls -> { | 274 | R.id.menu_overlay_controls -> { |
| 281 | showOverlayOptions() | 275 | showOverlayOptions() |
| 282 | true | 276 | true |
| @@ -341,129 +335,86 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 341 | binding.loadingTitle.isSelected = true | 335 | binding.loadingTitle.isSelected = true |
| 342 | binding.loadingText.isSelected = true | 336 | binding.loadingText.isSelected = true |
| 343 | 337 | ||
| 344 | viewLifecycleOwner.lifecycleScope.apply { | 338 | WindowInfoTracker.getOrCreate(requireContext()) |
| 345 | launch { | 339 | .windowLayoutInfo(requireActivity()).collect(viewLifecycleOwner) { |
| 346 | repeatOnLifecycle(Lifecycle.State.STARTED) { | 340 | updateFoldableLayout(requireActivity() as EmulationActivity, it) |
| 347 | WindowInfoTracker.getOrCreate(requireContext()) | ||
| 348 | .windowLayoutInfo(requireActivity()) | ||
| 349 | .collect { | ||
| 350 | updateFoldableLayout(requireActivity() as EmulationActivity, it) | ||
| 351 | } | ||
| 352 | } | ||
| 353 | } | 341 | } |
| 354 | launch { | 342 | emulationViewModel.shaderProgress.collect(viewLifecycleOwner) { |
| 355 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 343 | if (it > 0 && it != emulationViewModel.totalShaders.value) { |
| 356 | emulationViewModel.shaderProgress.collectLatest { | 344 | binding.loadingProgressIndicator.isIndeterminate = false |
| 357 | if (it > 0 && it != emulationViewModel.totalShaders.value) { | ||
| 358 | binding.loadingProgressIndicator.isIndeterminate = false | ||
| 359 | |||
| 360 | if (it < binding.loadingProgressIndicator.max) { | ||
| 361 | binding.loadingProgressIndicator.progress = it | ||
| 362 | } | ||
| 363 | } | ||
| 364 | 345 | ||
| 365 | if (it == emulationViewModel.totalShaders.value) { | 346 | if (it < binding.loadingProgressIndicator.max) { |
| 366 | binding.loadingText.setText(R.string.loading) | 347 | binding.loadingProgressIndicator.progress = it |
| 367 | binding.loadingProgressIndicator.isIndeterminate = true | ||
| 368 | } | ||
| 369 | } | ||
| 370 | } | 348 | } |
| 371 | } | 349 | } |
| 372 | launch { | 350 | |
| 373 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 351 | if (it == emulationViewModel.totalShaders.value) { |
| 374 | emulationViewModel.totalShaders.collectLatest { | 352 | binding.loadingText.setText(R.string.loading) |
| 375 | binding.loadingProgressIndicator.max = it | 353 | binding.loadingProgressIndicator.isIndeterminate = true |
| 376 | } | ||
| 377 | } | ||
| 378 | } | ||
| 379 | launch { | ||
| 380 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 381 | emulationViewModel.shaderMessage.collectLatest { | ||
| 382 | if (it.isNotEmpty()) { | ||
| 383 | binding.loadingText.text = it | ||
| 384 | } | ||
| 385 | } | ||
| 386 | } | ||
| 387 | } | 354 | } |
| 388 | launch { | 355 | } |
| 389 | repeatOnLifecycle(Lifecycle.State.RESUMED) { | 356 | emulationViewModel.totalShaders.collect(viewLifecycleOwner) { |
| 390 | driverViewModel.isInteractionAllowed.collect { | 357 | binding.loadingProgressIndicator.max = it |
| 391 | if (it) { | 358 | } |
| 392 | startEmulation() | 359 | emulationViewModel.shaderMessage.collect(viewLifecycleOwner) { |
| 393 | } | 360 | if (it.isNotEmpty()) { |
| 394 | } | 361 | binding.loadingText.text = it |
| 395 | } | ||
| 396 | } | 362 | } |
| 397 | launch { | 363 | } |
| 398 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 364 | |
| 399 | emulationViewModel.emulationStarted.collectLatest { | 365 | emulationViewModel.emulationStarted.collect(viewLifecycleOwner) { |
| 400 | if (it) { | 366 | if (it) { |
| 401 | binding.drawerLayout.setDrawerLockMode(IntSetting.LOCK_DRAWER.getInt()) | 367 | binding.drawerLayout.setDrawerLockMode(IntSetting.LOCK_DRAWER.getInt()) |
| 402 | ViewUtils.showView(binding.surfaceInputOverlay) | 368 | ViewUtils.showView(binding.surfaceInputOverlay) |
| 403 | ViewUtils.hideView(binding.loadingIndicator) | 369 | ViewUtils.hideView(binding.loadingIndicator) |
| 404 | 370 | ||
| 405 | emulationState.updateSurface() | 371 | emulationState.updateSurface() |
| 406 | 372 | ||
| 407 | // Setup overlays | 373 | // Setup overlays |
| 408 | updateShowFpsOverlay() | 374 | updateShowFpsOverlay() |
| 409 | updateThermalOverlay() | 375 | updateThermalOverlay() |
| 410 | } | ||
| 411 | } | ||
| 412 | } | ||
| 413 | } | 376 | } |
| 414 | launch { | 377 | } |
| 415 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 378 | emulationViewModel.isEmulationStopping.collect(viewLifecycleOwner) { |
| 416 | emulationViewModel.isEmulationStopping.collectLatest { | 379 | if (it) { |
| 417 | if (it) { | 380 | binding.loadingText.setText(R.string.shutting_down) |
| 418 | binding.loadingText.setText(R.string.shutting_down) | 381 | ViewUtils.showView(binding.loadingIndicator) |
| 419 | ViewUtils.showView(binding.loadingIndicator) | 382 | ViewUtils.hideView(binding.inputContainer) |
| 420 | ViewUtils.hideView(binding.inputContainer) | 383 | ViewUtils.hideView(binding.showFpsText) |
| 421 | ViewUtils.hideView(binding.showFpsText) | ||
| 422 | } | ||
| 423 | } | ||
| 424 | } | ||
| 425 | } | 384 | } |
| 426 | launch { | 385 | } |
| 427 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 386 | emulationViewModel.drawerOpen.collect(viewLifecycleOwner) { |
| 428 | emulationViewModel.drawerOpen.collect { | 387 | if (it) { |
| 429 | if (it) { | 388 | binding.drawerLayout.open() |
| 430 | binding.drawerLayout.open() | 389 | binding.inGameMenu.requestFocus() |
| 431 | binding.inGameMenu.requestFocus() | 390 | } else { |
| 432 | } else { | 391 | binding.drawerLayout.close() |
| 433 | binding.drawerLayout.close() | ||
| 434 | } | ||
| 435 | } | ||
| 436 | } | ||
| 437 | } | 392 | } |
| 438 | launch { | 393 | } |
| 439 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 394 | emulationViewModel.programChanged.collect(viewLifecycleOwner) { |
| 440 | emulationViewModel.programChanged.collect { | 395 | if (it != 0) { |
| 441 | if (it != 0) { | 396 | emulationViewModel.setEmulationStarted(false) |
| 442 | emulationViewModel.setEmulationStarted(false) | 397 | binding.drawerLayout.close() |
| 443 | binding.drawerLayout.close() | 398 | binding.drawerLayout |
| 444 | binding.drawerLayout | 399 | .setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) |
| 445 | .setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) | 400 | ViewUtils.hideView(binding.surfaceInputOverlay) |
| 446 | ViewUtils.hideView(binding.surfaceInputOverlay) | 401 | ViewUtils.showView(binding.loadingIndicator) |
| 447 | ViewUtils.showView(binding.loadingIndicator) | ||
| 448 | } | ||
| 449 | } | ||
| 450 | } | ||
| 451 | } | 402 | } |
| 452 | launch { | 403 | } |
| 453 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 404 | emulationViewModel.emulationStopped.collect(viewLifecycleOwner) { |
| 454 | emulationViewModel.emulationStopped.collect { | 405 | if (it && emulationViewModel.programChanged.value != -1) { |
| 455 | if (it && emulationViewModel.programChanged.value != -1) { | 406 | if (perfStatsUpdater != null) { |
| 456 | if (perfStatsUpdater != null) { | 407 | perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!) |
| 457 | perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!) | ||
| 458 | } | ||
| 459 | emulationState.changeProgram(emulationViewModel.programChanged.value) | ||
| 460 | emulationViewModel.setProgramChanged(-1) | ||
| 461 | emulationViewModel.setEmulationStopped(false) | ||
| 462 | } | ||
| 463 | } | ||
| 464 | } | 408 | } |
| 409 | emulationState.changeProgram(emulationViewModel.programChanged.value) | ||
| 410 | emulationViewModel.setProgramChanged(-1) | ||
| 411 | emulationViewModel.setEmulationStopped(false) | ||
| 465 | } | 412 | } |
| 466 | } | 413 | } |
| 414 | |||
| 415 | driverViewModel.isInteractionAllowed.collect(viewLifecycleOwner) { | ||
| 416 | if (it) startEmulation() | ||
| 417 | } | ||
| 467 | } | 418 | } |
| 468 | 419 | ||
| 469 | private fun startEmulation(programIndex: Int = 0) { | 420 | private fun startEmulation(programIndex: Int = 0) { |
| @@ -491,14 +442,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 491 | binding.drawerLayout.close() | 442 | binding.drawerLayout.close() |
| 492 | } | 443 | } |
| 493 | if (showInputOverlay) { | 444 | if (showInputOverlay) { |
| 494 | binding.surfaceInputOverlay.visibility = View.INVISIBLE | 445 | binding.surfaceInputOverlay.setVisible(visible = false, gone = false) |
| 495 | } | 446 | } |
| 496 | } else { | 447 | } else { |
| 497 | if (showInputOverlay && emulationViewModel.emulationStarted.value) { | 448 | binding.surfaceInputOverlay.setVisible( |
| 498 | binding.surfaceInputOverlay.visibility = View.VISIBLE | 449 | showInputOverlay && emulationViewModel.emulationStarted.value |
| 499 | } else { | 450 | ) |
| 500 | binding.surfaceInputOverlay.visibility = View.INVISIBLE | ||
| 501 | } | ||
| 502 | if (!isInFoldableLayout) { | 451 | if (!isInFoldableLayout) { |
| 503 | if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { | 452 | if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { |
| 504 | binding.surfaceInputOverlay.layout = OverlayLayout.Portrait | 453 | binding.surfaceInputOverlay.layout = OverlayLayout.Portrait |
| @@ -535,7 +484,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 535 | } | 484 | } |
| 536 | 485 | ||
| 537 | private fun updateShowFpsOverlay() { | 486 | private fun updateShowFpsOverlay() { |
| 538 | if (BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean()) { | 487 | val showOverlay = BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean() |
| 488 | binding.showFpsText.setVisible(showOverlay) | ||
| 489 | if (showOverlay) { | ||
| 539 | val SYSTEM_FPS = 0 | 490 | val SYSTEM_FPS = 0 |
| 540 | val FPS = 1 | 491 | val FPS = 1 |
| 541 | val FRAMETIME = 2 | 492 | val FRAMETIME = 2 |
| @@ -555,17 +506,17 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 555 | } | 506 | } |
| 556 | } | 507 | } |
| 557 | perfStatsUpdateHandler.post(perfStatsUpdater!!) | 508 | perfStatsUpdateHandler.post(perfStatsUpdater!!) |
| 558 | binding.showFpsText.visibility = View.VISIBLE | ||
| 559 | } else { | 509 | } else { |
| 560 | if (perfStatsUpdater != null) { | 510 | if (perfStatsUpdater != null) { |
| 561 | perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!) | 511 | perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!) |
| 562 | } | 512 | } |
| 563 | binding.showFpsText.visibility = View.GONE | ||
| 564 | } | 513 | } |
| 565 | } | 514 | } |
| 566 | 515 | ||
| 567 | private fun updateThermalOverlay() { | 516 | private fun updateThermalOverlay() { |
| 568 | if (BooleanSetting.SHOW_THERMAL_OVERLAY.getBoolean()) { | 517 | val showOverlay = BooleanSetting.SHOW_THERMAL_OVERLAY.getBoolean() |
| 518 | binding.showThermalsText.setVisible(showOverlay) | ||
| 519 | if (showOverlay) { | ||
| 569 | thermalStatsUpdater = { | 520 | thermalStatsUpdater = { |
| 570 | if (emulationViewModel.emulationStarted.value && | 521 | if (emulationViewModel.emulationStarted.value && |
| 571 | !emulationViewModel.isEmulationStopping.value | 522 | !emulationViewModel.isEmulationStopping.value |
| @@ -587,12 +538,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 587 | } | 538 | } |
| 588 | } | 539 | } |
| 589 | thermalStatsUpdateHandler.post(thermalStatsUpdater!!) | 540 | thermalStatsUpdateHandler.post(thermalStatsUpdater!!) |
| 590 | binding.showThermalsText.visibility = View.VISIBLE | ||
| 591 | } else { | 541 | } else { |
| 592 | if (thermalStatsUpdater != null) { | 542 | if (thermalStatsUpdater != null) { |
| 593 | thermalStatsUpdateHandler.removeCallbacks(thermalStatsUpdater!!) | 543 | thermalStatsUpdateHandler.removeCallbacks(thermalStatsUpdater!!) |
| 594 | } | 544 | } |
| 595 | binding.showThermalsText.visibility = View.GONE | ||
| 596 | } | 545 | } |
| 597 | } | 546 | } |
| 598 | 547 | ||
| @@ -861,12 +810,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 861 | } | 810 | } |
| 862 | } | 811 | } |
| 863 | } | 812 | } |
| 864 | binding.doneControlConfig.visibility = View.VISIBLE | 813 | binding.doneControlConfig.setVisible(false) |
| 865 | binding.surfaceInputOverlay.setIsInEditMode(true) | 814 | binding.surfaceInputOverlay.setIsInEditMode(true) |
| 866 | } | 815 | } |
| 867 | 816 | ||
| 868 | private fun stopConfiguringControls() { | 817 | private fun stopConfiguringControls() { |
| 869 | binding.doneControlConfig.visibility = View.GONE | 818 | binding.doneControlConfig.setVisible(false) |
| 870 | binding.surfaceInputOverlay.setIsInEditMode(false) | 819 | binding.surfaceInputOverlay.setIsInEditMode(false) |
| 871 | // Unlock the orientation if it was locked for editing | 820 | // Unlock the orientation if it was locked for editing |
| 872 | if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == EmulationOrientation.Unspecified.int) { | 821 | if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == EmulationOrientation.Unspecified.int) { |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt index 5c558b1a5..3a6f7a38c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt | |||
| @@ -13,9 +13,6 @@ import androidx.core.view.WindowInsetsCompat | |||
| 13 | import androidx.core.view.updatePadding | 13 | import androidx.core.view.updatePadding |
| 14 | import androidx.fragment.app.Fragment | 14 | import androidx.fragment.app.Fragment |
| 15 | import androidx.fragment.app.activityViewModels | 15 | import androidx.fragment.app.activityViewModels |
| 16 | import androidx.lifecycle.Lifecycle | ||
| 17 | import androidx.lifecycle.lifecycleScope | ||
| 18 | import androidx.lifecycle.repeatOnLifecycle | ||
| 19 | import androidx.navigation.findNavController | 16 | import androidx.navigation.findNavController |
| 20 | import androidx.recyclerview.widget.GridLayoutManager | 17 | import androidx.recyclerview.widget.GridLayoutManager |
| 21 | import com.google.android.material.transition.MaterialSharedAxis | 18 | import com.google.android.material.transition.MaterialSharedAxis |
| @@ -27,6 +24,7 @@ import org.yuzu.yuzu_emu.model.GamesViewModel | |||
| 27 | import org.yuzu.yuzu_emu.model.HomeViewModel | 24 | import org.yuzu.yuzu_emu.model.HomeViewModel |
| 28 | import org.yuzu.yuzu_emu.ui.main.MainActivity | 25 | import org.yuzu.yuzu_emu.ui.main.MainActivity |
| 29 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins | 26 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins |
| 27 | import org.yuzu.yuzu_emu.utils.collect | ||
| 30 | 28 | ||
| 31 | class GameFoldersFragment : Fragment() { | 29 | class GameFoldersFragment : Fragment() { |
| 32 | private var _binding: FragmentFoldersBinding? = null | 30 | private var _binding: FragmentFoldersBinding? = null |
| @@ -70,12 +68,8 @@ class GameFoldersFragment : Fragment() { | |||
| 70 | adapter = FolderAdapter(requireActivity(), gamesViewModel) | 68 | adapter = FolderAdapter(requireActivity(), gamesViewModel) |
| 71 | } | 69 | } |
| 72 | 70 | ||
| 73 | viewLifecycleOwner.lifecycleScope.launch { | 71 | gamesViewModel.folders.collect(viewLifecycleOwner) { |
| 74 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 72 | (binding.listFolders.adapter as FolderAdapter).submitList(it) |
| 75 | gamesViewModel.folders.collect { | ||
| 76 | (binding.listFolders.adapter as FolderAdapter).submitList(it) | ||
| 77 | } | ||
| 78 | } | ||
| 79 | } | 73 | } |
| 80 | 74 | ||
| 81 | val mainActivity = requireActivity() as MainActivity | 75 | val mainActivity = requireActivity() as MainActivity |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt index dbd56e84f..97a8954bb 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt | |||
| @@ -27,6 +27,7 @@ import org.yuzu.yuzu_emu.databinding.FragmentGameInfoBinding | |||
| 27 | import org.yuzu.yuzu_emu.model.GameVerificationResult | 27 | import org.yuzu.yuzu_emu.model.GameVerificationResult |
| 28 | import org.yuzu.yuzu_emu.model.HomeViewModel | 28 | import org.yuzu.yuzu_emu.model.HomeViewModel |
| 29 | import org.yuzu.yuzu_emu.utils.GameMetadata | 29 | import org.yuzu.yuzu_emu.utils.GameMetadata |
| 30 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 30 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins | 31 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins |
| 31 | 32 | ||
| 32 | class GameInfoFragment : Fragment() { | 33 | class GameInfoFragment : Fragment() { |
| @@ -85,7 +86,7 @@ class GameInfoFragment : Fragment() { | |||
| 85 | copyToClipboard(getString(R.string.developer), args.game.developer) | 86 | copyToClipboard(getString(R.string.developer), args.game.developer) |
| 86 | } | 87 | } |
| 87 | } else { | 88 | } else { |
| 88 | developer.visibility = View.GONE | 89 | developer.setVisible(false) |
| 89 | } | 90 | } |
| 90 | 91 | ||
| 91 | version.setHint(R.string.version) | 92 | version.setHint(R.string.version) |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt index 3ea5e16ca..c06842c59 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt | |||
| @@ -3,11 +3,9 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.fragments | 4 | package org.yuzu.yuzu_emu.fragments |
| 5 | 5 | ||
| 6 | import android.annotation.SuppressLint | ||
| 7 | import android.content.pm.ShortcutInfo | 6 | import android.content.pm.ShortcutInfo |
| 8 | import android.content.pm.ShortcutManager | 7 | import android.content.pm.ShortcutManager |
| 9 | import android.os.Bundle | 8 | import android.os.Bundle |
| 10 | import android.text.TextUtils | ||
| 11 | import android.view.LayoutInflater | 9 | import android.view.LayoutInflater |
| 12 | import android.view.View | 10 | import android.view.View |
| 13 | import android.view.ViewGroup | 11 | import android.view.ViewGroup |
| @@ -18,9 +16,7 @@ import androidx.core.view.WindowInsetsCompat | |||
| 18 | import androidx.core.view.updatePadding | 16 | import androidx.core.view.updatePadding |
| 19 | import androidx.fragment.app.Fragment | 17 | import androidx.fragment.app.Fragment |
| 20 | import androidx.fragment.app.activityViewModels | 18 | import androidx.fragment.app.activityViewModels |
| 21 | import androidx.lifecycle.Lifecycle | ||
| 22 | import androidx.lifecycle.lifecycleScope | 19 | import androidx.lifecycle.lifecycleScope |
| 23 | import androidx.lifecycle.repeatOnLifecycle | ||
| 24 | import androidx.navigation.findNavController | 20 | import androidx.navigation.findNavController |
| 25 | import androidx.navigation.fragment.navArgs | 21 | import androidx.navigation.fragment.navArgs |
| 26 | import androidx.recyclerview.widget.GridLayoutManager | 22 | import androidx.recyclerview.widget.GridLayoutManager |
| @@ -46,7 +42,9 @@ import org.yuzu.yuzu_emu.utils.FileUtil | |||
| 46 | import org.yuzu.yuzu_emu.utils.GameIconUtils | 42 | import org.yuzu.yuzu_emu.utils.GameIconUtils |
| 47 | import org.yuzu.yuzu_emu.utils.GpuDriverHelper | 43 | import org.yuzu.yuzu_emu.utils.GpuDriverHelper |
| 48 | import org.yuzu.yuzu_emu.utils.MemoryUtil | 44 | import org.yuzu.yuzu_emu.utils.MemoryUtil |
| 45 | import org.yuzu.yuzu_emu.utils.ViewUtils.marquee | ||
| 49 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins | 46 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins |
| 47 | import org.yuzu.yuzu_emu.utils.collect | ||
| 50 | import java.io.BufferedOutputStream | 48 | import java.io.BufferedOutputStream |
| 51 | import java.io.File | 49 | import java.io.File |
| 52 | 50 | ||
| @@ -76,8 +74,6 @@ class GamePropertiesFragment : Fragment() { | |||
| 76 | return binding.root | 74 | return binding.root |
| 77 | } | 75 | } |
| 78 | 76 | ||
| 79 | // This is using the correct scope, lint is just acting up | ||
| 80 | @SuppressLint("UnsafeRepeatOnLifecycleDetector") | ||
| 81 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | 77 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| 82 | super.onViewCreated(view, savedInstanceState) | 78 | super.onViewCreated(view, savedInstanceState) |
| 83 | homeViewModel.setNavigationVisibility(visible = false, animated = true) | 79 | homeViewModel.setNavigationVisibility(visible = false, animated = true) |
| @@ -107,13 +103,7 @@ class GamePropertiesFragment : Fragment() { | |||
| 107 | 103 | ||
| 108 | GameIconUtils.loadGameIcon(args.game, binding.imageGameScreen) | 104 | GameIconUtils.loadGameIcon(args.game, binding.imageGameScreen) |
| 109 | binding.title.text = args.game.title | 105 | binding.title.text = args.game.title |
| 110 | binding.title.postDelayed( | 106 | binding.title.marquee() |
| 111 | { | ||
| 112 | binding.title.ellipsize = TextUtils.TruncateAt.MARQUEE | ||
| 113 | binding.title.isSelected = true | ||
| 114 | }, | ||
| 115 | 3000 | ||
| 116 | ) | ||
| 117 | 107 | ||
| 118 | binding.buttonStart.setOnClickListener { | 108 | binding.buttonStart.setOnClickListener { |
| 119 | LaunchGameDialogFragment.newInstance(args.game) | 109 | LaunchGameDialogFragment.newInstance(args.game) |
| @@ -122,28 +112,14 @@ class GamePropertiesFragment : Fragment() { | |||
| 122 | 112 | ||
| 123 | reloadList() | 113 | reloadList() |
| 124 | 114 | ||
| 125 | viewLifecycleOwner.lifecycleScope.apply { | 115 | homeViewModel.openImportSaves.collect( |
| 126 | launch { | 116 | viewLifecycleOwner, |
| 127 | repeatOnLifecycle(Lifecycle.State.STARTED) { | 117 | resetState = { homeViewModel.setOpenImportSaves(false) } |
| 128 | homeViewModel.openImportSaves.collect { | 118 | ) { if (it) importSaves.launch(arrayOf("application/zip")) } |
| 129 | if (it) { | 119 | homeViewModel.reloadPropertiesList.collect( |
| 130 | importSaves.launch(arrayOf("application/zip")) | 120 | viewLifecycleOwner, |
| 131 | homeViewModel.setOpenImportSaves(false) | 121 | resetState = { homeViewModel.reloadPropertiesList(false) } |
| 132 | } | 122 | ) { if (it) reloadList() } |
| 133 | } | ||
| 134 | } | ||
| 135 | } | ||
| 136 | launch { | ||
| 137 | repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
| 138 | homeViewModel.reloadPropertiesList.collect { | ||
| 139 | if (it) { | ||
| 140 | reloadList() | ||
| 141 | homeViewModel.reloadPropertiesList(false) | ||
| 142 | } | ||
| 143 | } | ||
| 144 | } | ||
| 145 | } | ||
| 146 | } | ||
| 147 | 123 | ||
| 148 | setInsets() | 124 | setInsets() |
| 149 | } | 125 | } |
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/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt index 63112dc6f..d218da1c8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt | |||
| @@ -14,9 +14,6 @@ import androidx.core.view.WindowInsetsCompat | |||
| 14 | import androidx.core.view.updatePadding | 14 | import androidx.core.view.updatePadding |
| 15 | import androidx.fragment.app.Fragment | 15 | import androidx.fragment.app.Fragment |
| 16 | import androidx.fragment.app.activityViewModels | 16 | import androidx.fragment.app.activityViewModels |
| 17 | import androidx.lifecycle.Lifecycle | ||
| 18 | import androidx.lifecycle.lifecycleScope | ||
| 19 | import androidx.lifecycle.repeatOnLifecycle | ||
| 20 | import androidx.navigation.findNavController | 17 | import androidx.navigation.findNavController |
| 21 | import androidx.recyclerview.widget.GridLayoutManager | 18 | import androidx.recyclerview.widget.GridLayoutManager |
| 22 | import com.google.android.material.transition.MaterialSharedAxis | 19 | import com.google.android.material.transition.MaterialSharedAxis |
| @@ -35,6 +32,7 @@ import org.yuzu.yuzu_emu.ui.main.MainActivity | |||
| 35 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization | 32 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization |
| 36 | import org.yuzu.yuzu_emu.utils.FileUtil | 33 | import org.yuzu.yuzu_emu.utils.FileUtil |
| 37 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins | 34 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins |
| 35 | import org.yuzu.yuzu_emu.utils.collect | ||
| 38 | import java.io.BufferedOutputStream | 36 | import java.io.BufferedOutputStream |
| 39 | import java.io.File | 37 | import java.io.File |
| 40 | import java.math.BigInteger | 38 | import java.math.BigInteger |
| @@ -75,14 +73,10 @@ class InstallableFragment : Fragment() { | |||
| 75 | binding.root.findNavController().popBackStack() | 73 | binding.root.findNavController().popBackStack() |
| 76 | } | 74 | } |
| 77 | 75 | ||
| 78 | viewLifecycleOwner.lifecycleScope.launch { | 76 | homeViewModel.openImportSaves.collect(viewLifecycleOwner) { |
| 79 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 77 | if (it) { |
| 80 | homeViewModel.openImportSaves.collect { | 78 | importSaves.launch(arrayOf("application/zip")) |
| 81 | if (it) { | 79 | homeViewModel.setOpenImportSaves(false) |
| 82 | importSaves.launch(arrayOf("application/zip")) | ||
| 83 | homeViewModel.setOpenImportSaves(false) | ||
| 84 | } | ||
| 85 | } | ||
| 86 | } | 80 | } |
| 87 | } | 81 | } |
| 88 | 82 | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt index d201cb80c..ee3bb0386 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt | |||
| @@ -13,15 +13,13 @@ import androidx.appcompat.app.AlertDialog | |||
| 13 | import androidx.fragment.app.DialogFragment | 13 | import androidx.fragment.app.DialogFragment |
| 14 | import androidx.fragment.app.FragmentActivity | 14 | import androidx.fragment.app.FragmentActivity |
| 15 | import androidx.fragment.app.activityViewModels | 15 | import androidx.fragment.app.activityViewModels |
| 16 | import androidx.lifecycle.Lifecycle | ||
| 17 | import androidx.lifecycle.ViewModelProvider | 16 | import androidx.lifecycle.ViewModelProvider |
| 18 | import androidx.lifecycle.lifecycleScope | ||
| 19 | import androidx.lifecycle.repeatOnLifecycle | ||
| 20 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | 17 | import com.google.android.material.dialog.MaterialAlertDialogBuilder |
| 21 | import kotlinx.coroutines.launch | ||
| 22 | import org.yuzu.yuzu_emu.R | 18 | import org.yuzu.yuzu_emu.R |
| 23 | import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding | 19 | import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding |
| 24 | import org.yuzu.yuzu_emu.model.TaskViewModel | 20 | import org.yuzu.yuzu_emu.model.TaskViewModel |
| 21 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 22 | import org.yuzu.yuzu_emu.utils.collect | ||
| 25 | 23 | ||
| 26 | class ProgressDialogFragment : DialogFragment() { | 24 | class ProgressDialogFragment : DialogFragment() { |
| 27 | private val taskViewModel: TaskViewModel by activityViewModels() | 25 | private val taskViewModel: TaskViewModel by activityViewModels() |
| @@ -64,72 +62,50 @@ class ProgressDialogFragment : DialogFragment() { | |||
| 64 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | 62 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| 65 | super.onViewCreated(view, savedInstanceState) | 63 | super.onViewCreated(view, savedInstanceState) |
| 66 | binding.message.isSelected = true | 64 | binding.message.isSelected = true |
| 67 | viewLifecycleOwner.lifecycleScope.apply { | 65 | taskViewModel.isComplete.collect(viewLifecycleOwner) { |
| 68 | launch { | 66 | if (it) { |
| 69 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 67 | dismiss() |
| 70 | taskViewModel.isComplete.collect { | 68 | when (val result = taskViewModel.result.value) { |
| 71 | if (it) { | 69 | is String -> Toast.makeText( |
| 72 | dismiss() | 70 | requireContext(), |
| 73 | when (val result = taskViewModel.result.value) { | 71 | result, |
| 74 | is String -> Toast.makeText( | 72 | Toast.LENGTH_LONG |
| 75 | requireContext(), | 73 | ).show() |
| 76 | result, | 74 | |
| 77 | Toast.LENGTH_LONG | 75 | is MessageDialogFragment -> result.show( |
| 78 | ).show() | 76 | requireActivity().supportFragmentManager, |
| 79 | 77 | MessageDialogFragment.TAG | |
| 80 | is MessageDialogFragment -> result.show( | 78 | ) |
| 81 | requireActivity().supportFragmentManager, | 79 | |
| 82 | MessageDialogFragment.TAG | 80 | else -> { |
| 83 | ) | 81 | // Do nothing |
| 84 | |||
| 85 | else -> { | ||
| 86 | // Do nothing | ||
| 87 | } | ||
| 88 | } | ||
| 89 | taskViewModel.clear() | ||
| 90 | } | ||
| 91 | } | 82 | } |
| 92 | } | 83 | } |
| 84 | taskViewModel.clear() | ||
| 93 | } | 85 | } |
| 94 | launch { | 86 | } |
| 95 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 87 | taskViewModel.cancelled.collect(viewLifecycleOwner) { |
| 96 | taskViewModel.cancelled.collect { | 88 | if (it) { |
| 97 | if (it) { | 89 | dialog?.setTitle(R.string.cancelling) |
| 98 | dialog?.setTitle(R.string.cancelling) | ||
| 99 | } | ||
| 100 | } | ||
| 101 | } | ||
| 102 | } | ||
| 103 | launch { | ||
| 104 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 105 | taskViewModel.progress.collect { | ||
| 106 | if (it != 0.0) { | ||
| 107 | binding.progressBar.apply { | ||
| 108 | isIndeterminate = false | ||
| 109 | progress = ( | ||
| 110 | (it / taskViewModel.maxProgress.value) * | ||
| 111 | PROGRESS_BAR_RESOLUTION | ||
| 112 | ).toInt() | ||
| 113 | min = 0 | ||
| 114 | max = PROGRESS_BAR_RESOLUTION | ||
| 115 | } | ||
| 116 | } | ||
| 117 | } | ||
| 118 | } | ||
| 119 | } | 90 | } |
| 120 | launch { | 91 | } |
| 121 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 92 | taskViewModel.progress.collect(viewLifecycleOwner) { |
| 122 | taskViewModel.message.collect { | 93 | if (it != 0.0) { |
| 123 | if (it.isEmpty()) { | 94 | binding.progressBar.apply { |
| 124 | binding.message.visibility = View.GONE | 95 | isIndeterminate = false |
| 125 | } else { | 96 | progress = ( |
| 126 | binding.message.visibility = View.VISIBLE | 97 | (it / taskViewModel.maxProgress.value) * |
| 127 | binding.message.text = it | 98 | PROGRESS_BAR_RESOLUTION |
| 128 | } | 99 | ).toInt() |
| 129 | } | 100 | min = 0 |
| 101 | max = PROGRESS_BAR_RESOLUTION | ||
| 130 | } | 102 | } |
| 131 | } | 103 | } |
| 132 | } | 104 | } |
| 105 | taskViewModel.message.collect(viewLifecycleOwner) { | ||
| 106 | binding.message.setVisible(it.isNotEmpty()) | ||
| 107 | binding.message.text = it | ||
| 108 | } | ||
| 133 | } | 109 | } |
| 134 | 110 | ||
| 135 | // By default, the ProgressDialog will immediately dismiss itself upon a button being pressed. | 111 | // By default, the ProgressDialog will immediately dismiss itself upon a button being pressed. |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt index 20b10b1a0..662ae9760 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt | |||
| @@ -3,7 +3,6 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.fragments | 4 | package org.yuzu.yuzu_emu.fragments |
| 5 | 5 | ||
| 6 | import android.annotation.SuppressLint | ||
| 7 | import android.content.Context | 6 | import android.content.Context |
| 8 | import android.content.SharedPreferences | 7 | import android.content.SharedPreferences |
| 9 | import android.os.Bundle | 8 | import android.os.Bundle |
| @@ -18,14 +17,9 @@ import androidx.core.view.updatePadding | |||
| 18 | import androidx.core.widget.doOnTextChanged | 17 | import androidx.core.widget.doOnTextChanged |
| 19 | import androidx.fragment.app.Fragment | 18 | import androidx.fragment.app.Fragment |
| 20 | import androidx.fragment.app.activityViewModels | 19 | import androidx.fragment.app.activityViewModels |
| 21 | import androidx.lifecycle.Lifecycle | ||
| 22 | import androidx.lifecycle.lifecycleScope | ||
| 23 | import androidx.lifecycle.repeatOnLifecycle | ||
| 24 | import androidx.preference.PreferenceManager | 20 | import androidx.preference.PreferenceManager |
| 25 | import info.debatty.java.stringsimilarity.Jaccard | 21 | import info.debatty.java.stringsimilarity.Jaccard |
| 26 | import info.debatty.java.stringsimilarity.JaroWinkler | 22 | import info.debatty.java.stringsimilarity.JaroWinkler |
| 27 | import kotlinx.coroutines.flow.collectLatest | ||
| 28 | import kotlinx.coroutines.launch | ||
| 29 | import java.util.Locale | 23 | import java.util.Locale |
| 30 | import org.yuzu.yuzu_emu.R | 24 | import org.yuzu.yuzu_emu.R |
| 31 | import org.yuzu.yuzu_emu.YuzuApplication | 25 | import org.yuzu.yuzu_emu.YuzuApplication |
| @@ -35,6 +29,8 @@ import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager | |||
| 35 | import org.yuzu.yuzu_emu.model.Game | 29 | import org.yuzu.yuzu_emu.model.Game |
| 36 | import org.yuzu.yuzu_emu.model.GamesViewModel | 30 | import org.yuzu.yuzu_emu.model.GamesViewModel |
| 37 | import org.yuzu.yuzu_emu.model.HomeViewModel | 31 | import org.yuzu.yuzu_emu.model.HomeViewModel |
| 32 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 33 | import org.yuzu.yuzu_emu.utils.collect | ||
| 38 | 34 | ||
| 39 | class SearchFragment : Fragment() { | 35 | class SearchFragment : Fragment() { |
| 40 | private var _binding: FragmentSearchBinding? = null | 36 | private var _binding: FragmentSearchBinding? = null |
| @@ -58,8 +54,6 @@ class SearchFragment : Fragment() { | |||
| 58 | return binding.root | 54 | return binding.root |
| 59 | } | 55 | } |
| 60 | 56 | ||
| 61 | // This is using the correct scope, lint is just acting up | ||
| 62 | @SuppressLint("UnsafeRepeatOnLifecycleDetector") | ||
| 63 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | 57 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| 64 | super.onViewCreated(view, savedInstanceState) | 58 | super.onViewCreated(view, savedInstanceState) |
| 65 | homeViewModel.setNavigationVisibility(visible = true, animated = true) | 59 | homeViewModel.setNavigationVisibility(visible = true, animated = true) |
| @@ -81,42 +75,18 @@ class SearchFragment : Fragment() { | |||
| 81 | binding.chipGroup.setOnCheckedStateChangeListener { _, _ -> filterAndSearch() } | 75 | binding.chipGroup.setOnCheckedStateChangeListener { _, _ -> filterAndSearch() } |
| 82 | 76 | ||
| 83 | binding.searchText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int -> | 77 | binding.searchText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int -> |
| 84 | if (text.toString().isNotEmpty()) { | 78 | binding.clearButton.setVisible(text.toString().isNotEmpty()) |
| 85 | binding.clearButton.visibility = View.VISIBLE | ||
| 86 | } else { | ||
| 87 | binding.clearButton.visibility = View.INVISIBLE | ||
| 88 | } | ||
| 89 | filterAndSearch() | 79 | filterAndSearch() |
| 90 | } | 80 | } |
| 91 | 81 | ||
| 92 | viewLifecycleOwner.lifecycleScope.apply { | 82 | gamesViewModel.searchFocused.collect( |
| 93 | launch { | 83 | viewLifecycleOwner, |
| 94 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 84 | resetState = { gamesViewModel.setSearchFocused(false) } |
| 95 | gamesViewModel.searchFocused.collect { | 85 | ) { if (it) focusSearch() } |
| 96 | if (it) { | 86 | gamesViewModel.games.collect(viewLifecycleOwner) { filterAndSearch() } |
| 97 | focusSearch() | 87 | gamesViewModel.searchedGames.collect(viewLifecycleOwner) { |
| 98 | gamesViewModel.setSearchFocused(false) | 88 | (binding.gridGamesSearch.adapter as GameAdapter).submitList(it) |
| 99 | } | 89 | binding.noResultsView.setVisible(it.isNotEmpty()) |
| 100 | } | ||
| 101 | } | ||
| 102 | } | ||
| 103 | launch { | ||
| 104 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 105 | gamesViewModel.games.collectLatest { filterAndSearch() } | ||
| 106 | } | ||
| 107 | } | ||
| 108 | launch { | ||
| 109 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 110 | gamesViewModel.searchedGames.collect { | ||
| 111 | (binding.gridGamesSearch.adapter as GameAdapter).submitList(it) | ||
| 112 | if (it.isEmpty()) { | ||
| 113 | binding.noResultsView.visibility = View.VISIBLE | ||
| 114 | } else { | ||
| 115 | binding.noResultsView.visibility = View.GONE | ||
| 116 | } | ||
| 117 | } | ||
| 118 | } | ||
| 119 | } | ||
| 120 | } | 90 | } |
| 121 | 91 | ||
| 122 | binding.clearButton.setOnClickListener { binding.searchText.setText("") } | 92 | binding.clearButton.setOnClickListener { binding.searchText.setText("") } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt index ebf41a639..4f7548e98 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt | |||
| @@ -4,7 +4,6 @@ | |||
| 4 | package org.yuzu.yuzu_emu.fragments | 4 | package org.yuzu.yuzu_emu.fragments |
| 5 | 5 | ||
| 6 | import android.Manifest | 6 | import android.Manifest |
| 7 | import android.annotation.SuppressLint | ||
| 8 | import android.content.Intent | 7 | import android.content.Intent |
| 9 | import android.os.Build | 8 | import android.os.Build |
| 10 | import android.os.Bundle | 9 | import android.os.Bundle |
| @@ -23,9 +22,6 @@ import androidx.core.view.isVisible | |||
| 23 | import androidx.core.view.updatePadding | 22 | import androidx.core.view.updatePadding |
| 24 | import androidx.fragment.app.Fragment | 23 | import androidx.fragment.app.Fragment |
| 25 | import androidx.fragment.app.activityViewModels | 24 | import androidx.fragment.app.activityViewModels |
| 26 | import androidx.lifecycle.Lifecycle | ||
| 27 | import androidx.lifecycle.lifecycleScope | ||
| 28 | import androidx.lifecycle.repeatOnLifecycle | ||
| 29 | import androidx.navigation.findNavController | 25 | import androidx.navigation.findNavController |
| 30 | import androidx.preference.PreferenceManager | 26 | import androidx.preference.PreferenceManager |
| 31 | import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback | 27 | import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback |
| @@ -46,6 +42,8 @@ import org.yuzu.yuzu_emu.ui.main.MainActivity | |||
| 46 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization | 42 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization |
| 47 | import org.yuzu.yuzu_emu.utils.NativeConfig | 43 | import org.yuzu.yuzu_emu.utils.NativeConfig |
| 48 | import org.yuzu.yuzu_emu.utils.ViewUtils | 44 | import org.yuzu.yuzu_emu.utils.ViewUtils |
| 45 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 46 | import org.yuzu.yuzu_emu.utils.collect | ||
| 49 | 47 | ||
| 50 | class SetupFragment : Fragment() { | 48 | class SetupFragment : Fragment() { |
| 51 | private var _binding: FragmentSetupBinding? = null | 49 | private var _binding: FragmentSetupBinding? = null |
| @@ -77,8 +75,6 @@ class SetupFragment : Fragment() { | |||
| 77 | return binding.root | 75 | return binding.root |
| 78 | } | 76 | } |
| 79 | 77 | ||
| 80 | // This is using the correct scope, lint is just acting up | ||
| 81 | @SuppressLint("UnsafeRepeatOnLifecycleDetector") | ||
| 82 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | 78 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| 83 | mainActivity = requireActivity() as MainActivity | 79 | mainActivity = requireActivity() as MainActivity |
| 84 | 80 | ||
| @@ -210,28 +206,14 @@ class SetupFragment : Fragment() { | |||
| 210 | ) | 206 | ) |
| 211 | } | 207 | } |
| 212 | 208 | ||
| 213 | viewLifecycleOwner.lifecycleScope.apply { | 209 | homeViewModel.shouldPageForward.collect( |
| 214 | launch { | 210 | viewLifecycleOwner, |
| 215 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 211 | resetState = { homeViewModel.setShouldPageForward(false) } |
| 216 | homeViewModel.shouldPageForward.collect { | 212 | ) { if (it) pageForward() } |
| 217 | if (it) { | 213 | homeViewModel.gamesDirSelected.collect( |
| 218 | pageForward() | 214 | viewLifecycleOwner, |
| 219 | homeViewModel.setShouldPageForward(false) | 215 | resetState = { homeViewModel.setGamesDirSelected(false) } |
| 220 | } | 216 | ) { if (it) gamesDirCallback.onStepCompleted() } |
| 221 | } | ||
| 222 | } | ||
| 223 | } | ||
| 224 | launch { | ||
| 225 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 226 | homeViewModel.gamesDirSelected.collect { | ||
| 227 | if (it) { | ||
| 228 | gamesDirCallback.onStepCompleted() | ||
| 229 | homeViewModel.setGamesDirSelected(false) | ||
| 230 | } | ||
| 231 | } | ||
| 232 | } | ||
| 233 | } | ||
| 234 | } | ||
| 235 | 217 | ||
| 236 | binding.viewPager2.apply { | 218 | binding.viewPager2.apply { |
| 237 | adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages) | 219 | adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages) |
| @@ -292,12 +274,8 @@ class SetupFragment : Fragment() { | |||
| 292 | val backIsVisible = savedInstanceState.getBoolean(KEY_BACK_VISIBILITY) | 274 | val backIsVisible = savedInstanceState.getBoolean(KEY_BACK_VISIBILITY) |
| 293 | hasBeenWarned = savedInstanceState.getBooleanArray(KEY_HAS_BEEN_WARNED)!! | 275 | hasBeenWarned = savedInstanceState.getBooleanArray(KEY_HAS_BEEN_WARNED)!! |
| 294 | 276 | ||
| 295 | if (nextIsVisible) { | 277 | binding.buttonNext.setVisible(nextIsVisible) |
| 296 | binding.buttonNext.visibility = View.VISIBLE | 278 | binding.buttonBack.setVisible(backIsVisible) |
| 297 | } | ||
| 298 | if (backIsVisible) { | ||
| 299 | binding.buttonBack.visibility = View.VISIBLE | ||
| 300 | } | ||
| 301 | } else { | 279 | } else { |
| 302 | hasBeenWarned = BooleanArray(pages.size) | 280 | hasBeenWarned = BooleanArray(pages.size) |
| 303 | } | 281 | } |
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/ui/GamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt index 23ca49b53..fadb20e39 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt | |||
| @@ -3,7 +3,6 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.ui | 4 | package org.yuzu.yuzu_emu.ui |
| 5 | 5 | ||
| 6 | import android.annotation.SuppressLint | ||
| 7 | import android.os.Bundle | 6 | import android.os.Bundle |
| 8 | import android.view.LayoutInflater | 7 | import android.view.LayoutInflater |
| 9 | import android.view.View | 8 | import android.view.View |
| @@ -14,19 +13,16 @@ import androidx.core.view.WindowInsetsCompat | |||
| 14 | import androidx.core.view.updatePadding | 13 | import androidx.core.view.updatePadding |
| 15 | import androidx.fragment.app.Fragment | 14 | import androidx.fragment.app.Fragment |
| 16 | import androidx.fragment.app.activityViewModels | 15 | import androidx.fragment.app.activityViewModels |
| 17 | import androidx.lifecycle.Lifecycle | ||
| 18 | import androidx.lifecycle.lifecycleScope | ||
| 19 | import androidx.lifecycle.repeatOnLifecycle | ||
| 20 | import com.google.android.material.color.MaterialColors | 16 | import com.google.android.material.color.MaterialColors |
| 21 | import kotlinx.coroutines.flow.collectLatest | ||
| 22 | import kotlinx.coroutines.launch | ||
| 23 | import org.yuzu.yuzu_emu.R | 17 | import org.yuzu.yuzu_emu.R |
| 24 | import org.yuzu.yuzu_emu.adapters.GameAdapter | 18 | import org.yuzu.yuzu_emu.adapters.GameAdapter |
| 25 | import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding | 19 | import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding |
| 26 | import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager | 20 | import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager |
| 27 | import org.yuzu.yuzu_emu.model.GamesViewModel | 21 | import org.yuzu.yuzu_emu.model.GamesViewModel |
| 28 | import org.yuzu.yuzu_emu.model.HomeViewModel | 22 | import org.yuzu.yuzu_emu.model.HomeViewModel |
| 23 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 29 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins | 24 | import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins |
| 25 | import org.yuzu.yuzu_emu.utils.collect | ||
| 30 | 26 | ||
| 31 | class GamesFragment : Fragment() { | 27 | class GamesFragment : Fragment() { |
| 32 | private var _binding: FragmentGamesBinding? = null | 28 | private var _binding: FragmentGamesBinding? = null |
| @@ -44,8 +40,6 @@ class GamesFragment : Fragment() { | |||
| 44 | return binding.root | 40 | return binding.root |
| 45 | } | 41 | } |
| 46 | 42 | ||
| 47 | // This is using the correct scope, lint is just acting up | ||
| 48 | @SuppressLint("UnsafeRepeatOnLifecycleDetector") | ||
| 49 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | 43 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| 50 | super.onViewCreated(view, savedInstanceState) | 44 | super.onViewCreated(view, savedInstanceState) |
| 51 | homeViewModel.setNavigationVisibility(visible = true, animated = true) | 45 | homeViewModel.setNavigationVisibility(visible = true, animated = true) |
| @@ -88,49 +82,28 @@ class GamesFragment : Fragment() { | |||
| 88 | } | 82 | } |
| 89 | } | 83 | } |
| 90 | 84 | ||
| 91 | viewLifecycleOwner.lifecycleScope.apply { | 85 | gamesViewModel.isReloading.collect(viewLifecycleOwner) { |
| 92 | launch { | 86 | binding.swipeRefresh.isRefreshing = it |
| 93 | repeatOnLifecycle(Lifecycle.State.RESUMED) { | 87 | binding.noticeText.setVisible( |
| 94 | gamesViewModel.isReloading.collect { | 88 | visible = gamesViewModel.games.value.isEmpty() && !it, |
| 95 | binding.swipeRefresh.isRefreshing = it | 89 | gone = false |
| 96 | if (gamesViewModel.games.value.isEmpty() && !it) { | 90 | ) |
| 97 | binding.noticeText.visibility = View.VISIBLE | 91 | } |
| 98 | } else { | 92 | gamesViewModel.games.collect(viewLifecycleOwner) { |
| 99 | binding.noticeText.visibility = View.INVISIBLE | 93 | (binding.gridGames.adapter as GameAdapter).submitList(it) |
| 100 | } | 94 | } |
| 101 | } | 95 | gamesViewModel.shouldSwapData.collect( |
| 102 | } | 96 | viewLifecycleOwner, |
| 103 | } | 97 | resetState = { gamesViewModel.setShouldSwapData(false) } |
| 104 | launch { | 98 | ) { |
| 105 | repeatOnLifecycle(Lifecycle.State.RESUMED) { | 99 | if (it) { |
| 106 | gamesViewModel.games.collectLatest { | 100 | (binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value) |
| 107 | (binding.gridGames.adapter as GameAdapter).submitList(it) | ||
| 108 | } | ||
| 109 | } | ||
| 110 | } | ||
| 111 | launch { | ||
| 112 | repeatOnLifecycle(Lifecycle.State.RESUMED) { | ||
| 113 | gamesViewModel.shouldSwapData.collect { | ||
| 114 | if (it) { | ||
| 115 | (binding.gridGames.adapter as GameAdapter).submitList( | ||
| 116 | gamesViewModel.games.value | ||
| 117 | ) | ||
| 118 | gamesViewModel.setShouldSwapData(false) | ||
| 119 | } | ||
| 120 | } | ||
| 121 | } | ||
| 122 | } | ||
| 123 | launch { | ||
| 124 | repeatOnLifecycle(Lifecycle.State.RESUMED) { | ||
| 125 | gamesViewModel.shouldScrollToTop.collect { | ||
| 126 | if (it) { | ||
| 127 | scrollToTop() | ||
| 128 | gamesViewModel.setShouldScrollToTop(false) | ||
| 129 | } | ||
| 130 | } | ||
| 131 | } | ||
| 132 | } | 101 | } |
| 133 | } | 102 | } |
| 103 | gamesViewModel.shouldScrollToTop.collect( | ||
| 104 | viewLifecycleOwner, | ||
| 105 | resetState = { gamesViewModel.setShouldScrollToTop(false) } | ||
| 106 | ) { if (it) scrollToTop() } | ||
| 134 | 107 | ||
| 135 | setInsets() | 108 | setInsets() |
| 136 | } | 109 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 4df4ac4c6..757463a0b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt | |||
| @@ -19,9 +19,6 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen | |||
| 19 | import androidx.core.view.ViewCompat | 19 | import androidx.core.view.ViewCompat |
| 20 | import androidx.core.view.WindowCompat | 20 | import androidx.core.view.WindowCompat |
| 21 | import androidx.core.view.WindowInsetsCompat | 21 | import androidx.core.view.WindowInsetsCompat |
| 22 | import androidx.lifecycle.Lifecycle | ||
| 23 | import androidx.lifecycle.lifecycleScope | ||
| 24 | import androidx.lifecycle.repeatOnLifecycle | ||
| 25 | import androidx.navigation.NavController | 22 | import androidx.navigation.NavController |
| 26 | import androidx.navigation.fragment.NavHostFragment | 23 | import androidx.navigation.fragment.NavHostFragment |
| 27 | import androidx.navigation.ui.setupWithNavController | 24 | import androidx.navigation.ui.setupWithNavController |
| @@ -30,7 +27,6 @@ import com.google.android.material.color.MaterialColors | |||
| 30 | import com.google.android.material.navigation.NavigationBarView | 27 | import com.google.android.material.navigation.NavigationBarView |
| 31 | import java.io.File | 28 | import java.io.File |
| 32 | import java.io.FilenameFilter | 29 | import java.io.FilenameFilter |
| 33 | import kotlinx.coroutines.launch | ||
| 34 | import org.yuzu.yuzu_emu.HomeNavigationDirections | 30 | import org.yuzu.yuzu_emu.HomeNavigationDirections |
| 35 | import org.yuzu.yuzu_emu.NativeLibrary | 31 | import org.yuzu.yuzu_emu.NativeLibrary |
| 36 | import org.yuzu.yuzu_emu.R | 32 | import org.yuzu.yuzu_emu.R |
| @@ -47,6 +43,7 @@ import org.yuzu.yuzu_emu.model.InstallResult | |||
| 47 | import org.yuzu.yuzu_emu.model.TaskState | 43 | import org.yuzu.yuzu_emu.model.TaskState |
| 48 | import org.yuzu.yuzu_emu.model.TaskViewModel | 44 | import org.yuzu.yuzu_emu.model.TaskViewModel |
| 49 | import org.yuzu.yuzu_emu.utils.* | 45 | import org.yuzu.yuzu_emu.utils.* |
| 46 | import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible | ||
| 50 | import java.io.BufferedInputStream | 47 | import java.io.BufferedInputStream |
| 51 | import java.io.BufferedOutputStream | 48 | import java.io.BufferedOutputStream |
| 52 | import java.util.zip.ZipEntry | 49 | import java.util.zip.ZipEntry |
| @@ -139,42 +136,23 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 139 | 136 | ||
| 140 | // Prevents navigation from being drawn for a short time on recreation if set to hidden | 137 | // Prevents navigation from being drawn for a short time on recreation if set to hidden |
| 141 | if (!homeViewModel.navigationVisible.value.first) { | 138 | if (!homeViewModel.navigationVisible.value.first) { |
| 142 | binding.navigationView.visibility = View.INVISIBLE | 139 | binding.navigationView.setVisible(visible = false, gone = false) |
| 143 | binding.statusBarShade.visibility = View.INVISIBLE | 140 | binding.statusBarShade.setVisible(visible = false, gone = false) |
| 144 | } | 141 | } |
| 145 | 142 | ||
| 146 | lifecycleScope.apply { | 143 | homeViewModel.navigationVisible.collect(this) { showNavigation(it.first, it.second) } |
| 147 | launch { | 144 | homeViewModel.statusBarShadeVisible.collect(this) { showStatusBarShade(it) } |
| 148 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 145 | homeViewModel.contentToInstall.collect( |
| 149 | homeViewModel.navigationVisible.collect { showNavigation(it.first, it.second) } | 146 | this, |
| 150 | } | 147 | resetState = { homeViewModel.setContentToInstall(null) } |
| 151 | } | 148 | ) { |
| 152 | launch { | 149 | if (it != null) { |
| 153 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 150 | installContent(it) |
| 154 | homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) } | ||
| 155 | } | ||
| 156 | } | ||
| 157 | launch { | ||
| 158 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 159 | homeViewModel.contentToInstall.collect { | ||
| 160 | if (it != null) { | ||
| 161 | installContent(it) | ||
| 162 | homeViewModel.setContentToInstall(null) | ||
| 163 | } | ||
| 164 | } | ||
| 165 | } | ||
| 166 | } | ||
| 167 | launch { | ||
| 168 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 169 | homeViewModel.checkKeys.collect { | ||
| 170 | if (it) { | ||
| 171 | checkKeys() | ||
| 172 | homeViewModel.setCheckKeys(false) | ||
| 173 | } | ||
| 174 | } | ||
| 175 | } | ||
| 176 | } | 151 | } |
| 177 | } | 152 | } |
| 153 | homeViewModel.checkKeys.collect(this, resetState = { homeViewModel.setCheckKeys(false) }) { | ||
| 154 | if (it) checkKeys() | ||
| 155 | } | ||
| 178 | 156 | ||
| 179 | setInsets() | 157 | setInsets() |
| 180 | } | 158 | } |
| @@ -214,18 +192,14 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 214 | 192 | ||
| 215 | private fun showNavigation(visible: Boolean, animated: Boolean) { | 193 | private fun showNavigation(visible: Boolean, animated: Boolean) { |
| 216 | if (!animated) { | 194 | if (!animated) { |
| 217 | if (visible) { | 195 | binding.navigationView.setVisible(visible) |
| 218 | binding.navigationView.visibility = View.VISIBLE | ||
| 219 | } else { | ||
| 220 | binding.navigationView.visibility = View.INVISIBLE | ||
| 221 | } | ||
| 222 | return | 196 | return |
| 223 | } | 197 | } |
| 224 | 198 | ||
| 225 | val smallLayout = resources.getBoolean(R.bool.small_layout) | 199 | val smallLayout = resources.getBoolean(R.bool.small_layout) |
| 226 | binding.navigationView.animate().apply { | 200 | binding.navigationView.animate().apply { |
| 227 | if (visible) { | 201 | if (visible) { |
| 228 | binding.navigationView.visibility = View.VISIBLE | 202 | binding.navigationView.setVisible(true) |
| 229 | duration = 300 | 203 | duration = 300 |
| 230 | interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f) | 204 | interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f) |
| 231 | 205 | ||
| @@ -264,7 +238,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 264 | } | 238 | } |
| 265 | }.withEndAction { | 239 | }.withEndAction { |
| 266 | if (!visible) { | 240 | if (!visible) { |
| 267 | binding.navigationView.visibility = View.INVISIBLE | 241 | binding.navigationView.setVisible(visible = false, gone = false) |
| 268 | } | 242 | } |
| 269 | }.start() | 243 | }.start() |
| 270 | } | 244 | } |
| @@ -272,7 +246,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 272 | private fun showStatusBarShade(visible: Boolean) { | 246 | private fun showStatusBarShade(visible: Boolean) { |
| 273 | binding.statusBarShade.animate().apply { | 247 | binding.statusBarShade.animate().apply { |
| 274 | if (visible) { | 248 | if (visible) { |
| 275 | binding.statusBarShade.visibility = View.VISIBLE | 249 | binding.statusBarShade.setVisible(true) |
| 276 | binding.statusBarShade.translationY = binding.statusBarShade.height.toFloat() * -2 | 250 | binding.statusBarShade.translationY = binding.statusBarShade.height.toFloat() * -2 |
| 277 | duration = 300 | 251 | duration = 300 |
| 278 | translationY(0f) | 252 | translationY(0f) |
| @@ -284,7 +258,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 284 | } | 258 | } |
| 285 | }.withEndAction { | 259 | }.withEndAction { |
| 286 | if (!visible) { | 260 | if (!visible) { |
| 287 | binding.statusBarShade.visibility = View.INVISIBLE | 261 | binding.statusBarShade.setVisible(visible = false, gone = false) |
| 288 | } | 262 | } |
| 289 | }.start() | 263 | }.start() |
| 290 | } | 264 | } |
| @@ -524,7 +498,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 524 | this@MainActivity, | 498 | this@MainActivity, |
| 525 | titleId = R.string.content_install_notice, | 499 | titleId = R.string.content_install_notice, |
| 526 | descriptionId = R.string.content_install_notice_description, | 500 | descriptionId = R.string.content_install_notice_description, |
| 527 | positiveAction = { homeViewModel.setContentToInstall(documents) } | 501 | positiveAction = { homeViewModel.setContentToInstall(documents) }, |
| 502 | negativeAction = {} | ||
| 528 | ) | 503 | ) |
| 529 | } | 504 | } |
| 530 | }.show(supportFragmentManager, ProgressDialogFragment.TAG) | 505 | }.show(supportFragmentManager, ProgressDialogFragment.TAG) |
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/LifecycleUtils.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/LifecycleUtils.kt new file mode 100644 index 000000000..d5c19c681 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/LifecycleUtils.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.utils | ||
| 5 | |||
| 6 | import androidx.lifecycle.Lifecycle | ||
| 7 | import androidx.lifecycle.LifecycleOwner | ||
| 8 | import androidx.lifecycle.lifecycleScope | ||
| 9 | import androidx.lifecycle.repeatOnLifecycle | ||
| 10 | import kotlinx.coroutines.flow.Flow | ||
| 11 | import kotlinx.coroutines.flow.MutableStateFlow | ||
| 12 | import kotlinx.coroutines.launch | ||
| 13 | |||
| 14 | /** | ||
| 15 | * Collects this [Flow] with a given [LifecycleOwner]. | ||
| 16 | * @param scope [LifecycleOwner] that this [Flow] will be collected with. | ||
| 17 | * @param repeatState When to repeat collection on this [Flow]. | ||
| 18 | * @param resetState Optional lambda to reset state of an underlying [MutableStateFlow] after | ||
| 19 | * [stateCollector] has been run. | ||
| 20 | * @param stateCollector Lambda that receives new state. | ||
| 21 | */ | ||
| 22 | inline fun <reified T> Flow<T>.collect( | ||
| 23 | scope: LifecycleOwner, | ||
| 24 | repeatState: Lifecycle.State = Lifecycle.State.CREATED, | ||
| 25 | crossinline resetState: () -> Unit = {}, | ||
| 26 | crossinline stateCollector: (state: T) -> Unit | ||
| 27 | ) { | ||
| 28 | scope.apply { | ||
| 29 | lifecycleScope.launch { | ||
| 30 | repeatOnLifecycle(repeatState) { | ||
| 31 | this@collect.collect { | ||
| 32 | stateCollector(it) | ||
| 33 | resetState() | ||
| 34 | } | ||
| 35 | } | ||
| 36 | } | ||
| 37 | } | ||
| 38 | } | ||
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/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt index ffbfa9337..244091aec 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt | |||
| @@ -3,8 +3,10 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.utils | 4 | package org.yuzu.yuzu_emu.utils |
| 5 | 5 | ||
| 6 | import android.text.TextUtils | ||
| 6 | import android.view.View | 7 | import android.view.View |
| 7 | import android.view.ViewGroup | 8 | import android.view.ViewGroup |
| 9 | import android.widget.TextView | ||
| 8 | 10 | ||
| 9 | object ViewUtils { | 11 | object ViewUtils { |
| 10 | fun showView(view: View, length: Long = 300) { | 12 | fun showView(view: View, length: Long = 300) { |
| @@ -57,4 +59,35 @@ object ViewUtils { | |||
| 57 | } | 59 | } |
| 58 | this.layoutParams = layoutParams | 60 | this.layoutParams = layoutParams |
| 59 | } | 61 | } |
| 62 | |||
| 63 | /** | ||
| 64 | * Shows or hides a view. | ||
| 65 | * @param visible Whether a view will be made View.VISIBLE or View.INVISIBLE/GONE. | ||
| 66 | * @param gone Optional parameter for hiding a view. Uses View.GONE if true and View.INVISIBLE otherwise. | ||
| 67 | */ | ||
| 68 | fun View.setVisible(visible: Boolean, gone: Boolean = true) { | ||
| 69 | visibility = if (visible) { | ||
| 70 | View.VISIBLE | ||
| 71 | } else { | ||
| 72 | if (gone) { | ||
| 73 | View.GONE | ||
| 74 | } else { | ||
| 75 | View.INVISIBLE | ||
| 76 | } | ||
| 77 | } | ||
| 78 | } | ||
| 79 | |||
| 80 | /** | ||
| 81 | * Starts a marquee on some text. | ||
| 82 | * @param delay Optional parameter for changing the start delay. 3 seconds of delay by default. | ||
| 83 | */ | ||
| 84 | fun TextView.marquee(delay: Long = 3000) { | ||
| 85 | ellipsize = null | ||
| 86 | marqueeRepeatLimit = -1 | ||
| 87 | isSingleLine = true | ||
| 88 | postDelayed({ | ||
| 89 | ellipsize = TextUtils.TruncateAt.MARQUEE | ||
| 90 | isSelected = true | ||
| 91 | }, delay) | ||
| 92 | } | ||
| 60 | } | 93 | } |
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..06db55369 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" |
| @@ -24,39 +25,18 @@ void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) { | |||
| 24 | 25 | ||
| 25 | void EmuWindow_Android::OnTouchPressed(int id, float x, float y) { | 26 | void EmuWindow_Android::OnTouchPressed(int id, float x, float y) { |
| 26 | const auto [touch_x, touch_y] = MapToTouchScreen(x, y); | 27 | const auto [touch_x, touch_y] = MapToTouchScreen(x, y); |
| 27 | m_input_subsystem->GetTouchScreen()->TouchPressed(touch_x, touch_y, id); | 28 | EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchPressed(touch_x, |
| 29 | touch_y, id); | ||
| 28 | } | 30 | } |
| 29 | 31 | ||
| 30 | void EmuWindow_Android::OnTouchMoved(int id, float x, float y) { | 32 | void EmuWindow_Android::OnTouchMoved(int id, float x, float y) { |
| 31 | const auto [touch_x, touch_y] = MapToTouchScreen(x, y); | 33 | const auto [touch_x, touch_y] = MapToTouchScreen(x, y); |
| 32 | m_input_subsystem->GetTouchScreen()->TouchMoved(touch_x, touch_y, id); | 34 | EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchMoved(touch_x, |
| 35 | touch_y, id); | ||
| 33 | } | 36 | } |
| 34 | 37 | ||
| 35 | void EmuWindow_Android::OnTouchReleased(int id) { | 38 | void EmuWindow_Android::OnTouchReleased(int id) { |
| 36 | m_input_subsystem->GetTouchScreen()->TouchReleased(id); | 39 | EmulationSession::GetInstance().GetInputSubsystem().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 | } | 40 | } |
| 61 | 41 | ||
| 62 | void EmuWindow_Android::OnFrameDisplayed() { | 42 | void EmuWindow_Android::OnFrameDisplayed() { |
| @@ -67,10 +47,9 @@ void EmuWindow_Android::OnFrameDisplayed() { | |||
| 67 | } | 47 | } |
| 68 | } | 48 | } |
| 69 | 49 | ||
| 70 | EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem, | 50 | EmuWindow_Android::EmuWindow_Android(ANativeWindow* surface, |
| 71 | ANativeWindow* surface, | ||
| 72 | std::shared_ptr<Common::DynamicLibrary> driver_library) | 51 | std::shared_ptr<Common::DynamicLibrary> driver_library) |
| 73 | : m_input_subsystem{input_subsystem}, m_driver_library{driver_library} { | 52 | : m_driver_library{driver_library} { |
| 74 | LOG_INFO(Frontend, "initializing"); | 53 | LOG_INFO(Frontend, "initializing"); |
| 75 | 54 | ||
| 76 | if (!surface) { | 55 | if (!surface) { |
| @@ -80,10 +59,4 @@ EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsyste | |||
| 80 | 59 | ||
| 81 | OnSurfaceChanged(surface); | 60 | OnSurfaceChanged(surface); |
| 82 | window_info.type = Core::Frontend::WindowSystemType::Android; | 61 | 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 | } | 62 | } |
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..d7b5fc6da 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,22 +30,17 @@ 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 OnFrameDisplayed() override; | ||
| 40 | |||
| 39 | void OnTouchPressed(int id, float x, float y); | 41 | void OnTouchPressed(int id, float x, float y); |
| 40 | void OnTouchMoved(int id, float x, float y); | 42 | void OnTouchMoved(int id, float x, float y); |
| 41 | void OnTouchReleased(int id); | 43 | 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; | ||
| 49 | 44 | ||
| 50 | std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override { | 45 | std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override { |
| 51 | return {std::make_unique<GraphicsContext_Android>(m_driver_library)}; | 46 | return {std::make_unique<GraphicsContext_Android>(m_driver_library)}; |
| @@ -55,8 +50,6 @@ public: | |||
| 55 | }; | 50 | }; |
| 56 | 51 | ||
| 57 | private: | 52 | private: |
| 58 | InputCommon::InputSubsystem* m_input_subsystem{}; | ||
| 59 | |||
| 60 | float m_window_width{}; | 53 | float m_window_width{}; |
| 61 | float m_window_height{}; | 54 | float m_window_height{}; |
| 62 | 55 | ||
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..37a65f2b8 --- /dev/null +++ b/src/android/app/src/main/jni/native_input.cpp | |||
| @@ -0,0 +1,629 @@ | |||
| 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().Window().OnTouchPressed(j_id, j_x_axis, j_y_axis); | ||
| 194 | } | ||
| 195 | } | ||
| 196 | |||
| 197 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchMoved(JNIEnv* env, jobject j_obj, | ||
| 198 | jint j_id, jfloat j_x_axis, | ||
| 199 | jfloat j_y_axis) { | ||
| 200 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 201 | EmulationSession::GetInstance().Window().OnTouchMoved(j_id, j_x_axis, j_y_axis); | ||
| 202 | } | ||
| 203 | } | ||
| 204 | |||
| 205 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchReleased(JNIEnv* env, jobject j_obj, | ||
| 206 | jint j_id) { | ||
| 207 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 208 | EmulationSession::GetInstance().Window().OnTouchReleased(j_id); | ||
| 209 | } | ||
| 210 | } | ||
| 211 | |||
| 212 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onOverlayButtonEventImpl( | ||
| 213 | JNIEnv* env, jobject j_obj, jint j_port, jint j_button_id, jint j_action) { | ||
| 214 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 215 | EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetButtonState( | ||
| 216 | j_port, j_button_id, j_action == 1); | ||
| 217 | } | ||
| 218 | } | ||
| 219 | |||
| 220 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onOverlayJoystickEventImpl( | ||
| 221 | JNIEnv* env, jobject j_obj, jint j_port, jint j_stick_id, jfloat j_x_axis, jfloat j_y_axis) { | ||
| 222 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 223 | EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetStickPosition( | ||
| 224 | j_port, j_stick_id, j_x_axis, j_y_axis); | ||
| 225 | } | ||
| 226 | } | ||
| 227 | |||
| 228 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onDeviceMotionEvent( | ||
| 229 | JNIEnv* env, jobject j_obj, jint j_port, jlong j_delta_timestamp, jfloat j_x_gyro, | ||
| 230 | jfloat j_y_gyro, jfloat j_z_gyro, jfloat j_x_accel, jfloat j_y_accel, jfloat j_z_accel) { | ||
| 231 | if (EmulationSession::GetInstance().IsRunning()) { | ||
| 232 | EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetMotionState( | ||
| 233 | j_port, j_delta_timestamp, j_x_gyro, j_y_gyro, j_z_gyro, j_x_accel, j_y_accel, | ||
| 234 | j_z_accel); | ||
| 235 | } | ||
| 236 | } | ||
| 237 | |||
| 238 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_reloadInputDevices(JNIEnv* env, | ||
| 239 | jobject j_obj) { | ||
| 240 | EmulationSession::GetInstance().System().HIDCore().ReloadInputDevices(); | ||
| 241 | } | ||
| 242 | |||
| 243 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_registerController(JNIEnv* env, | ||
| 244 | jobject j_obj, | ||
| 245 | jobject j_device) { | ||
| 246 | EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->RegisterController(j_device); | ||
| 247 | } | ||
| 248 | |||
| 249 | jobjectArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getInputDevices(JNIEnv* env, | ||
| 250 | jobject j_obj) { | ||
| 251 | auto devices = EmulationSession::GetInstance().GetInputSubsystem().GetInputDevices(); | ||
| 252 | jobjectArray jdevices = env->NewObjectArray(devices.size(), Common::Android::GetStringClass(), | ||
| 253 | Common::Android::ToJString(env, "")); | ||
| 254 | for (size_t i = 0; i < devices.size(); ++i) { | ||
| 255 | env->SetObjectArrayElement(jdevices, i, | ||
| 256 | Common::Android::ToJString(env, devices[i].Serialize())); | ||
| 257 | } | ||
| 258 | return jdevices; | ||
| 259 | } | ||
| 260 | |||
| 261 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadInputProfiles(JNIEnv* env, | ||
| 262 | jobject j_obj) { | ||
| 263 | map_profiles.clear(); | ||
| 264 | const auto input_profile_loc = | ||
| 265 | Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "input"; | ||
| 266 | |||
| 267 | if (Common::FS::IsDir(input_profile_loc)) { | ||
| 268 | Common::FS::IterateDirEntries( | ||
| 269 | input_profile_loc, | ||
| 270 | [&](const std::filesystem::path& full_path) { | ||
| 271 | const auto filename = full_path.filename(); | ||
| 272 | const auto name_without_ext = | ||
| 273 | Common::FS::PathToUTF8String(GetNameWithoutExtension(filename)); | ||
| 274 | |||
| 275 | if (filename.extension() == ".ini" && IsProfileNameValid(name_without_ext)) { | ||
| 276 | map_profiles.insert_or_assign( | ||
| 277 | name_without_ext, std::make_unique<AndroidConfig>( | ||
| 278 | name_without_ext, Config::ConfigType::InputProfile)); | ||
| 279 | } | ||
| 280 | |||
| 281 | return true; | ||
| 282 | }, | ||
| 283 | Common::FS::DirEntryFilter::File); | ||
| 284 | } | ||
| 285 | } | ||
| 286 | |||
| 287 | jobjectArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getInputProfileNames( | ||
| 288 | JNIEnv* env, jobject j_obj) { | ||
| 289 | std::vector<std::string> profile_names; | ||
| 290 | profile_names.reserve(map_profiles.size()); | ||
| 291 | |||
| 292 | auto it = map_profiles.cbegin(); | ||
| 293 | while (it != map_profiles.cend()) { | ||
| 294 | const auto& [profile_name, config] = *it; | ||
| 295 | if (!ProfileExistsInFilesystem(profile_name)) { | ||
| 296 | it = map_profiles.erase(it); | ||
| 297 | continue; | ||
| 298 | } | ||
| 299 | |||
| 300 | profile_names.push_back(profile_name); | ||
| 301 | ++it; | ||
| 302 | } | ||
| 303 | |||
| 304 | std::stable_sort(profile_names.begin(), profile_names.end()); | ||
| 305 | |||
| 306 | jobjectArray j_profile_names = | ||
| 307 | env->NewObjectArray(profile_names.size(), Common::Android::GetStringClass(), | ||
| 308 | Common::Android::ToJString(env, "")); | ||
| 309 | for (size_t i = 0; i < profile_names.size(); ++i) { | ||
| 310 | env->SetObjectArrayElement(j_profile_names, i, | ||
| 311 | Common::Android::ToJString(env, profile_names[i])); | ||
| 312 | } | ||
| 313 | |||
| 314 | return j_profile_names; | ||
| 315 | } | ||
| 316 | |||
| 317 | jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isProfileNameValid(JNIEnv* env, | ||
| 318 | jobject j_obj, | ||
| 319 | jstring j_name) { | ||
| 320 | return Common::Android::GetJString(env, j_name).find_first_of("<>:;\"/\\|,.!?*") == | ||
| 321 | std::string::npos; | ||
| 322 | } | ||
| 323 | |||
| 324 | jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_createProfile(JNIEnv* env, | ||
| 325 | jobject j_obj, | ||
| 326 | jstring j_name, | ||
| 327 | jint j_player_index) { | ||
| 328 | auto profile_name = Common::Android::GetJString(env, j_name); | ||
| 329 | if (ProfileExistsInMap(profile_name)) { | ||
| 330 | return false; | ||
| 331 | } | ||
| 332 | |||
| 333 | map_profiles.insert_or_assign( | ||
| 334 | profile_name, | ||
| 335 | std::make_unique<AndroidConfig>(profile_name, Config::ConfigType::InputProfile)); | ||
| 336 | |||
| 337 | return SaveProfile(profile_name, j_player_index); | ||
| 338 | } | ||
| 339 | |||
| 340 | jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_deleteProfile(JNIEnv* env, | ||
| 341 | jobject j_obj, | ||
| 342 | jstring j_name, | ||
| 343 | jint j_player_index) { | ||
| 344 | auto profile_name = Common::Android::GetJString(env, j_name); | ||
| 345 | if (!ProfileExistsInMap(profile_name)) { | ||
| 346 | return false; | ||
| 347 | } | ||
| 348 | |||
| 349 | if (!ProfileExistsInFilesystem(profile_name) || | ||
| 350 | Common::FS::RemoveFile(map_profiles[profile_name]->GetConfigFilePath())) { | ||
| 351 | map_profiles.erase(profile_name); | ||
| 352 | } | ||
| 353 | |||
| 354 | Settings::values.players.GetValue()[j_player_index].profile_name = ""; | ||
| 355 | return !ProfileExistsInMap(profile_name) && !ProfileExistsInFilesystem(profile_name); | ||
| 356 | } | ||
| 357 | |||
| 358 | jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadProfile(JNIEnv* env, jobject j_obj, | ||
| 359 | jstring j_name, | ||
| 360 | jint j_player_index) { | ||
| 361 | auto profile_name = Common::Android::GetJString(env, j_name); | ||
| 362 | return LoadProfile(profile_name, j_player_index); | ||
| 363 | } | ||
| 364 | |||
| 365 | jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_saveProfile(JNIEnv* env, jobject j_obj, | ||
| 366 | jstring j_name, | ||
| 367 | jint j_player_index) { | ||
| 368 | auto profile_name = Common::Android::GetJString(env, j_name); | ||
| 369 | return SaveProfile(profile_name, j_player_index); | ||
| 370 | } | ||
| 371 | |||
| 372 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadPerGameConfiguration( | ||
| 373 | JNIEnv* env, jobject j_obj, jint j_player_index, jint j_selected_index, | ||
| 374 | jstring j_selected_profile_name) { | ||
| 375 | static constexpr size_t HANDHELD_INDEX = 8; | ||
| 376 | |||
| 377 | auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); | ||
| 378 | Settings::values.players.SetGlobal(false); | ||
| 379 | |||
| 380 | auto profile_name = Common::Android::GetJString(env, j_selected_profile_name); | ||
| 381 | auto* emulated_controller = hid_core.GetEmulatedControllerByIndex(j_player_index); | ||
| 382 | |||
| 383 | if (j_selected_index == 0) { | ||
| 384 | Settings::values.players.GetValue()[j_player_index].profile_name = ""; | ||
| 385 | if (j_player_index == 0) { | ||
| 386 | Settings::values.players.GetValue()[HANDHELD_INDEX] = {}; | ||
| 387 | } | ||
| 388 | Settings::values.players.SetGlobal(true); | ||
| 389 | emulated_controller->ReloadFromSettings(); | ||
| 390 | return; | ||
| 391 | } | ||
| 392 | if (profile_name.empty()) { | ||
| 393 | return; | ||
| 394 | } | ||
| 395 | auto& player = Settings::values.players.GetValue()[j_player_index]; | ||
| 396 | auto& global_player = Settings::values.players.GetValue(true)[j_player_index]; | ||
| 397 | player.profile_name = profile_name; | ||
| 398 | global_player.profile_name = profile_name; | ||
| 399 | // Read from the profile into the custom player settings | ||
| 400 | LoadProfile(profile_name, j_player_index); | ||
| 401 | // Make sure the controller is connected | ||
| 402 | player.connected = true; | ||
| 403 | |||
| 404 | emulated_controller->ReloadFromSettings(); | ||
| 405 | |||
| 406 | if (j_player_index > 0) { | ||
| 407 | return; | ||
| 408 | } | ||
| 409 | // Handle Handheld cases | ||
| 410 | auto& handheld_player = Settings::values.players.GetValue()[HANDHELD_INDEX]; | ||
| 411 | auto* handheld_controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld); | ||
| 412 | if (player.controller_type == Settings::ControllerType::Handheld) { | ||
| 413 | handheld_player = player; | ||
| 414 | } else { | ||
| 415 | handheld_player = {}; | ||
| 416 | } | ||
| 417 | handheld_controller->ReloadFromSettings(); | ||
| 418 | } | ||
| 419 | |||
| 420 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_beginMapping(JNIEnv* env, jobject j_obj, | ||
| 421 | jint jtype) { | ||
| 422 | EmulationSession::GetInstance().GetInputSubsystem().BeginMapping( | ||
| 423 | static_cast<InputCommon::Polling::InputType>(jtype)); | ||
| 424 | } | ||
| 425 | |||
| 426 | jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getNextInput(JNIEnv* env, | ||
| 427 | jobject j_obj) { | ||
| 428 | return Common::Android::ToJString( | ||
| 429 | env, EmulationSession::GetInstance().GetInputSubsystem().GetNextInput().Serialize()); | ||
| 430 | } | ||
| 431 | |||
| 432 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_stopMapping(JNIEnv* env, jobject j_obj) { | ||
| 433 | EmulationSession::GetInstance().GetInputSubsystem().StopMapping(); | ||
| 434 | } | ||
| 435 | |||
| 436 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_updateMappingsWithDefaultImpl( | ||
| 437 | JNIEnv* env, jobject j_obj, jint j_player_index, jstring j_device_params, | ||
| 438 | jstring j_display_name) { | ||
| 439 | auto& input_subsystem = EmulationSession::GetInstance().GetInputSubsystem(); | ||
| 440 | |||
| 441 | // Clear all previous mappings | ||
| 442 | for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) { | ||
| 443 | ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { | ||
| 444 | controller->SetButtonParam(button_id, {}); | ||
| 445 | }); | ||
| 446 | } | ||
| 447 | for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) { | ||
| 448 | ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { | ||
| 449 | controller->SetStickParam(analog_id, {}); | ||
| 450 | }); | ||
| 451 | } | ||
| 452 | |||
| 453 | // Apply new mappings | ||
| 454 | auto device = Common::ParamPackage(Common::Android::GetJString(env, j_device_params)); | ||
| 455 | auto button_mappings = input_subsystem.GetButtonMappingForDevice(device); | ||
| 456 | auto analog_mappings = input_subsystem.GetAnalogMappingForDevice(device); | ||
| 457 | auto display_name = Common::Android::GetJString(env, j_display_name); | ||
| 458 | for (const auto& button_mapping : button_mappings) { | ||
| 459 | const std::size_t index = button_mapping.first; | ||
| 460 | auto named_mapping = button_mapping.second; | ||
| 461 | named_mapping.Set("display", display_name); | ||
| 462 | ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { | ||
| 463 | controller->SetButtonParam(index, named_mapping); | ||
| 464 | }); | ||
| 465 | } | ||
| 466 | for (const auto& analog_mapping : analog_mappings) { | ||
| 467 | const std::size_t index = analog_mapping.first; | ||
| 468 | auto named_mapping = analog_mapping.second; | ||
| 469 | named_mapping.Set("display", display_name); | ||
| 470 | ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { | ||
| 471 | controller->SetStickParam(index, named_mapping); | ||
| 472 | }); | ||
| 473 | } | ||
| 474 | } | ||
| 475 | |||
| 476 | jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getButtonParamImpl(JNIEnv* env, | ||
| 477 | jobject j_obj, | ||
| 478 | jint j_player_index, | ||
| 479 | jint j_button) { | ||
| 480 | return Common::Android::ToJString(env, EmulationSession::GetInstance() | ||
| 481 | .System() | ||
| 482 | .HIDCore() | ||
| 483 | .GetEmulatedControllerByIndex(j_player_index) | ||
| 484 | ->GetButtonParam(j_button) | ||
| 485 | .Serialize()); | ||
| 486 | } | ||
| 487 | |||
| 488 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setButtonParamImpl( | ||
| 489 | JNIEnv* env, jobject j_obj, jint j_player_index, jint j_button_id, jstring j_param) { | ||
| 490 | ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { | ||
| 491 | controller->SetButtonParam(j_button_id, | ||
| 492 | Common::ParamPackage(Common::Android::GetJString(env, j_param))); | ||
| 493 | }); | ||
| 494 | } | ||
| 495 | |||
| 496 | jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getStickParamImpl(JNIEnv* env, | ||
| 497 | jobject j_obj, | ||
| 498 | jint j_player_index, | ||
| 499 | jint j_stick) { | ||
| 500 | return Common::Android::ToJString(env, EmulationSession::GetInstance() | ||
| 501 | .System() | ||
| 502 | .HIDCore() | ||
| 503 | .GetEmulatedControllerByIndex(j_player_index) | ||
| 504 | ->GetStickParam(j_stick) | ||
| 505 | .Serialize()); | ||
| 506 | } | ||
| 507 | |||
| 508 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setStickParamImpl( | ||
| 509 | JNIEnv* env, jobject j_obj, jint j_player_index, jint j_stick_id, jstring j_param) { | ||
| 510 | ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { | ||
| 511 | controller->SetStickParam(j_stick_id, | ||
| 512 | Common::ParamPackage(Common::Android::GetJString(env, j_param))); | ||
| 513 | }); | ||
| 514 | } | ||
| 515 | |||
| 516 | jint Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getButtonNameImpl(JNIEnv* env, | ||
| 517 | jobject j_obj, | ||
| 518 | jstring j_param) { | ||
| 519 | return static_cast<jint>(EmulationSession::GetInstance().GetInputSubsystem().GetButtonName( | ||
| 520 | Common::ParamPackage(Common::Android::GetJString(env, j_param)))); | ||
| 521 | } | ||
| 522 | |||
| 523 | jintArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getSupportedStyleTagsImpl( | ||
| 524 | JNIEnv* env, jobject j_obj, jint j_player_index) { | ||
| 525 | auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); | ||
| 526 | const auto npad_style_set = hid_core.GetSupportedStyleTag(); | ||
| 527 | std::vector<s32> supported_indexes; | ||
| 528 | if (npad_style_set.fullkey == 1) { | ||
| 529 | supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::Fullkey)); | ||
| 530 | } | ||
| 531 | |||
| 532 | if (npad_style_set.joycon_dual == 1) { | ||
| 533 | supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconDual)); | ||
| 534 | } | ||
| 535 | |||
| 536 | if (npad_style_set.joycon_left == 1) { | ||
| 537 | supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconLeft)); | ||
| 538 | } | ||
| 539 | |||
| 540 | if (npad_style_set.joycon_right == 1) { | ||
| 541 | supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconRight)); | ||
| 542 | } | ||
| 543 | |||
| 544 | if (j_player_index == 0 && npad_style_set.handheld == 1) { | ||
| 545 | supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::Handheld)); | ||
| 546 | } | ||
| 547 | |||
| 548 | if (npad_style_set.gamecube == 1) { | ||
| 549 | supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::GameCube)); | ||
| 550 | } | ||
| 551 | |||
| 552 | jintArray j_supported_indexes = env->NewIntArray(supported_indexes.size()); | ||
| 553 | env->SetIntArrayRegion(j_supported_indexes, 0, supported_indexes.size(), | ||
| 554 | supported_indexes.data()); | ||
| 555 | return j_supported_indexes; | ||
| 556 | } | ||
| 557 | |||
| 558 | jint Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getStyleIndexImpl(JNIEnv* env, | ||
| 559 | jobject j_obj, | ||
| 560 | jint j_player_index) { | ||
| 561 | return static_cast<s32>(EmulationSession::GetInstance() | ||
| 562 | .System() | ||
| 563 | .HIDCore() | ||
| 564 | .GetEmulatedControllerByIndex(j_player_index) | ||
| 565 | ->GetNpadStyleIndex(true)); | ||
| 566 | } | ||
| 567 | |||
| 568 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setStyleIndexImpl(JNIEnv* env, | ||
| 569 | jobject j_obj, | ||
| 570 | jint j_player_index, | ||
| 571 | jint j_style_index) { | ||
| 572 | auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); | ||
| 573 | auto type = static_cast<Core::HID::NpadStyleIndex>(j_style_index); | ||
| 574 | ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { | ||
| 575 | controller->SetNpadStyleIndex(type); | ||
| 576 | }); | ||
| 577 | if (j_player_index == 0) { | ||
| 578 | auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld); | ||
| 579 | auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1); | ||
| 580 | ConnectController(j_player_index, | ||
| 581 | player_one->IsConnected(true) || handheld->IsConnected(true)); | ||
| 582 | } | ||
| 583 | } | ||
| 584 | |||
| 585 | jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isControllerImpl(JNIEnv* env, | ||
| 586 | jobject j_obj, | ||
| 587 | jstring jparams) { | ||
| 588 | return static_cast<jint>(EmulationSession::GetInstance().GetInputSubsystem().IsController( | ||
| 589 | Common::ParamPackage(Common::Android::GetJString(env, jparams)))); | ||
| 590 | } | ||
| 591 | |||
| 592 | jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getIsConnected(JNIEnv* env, | ||
| 593 | jobject j_obj, | ||
| 594 | jint j_player_index) { | ||
| 595 | auto& hid_core = EmulationSession::GetInstance().System().HIDCore(); | ||
| 596 | auto* controller = hid_core.GetEmulatedControllerByIndex(static_cast<size_t>(j_player_index)); | ||
| 597 | if (j_player_index == 0 && | ||
| 598 | controller->GetNpadStyleIndex(true) == Core::HID::NpadStyleIndex::Handheld) { | ||
| 599 | return hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld)->IsConnected(true); | ||
| 600 | } | ||
| 601 | return controller->IsConnected(true); | ||
| 602 | } | ||
| 603 | |||
| 604 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_connectControllersImpl( | ||
| 605 | JNIEnv* env, jobject j_obj, jbooleanArray j_connected) { | ||
| 606 | jboolean isCopy = false; | ||
| 607 | auto j_connected_array_size = env->GetArrayLength(j_connected); | ||
| 608 | jboolean* j_connected_array = env->GetBooleanArrayElements(j_connected, &isCopy); | ||
| 609 | for (int i = 0; i < j_connected_array_size; ++i) { | ||
| 610 | ConnectController(i, j_connected_array[i]); | ||
| 611 | } | ||
| 612 | } | ||
| 613 | |||
| 614 | void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_resetControllerMappings( | ||
| 615 | JNIEnv* env, jobject j_obj, jint j_player_index) { | ||
| 616 | // Clear all previous mappings | ||
| 617 | for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) { | ||
| 618 | ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { | ||
| 619 | controller->SetButtonParam(button_id, {}); | ||
| 620 | }); | ||
| 621 | } | ||
| 622 | for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) { | ||
| 623 | ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) { | ||
| 624 | controller->SetStickParam(analog_id, {}); | ||
| 625 | }); | ||
| 626 | } | ||
| 627 | } | ||
| 628 | |||
| 629 | } // 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/card_driver_option.xml b/src/android/app/src/main/res/layout/card_driver_option.xml index bda524f0f..09e26990b 100644 --- a/src/android/app/src/main/res/layout/card_driver_option.xml +++ b/src/android/app/src/main/res/layout/card_driver_option.xml | |||
| @@ -39,10 +39,7 @@ | |||
| 39 | style="@style/TextAppearance.Material3.TitleMedium" | 39 | style="@style/TextAppearance.Material3.TitleMedium" |
| 40 | android:layout_width="match_parent" | 40 | android:layout_width="match_parent" |
| 41 | android:layout_height="wrap_content" | 41 | android:layout_height="wrap_content" |
| 42 | android:ellipsize="none" | ||
| 43 | android:marqueeRepeatLimit="marquee_forever" | ||
| 44 | android:requiresFadingEdge="horizontal" | 42 | android:requiresFadingEdge="horizontal" |
| 45 | android:singleLine="true" | ||
| 46 | android:textAlignment="viewStart" | 43 | android:textAlignment="viewStart" |
| 47 | tools:text="@string/select_gpu_driver_default" /> | 44 | tools:text="@string/select_gpu_driver_default" /> |
| 48 | 45 | ||
| @@ -52,10 +49,7 @@ | |||
| 52 | android:layout_width="match_parent" | 49 | android:layout_width="match_parent" |
| 53 | android:layout_height="wrap_content" | 50 | android:layout_height="wrap_content" |
| 54 | android:layout_marginTop="6dp" | 51 | android:layout_marginTop="6dp" |
| 55 | android:ellipsize="none" | ||
| 56 | android:marqueeRepeatLimit="marquee_forever" | ||
| 57 | android:requiresFadingEdge="horizontal" | 52 | android:requiresFadingEdge="horizontal" |
| 58 | android:singleLine="true" | ||
| 59 | android:textAlignment="viewStart" | 53 | android:textAlignment="viewStart" |
| 60 | tools:text="@string/install_gpu_driver_description" /> | 54 | tools:text="@string/install_gpu_driver_description" /> |
| 61 | 55 | ||
| @@ -65,10 +59,7 @@ | |||
| 65 | android:layout_width="match_parent" | 59 | android:layout_width="match_parent" |
| 66 | android:layout_height="wrap_content" | 60 | android:layout_height="wrap_content" |
| 67 | android:layout_marginTop="6dp" | 61 | android:layout_marginTop="6dp" |
| 68 | android:ellipsize="none" | ||
| 69 | android:marqueeRepeatLimit="marquee_forever" | ||
| 70 | android:requiresFadingEdge="horizontal" | 62 | android:requiresFadingEdge="horizontal" |
| 71 | android:singleLine="true" | ||
| 72 | android:textAlignment="viewStart" | 63 | android:textAlignment="viewStart" |
| 73 | tools:text="@string/install_gpu_driver_description" /> | 64 | tools:text="@string/install_gpu_driver_description" /> |
| 74 | 65 | ||
diff --git a/src/android/app/src/main/res/layout/card_folder.xml b/src/android/app/src/main/res/layout/card_folder.xml index ed4a7ca8f..e3a5f1a86 100644 --- a/src/android/app/src/main/res/layout/card_folder.xml +++ b/src/android/app/src/main/res/layout/card_folder.xml | |||
| @@ -21,10 +21,7 @@ | |||
| 21 | android:layout_width="0dp" | 21 | android:layout_width="0dp" |
| 22 | android:layout_height="wrap_content" | 22 | android:layout_height="wrap_content" |
| 23 | android:layout_gravity="center_vertical|start" | 23 | android:layout_gravity="center_vertical|start" |
| 24 | android:ellipsize="none" | ||
| 25 | android:marqueeRepeatLimit="marquee_forever" | ||
| 26 | android:requiresFadingEdge="horizontal" | 24 | android:requiresFadingEdge="horizontal" |
| 27 | android:singleLine="true" | ||
| 28 | android:textAlignment="viewStart" | 25 | android:textAlignment="viewStart" |
| 29 | app:layout_constraintBottom_toBottomOf="parent" | 26 | app:layout_constraintBottom_toBottomOf="parent" |
| 30 | app:layout_constraintEnd_toStartOf="@+id/button_layout" | 27 | app:layout_constraintEnd_toStartOf="@+id/button_layout" |
diff --git a/src/android/app/src/main/res/layout/card_game.xml b/src/android/app/src/main/res/layout/card_game.xml index 6340171ec..411b50315 100644 --- a/src/android/app/src/main/res/layout/card_game.xml +++ b/src/android/app/src/main/res/layout/card_game.xml | |||
| @@ -40,10 +40,7 @@ | |||
| 40 | android:layout_width="0dp" | 40 | android:layout_width="0dp" |
| 41 | android:layout_height="wrap_content" | 41 | android:layout_height="wrap_content" |
| 42 | android:layout_marginTop="8dp" | 42 | android:layout_marginTop="8dp" |
| 43 | android:ellipsize="none" | ||
| 44 | android:marqueeRepeatLimit="marquee_forever" | ||
| 45 | android:requiresFadingEdge="horizontal" | 43 | android:requiresFadingEdge="horizontal" |
| 46 | android:singleLine="true" | ||
| 47 | android:textAlignment="center" | 44 | android:textAlignment="center" |
| 48 | android:textSize="14sp" | 45 | android:textSize="14sp" |
| 49 | app:layout_constraintEnd_toEndOf="@+id/image_game_screen" | 46 | app:layout_constraintEnd_toEndOf="@+id/image_game_screen" |
diff --git a/src/android/app/src/main/res/layout/card_simple_outlined.xml b/src/android/app/src/main/res/layout/card_simple_outlined.xml index b73930e7e..e29df6a2d 100644 --- a/src/android/app/src/main/res/layout/card_simple_outlined.xml +++ b/src/android/app/src/main/res/layout/card_simple_outlined.xml | |||
| @@ -59,9 +59,6 @@ | |||
| 59 | android:textAlignment="viewStart" | 59 | android:textAlignment="viewStart" |
| 60 | android:textSize="14sp" | 60 | android:textSize="14sp" |
| 61 | android:textStyle="bold" | 61 | android:textStyle="bold" |
| 62 | android:singleLine="true" | ||
| 63 | android:marqueeRepeatLimit="marquee_forever" | ||
| 64 | android:ellipsize="none" | ||
| 65 | android:requiresFadingEdge="horizontal" | 62 | android:requiresFadingEdge="horizontal" |
| 66 | android:layout_marginTop="6dp" | 63 | android:layout_marginTop="6dp" |
| 67 | android:visibility="gone" | 64 | android:visibility="gone" |
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/fragment_game_properties.xml b/src/android/app/src/main/res/layout/fragment_game_properties.xml index 436ebd79d..5e3f3cf28 100644 --- a/src/android/app/src/main/res/layout/fragment_game_properties.xml +++ b/src/android/app/src/main/res/layout/fragment_game_properties.xml | |||
| @@ -76,10 +76,7 @@ | |||
| 76 | android:layout_marginTop="12dp" | 76 | android:layout_marginTop="12dp" |
| 77 | android:layout_marginBottom="12dp" | 77 | android:layout_marginBottom="12dp" |
| 78 | android:layout_marginHorizontal="16dp" | 78 | android:layout_marginHorizontal="16dp" |
| 79 | android:ellipsize="none" | ||
| 80 | android:marqueeRepeatLimit="marquee_forever" | ||
| 81 | android:requiresFadingEdge="horizontal" | 79 | android:requiresFadingEdge="horizontal" |
| 82 | android:singleLine="true" | ||
| 83 | android:textAlignment="center" | 80 | android:textAlignment="center" |
| 84 | tools:text="deko_basic" /> | 81 | tools:text="deko_basic" /> |
| 85 | 82 | ||
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/core/core.cpp b/src/core/core.cpp index 435ef6793..bd5f11d53 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp | |||
| @@ -242,7 +242,7 @@ struct System::Impl { | |||
| 242 | void Run() { | 242 | void Run() { |
| 243 | std::unique_lock<std::mutex> lk(suspend_guard); | 243 | std::unique_lock<std::mutex> lk(suspend_guard); |
| 244 | 244 | ||
| 245 | kernel.SuspendApplication(false); | 245 | kernel.SuspendEmulation(false); |
| 246 | core_timing.SyncPause(false); | 246 | core_timing.SyncPause(false); |
| 247 | is_paused.store(false, std::memory_order_relaxed); | 247 | is_paused.store(false, std::memory_order_relaxed); |
| 248 | } | 248 | } |
| @@ -251,7 +251,7 @@ struct System::Impl { | |||
| 251 | std::unique_lock<std::mutex> lk(suspend_guard); | 251 | std::unique_lock<std::mutex> lk(suspend_guard); |
| 252 | 252 | ||
| 253 | core_timing.SyncPause(true); | 253 | core_timing.SyncPause(true); |
| 254 | kernel.SuspendApplication(true); | 254 | kernel.SuspendEmulation(true); |
| 255 | is_paused.store(true, std::memory_order_relaxed); | 255 | is_paused.store(true, std::memory_order_relaxed); |
| 256 | } | 256 | } |
| 257 | 257 | ||
| @@ -261,7 +261,7 @@ struct System::Impl { | |||
| 261 | 261 | ||
| 262 | std::unique_lock<std::mutex> StallApplication() { | 262 | std::unique_lock<std::mutex> StallApplication() { |
| 263 | std::unique_lock<std::mutex> lk(suspend_guard); | 263 | std::unique_lock<std::mutex> lk(suspend_guard); |
| 264 | kernel.SuspendApplication(true); | 264 | kernel.SuspendEmulation(true); |
| 265 | core_timing.SyncPause(true); | 265 | core_timing.SyncPause(true); |
| 266 | return lk; | 266 | return lk; |
| 267 | } | 267 | } |
| @@ -269,7 +269,7 @@ struct System::Impl { | |||
| 269 | void UnstallApplication() { | 269 | void UnstallApplication() { |
| 270 | if (!IsPaused()) { | 270 | if (!IsPaused()) { |
| 271 | core_timing.SyncPause(false); | 271 | core_timing.SyncPause(false); |
| 272 | kernel.SuspendApplication(false); | 272 | kernel.SuspendEmulation(false); |
| 273 | } | 273 | } |
| 274 | } | 274 | } |
| 275 | 275 | ||
| @@ -459,7 +459,7 @@ struct System::Impl { | |||
| 459 | } | 459 | } |
| 460 | 460 | ||
| 461 | Network::CancelPendingSocketOperations(); | 461 | Network::CancelPendingSocketOperations(); |
| 462 | kernel.SuspendApplication(true); | 462 | kernel.SuspendEmulation(true); |
| 463 | if (services) { | 463 | if (services) { |
| 464 | services->KillNVNFlinger(); | 464 | services->KillNVNFlinger(); |
| 465 | } | 465 | } |
diff --git a/src/core/file_sys/control_metadata.h b/src/core/file_sys/control_metadata.h index 555b9d8f7..667efbbab 100644 --- a/src/core/file_sys/control_metadata.h +++ b/src/core/file_sys/control_metadata.h | |||
| @@ -64,8 +64,8 @@ struct RawNACP { | |||
| 64 | u64_le cache_storage_size; | 64 | u64_le cache_storage_size; |
| 65 | u64_le cache_storage_journal_size; | 65 | u64_le cache_storage_journal_size; |
| 66 | u64_le cache_storage_data_and_journal_max_size; | 66 | u64_le cache_storage_data_and_journal_max_size; |
| 67 | u64_le cache_storage_max_index; | 67 | u16_le cache_storage_max_index; |
| 68 | INSERT_PADDING_BYTES(0xE70); | 68 | INSERT_PADDING_BYTES(0xE76); |
| 69 | }; | 69 | }; |
| 70 | static_assert(sizeof(RawNACP) == 0x4000, "RawNACP has incorrect size."); | 70 | static_assert(sizeof(RawNACP) == 0x4000, "RawNACP has incorrect size."); |
| 71 | 71 | ||
diff --git a/src/core/hle/kernel/k_thread.h b/src/core/hle/kernel/k_thread.h index f13e232b2..e928cfebc 100644 --- a/src/core/hle/kernel/k_thread.h +++ b/src/core/hle/kernel/k_thread.h | |||
| @@ -66,6 +66,7 @@ enum class SuspendType : u32 { | |||
| 66 | Debug = 2, | 66 | Debug = 2, |
| 67 | Backtrace = 3, | 67 | Backtrace = 3, |
| 68 | Init = 4, | 68 | Init = 4, |
| 69 | System = 5, | ||
| 69 | 70 | ||
| 70 | Count, | 71 | Count, |
| 71 | }; | 72 | }; |
| @@ -84,8 +85,9 @@ enum class ThreadState : u16 { | |||
| 84 | DebugSuspended = (1 << (2 + SuspendShift)), | 85 | DebugSuspended = (1 << (2 + SuspendShift)), |
| 85 | BacktraceSuspended = (1 << (3 + SuspendShift)), | 86 | BacktraceSuspended = (1 << (3 + SuspendShift)), |
| 86 | InitSuspended = (1 << (4 + SuspendShift)), | 87 | InitSuspended = (1 << (4 + SuspendShift)), |
| 88 | SystemSuspended = (1 << (5 + SuspendShift)), | ||
| 87 | 89 | ||
| 88 | SuspendFlagMask = ((1 << 5) - 1) << SuspendShift, | 90 | SuspendFlagMask = ((1 << 6) - 1) << SuspendShift, |
| 89 | }; | 91 | }; |
| 90 | DECLARE_ENUM_FLAG_OPERATORS(ThreadState); | 92 | DECLARE_ENUM_FLAG_OPERATORS(ThreadState); |
| 91 | 93 | ||
diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp index 34b25be66..4f4b02fac 100644 --- a/src/core/hle/kernel/kernel.cpp +++ b/src/core/hle/kernel/kernel.cpp | |||
| @@ -1204,39 +1204,48 @@ const Kernel::KSharedMemory& KernelCore::GetHidBusSharedMem() const { | |||
| 1204 | return *impl->hidbus_shared_mem; | 1204 | return *impl->hidbus_shared_mem; |
| 1205 | } | 1205 | } |
| 1206 | 1206 | ||
| 1207 | void KernelCore::SuspendApplication(bool suspended) { | 1207 | void KernelCore::SuspendEmulation(bool suspended) { |
| 1208 | const bool should_suspend{exception_exited || suspended}; | 1208 | const bool should_suspend{exception_exited || suspended}; |
| 1209 | const auto activity = | 1209 | auto processes = GetProcessList(); |
| 1210 | should_suspend ? Svc::ProcessActivity::Paused : Svc::ProcessActivity::Runnable; | ||
| 1211 | 1210 | ||
| 1212 | // Get the application process. | 1211 | for (auto& process : processes) { |
| 1213 | KScopedAutoObject<KProcess> process = ApplicationProcess(); | 1212 | KScopedLightLock ll{process->GetListLock()}; |
| 1214 | if (process.IsNull()) { | 1213 | |
| 1215 | return; | 1214 | for (auto& thread : process->GetThreadList()) { |
| 1215 | if (should_suspend) { | ||
| 1216 | thread.RequestSuspend(SuspendType::System); | ||
| 1217 | } else { | ||
| 1218 | thread.Resume(SuspendType::System); | ||
| 1219 | } | ||
| 1220 | } | ||
| 1216 | } | 1221 | } |
| 1217 | 1222 | ||
| 1218 | // Set the new activity. | 1223 | if (!should_suspend) { |
| 1219 | process->SetActivity(activity); | 1224 | return; |
| 1225 | } | ||
| 1220 | 1226 | ||
| 1221 | // Wait for process execution to stop. | 1227 | // Wait for process execution to stop. |
| 1222 | bool must_wait{should_suspend}; | 1228 | // KernelCore::SuspendEmulation must be called from locked context, |
| 1223 | 1229 | // or we could race another call, interfering with waiting. | |
| 1224 | // KernelCore::SuspendApplication must be called from locked context, | 1230 | const auto TryWait = [&]() { |
| 1225 | // or we could race another call to SetActivity, interfering with waiting. | ||
| 1226 | while (must_wait) { | ||
| 1227 | KScopedSchedulerLock sl{*this}; | 1231 | KScopedSchedulerLock sl{*this}; |
| 1228 | 1232 | ||
| 1229 | // Assume that all threads have finished running. | 1233 | for (auto& process : processes) { |
| 1230 | must_wait = false; | 1234 | for (auto i = 0; i < static_cast<s32>(Core::Hardware::NUM_CPU_CORES); ++i) { |
| 1231 | 1235 | if (Scheduler(i).GetSchedulerCurrentThread()->GetOwnerProcess() == | |
| 1232 | for (auto i = 0; i < static_cast<s32>(Core::Hardware::NUM_CPU_CORES); ++i) { | 1236 | process.GetPointerUnsafe()) { |
| 1233 | if (Scheduler(i).GetSchedulerCurrentThread()->GetOwnerProcess() == | 1237 | // A thread has not finished running yet. |
| 1234 | process.GetPointerUnsafe()) { | 1238 | // Continue waiting. |
| 1235 | // A thread has not finished running yet. | 1239 | return false; |
| 1236 | // Continue waiting. | 1240 | } |
| 1237 | must_wait = true; | ||
| 1238 | } | 1241 | } |
| 1239 | } | 1242 | } |
| 1243 | |||
| 1244 | return true; | ||
| 1245 | }; | ||
| 1246 | |||
| 1247 | while (!TryWait()) { | ||
| 1248 | // ... | ||
| 1240 | } | 1249 | } |
| 1241 | } | 1250 | } |
| 1242 | 1251 | ||
| @@ -1260,7 +1269,7 @@ bool KernelCore::IsShuttingDown() const { | |||
| 1260 | 1269 | ||
| 1261 | void KernelCore::ExceptionalExitApplication() { | 1270 | void KernelCore::ExceptionalExitApplication() { |
| 1262 | exception_exited = true; | 1271 | exception_exited = true; |
| 1263 | SuspendApplication(true); | 1272 | SuspendEmulation(true); |
| 1264 | } | 1273 | } |
| 1265 | 1274 | ||
| 1266 | void KernelCore::EnterSVCProfile() { | 1275 | void KernelCore::EnterSVCProfile() { |
diff --git a/src/core/hle/kernel/kernel.h b/src/core/hle/kernel/kernel.h index 8ea5bed1c..57182c0c8 100644 --- a/src/core/hle/kernel/kernel.h +++ b/src/core/hle/kernel/kernel.h | |||
| @@ -258,8 +258,8 @@ public: | |||
| 258 | /// Gets the shared memory object for HIDBus services. | 258 | /// Gets the shared memory object for HIDBus services. |
| 259 | const Kernel::KSharedMemory& GetHidBusSharedMem() const; | 259 | const Kernel::KSharedMemory& GetHidBusSharedMem() const; |
| 260 | 260 | ||
| 261 | /// Suspend/unsuspend application process. | 261 | /// Suspend/unsuspend emulated processes. |
| 262 | void SuspendApplication(bool suspend); | 262 | void SuspendEmulation(bool suspend); |
| 263 | 263 | ||
| 264 | /// Exceptional exit application process. | 264 | /// Exceptional exit application process. |
| 265 | void ExceptionalExitApplication(); | 265 | void ExceptionalExitApplication(); |
diff --git a/src/core/hle/service/am/service/application_functions.cpp b/src/core/hle/service/am/service/application_functions.cpp index b788fddd4..63dd12a47 100644 --- a/src/core/hle/service/am/service/application_functions.cpp +++ b/src/core/hle/service/am/service/application_functions.cpp | |||
| @@ -15,6 +15,7 @@ | |||
| 15 | #include "core/hle/service/cmif_serialization.h" | 15 | #include "core/hle/service/cmif_serialization.h" |
| 16 | #include "core/hle/service/filesystem/filesystem.h" | 16 | #include "core/hle/service/filesystem/filesystem.h" |
| 17 | #include "core/hle/service/filesystem/save_data_controller.h" | 17 | #include "core/hle/service/filesystem/save_data_controller.h" |
| 18 | #include "core/hle/service/glue/glue_manager.h" | ||
| 18 | #include "core/hle/service/ns/ns.h" | 19 | #include "core/hle/service/ns/ns.h" |
| 19 | #include "core/hle/service/sm/sm.h" | 20 | #include "core/hle/service/sm/sm.h" |
| 20 | 21 | ||
| @@ -40,7 +41,7 @@ IApplicationFunctions::IApplicationFunctions(Core::System& system_, std::shared_ | |||
| 40 | {26, D<&IApplicationFunctions::GetSaveDataSize>, "GetSaveDataSize"}, | 41 | {26, D<&IApplicationFunctions::GetSaveDataSize>, "GetSaveDataSize"}, |
| 41 | {27, D<&IApplicationFunctions::CreateCacheStorage>, "CreateCacheStorage"}, | 42 | {27, D<&IApplicationFunctions::CreateCacheStorage>, "CreateCacheStorage"}, |
| 42 | {28, D<&IApplicationFunctions::GetSaveDataSizeMax>, "GetSaveDataSizeMax"}, | 43 | {28, D<&IApplicationFunctions::GetSaveDataSizeMax>, "GetSaveDataSizeMax"}, |
| 43 | {29, nullptr, "GetCacheStorageMax"}, | 44 | {29, D<&IApplicationFunctions::GetCacheStorageMax>, "GetCacheStorageMax"}, |
| 44 | {30, D<&IApplicationFunctions::BeginBlockingHomeButtonShortAndLongPressed>, "BeginBlockingHomeButtonShortAndLongPressed"}, | 45 | {30, D<&IApplicationFunctions::BeginBlockingHomeButtonShortAndLongPressed>, "BeginBlockingHomeButtonShortAndLongPressed"}, |
| 45 | {31, D<&IApplicationFunctions::EndBlockingHomeButtonShortAndLongPressed>, "EndBlockingHomeButtonShortAndLongPressed"}, | 46 | {31, D<&IApplicationFunctions::EndBlockingHomeButtonShortAndLongPressed>, "EndBlockingHomeButtonShortAndLongPressed"}, |
| 46 | {32, D<&IApplicationFunctions::BeginBlockingHomeButton>, "BeginBlockingHomeButton"}, | 47 | {32, D<&IApplicationFunctions::BeginBlockingHomeButton>, "BeginBlockingHomeButton"}, |
| @@ -267,6 +268,22 @@ Result IApplicationFunctions::GetSaveDataSizeMax(Out<u64> out_max_normal_size, | |||
| 267 | R_SUCCEED(); | 268 | R_SUCCEED(); |
| 268 | } | 269 | } |
| 269 | 270 | ||
| 271 | Result IApplicationFunctions::GetCacheStorageMax(Out<u32> out_cache_storage_index_max, | ||
| 272 | Out<u64> out_max_journal_size) { | ||
| 273 | LOG_DEBUG(Service_AM, "called"); | ||
| 274 | |||
| 275 | std::vector<u8> nacp; | ||
| 276 | R_TRY(system.GetARPManager().GetControlProperty(&nacp, m_applet->program_id)); | ||
| 277 | |||
| 278 | auto raw_nacp = std::make_unique<FileSys::RawNACP>(); | ||
| 279 | std::memcpy(raw_nacp.get(), nacp.data(), std::min(sizeof(*raw_nacp), nacp.size())); | ||
| 280 | |||
| 281 | *out_cache_storage_index_max = static_cast<u32>(raw_nacp->cache_storage_max_index); | ||
| 282 | *out_max_journal_size = static_cast<u64>(raw_nacp->cache_storage_data_and_journal_max_size); | ||
| 283 | |||
| 284 | R_SUCCEED(); | ||
| 285 | } | ||
| 286 | |||
| 270 | Result IApplicationFunctions::BeginBlockingHomeButtonShortAndLongPressed(s64 unused) { | 287 | Result IApplicationFunctions::BeginBlockingHomeButtonShortAndLongPressed(s64 unused) { |
| 271 | LOG_WARNING(Service_AM, "(STUBBED) called"); | 288 | LOG_WARNING(Service_AM, "(STUBBED) called"); |
| 272 | 289 | ||
diff --git a/src/core/hle/service/am/service/application_functions.h b/src/core/hle/service/am/service/application_functions.h index 3548202f8..10025a152 100644 --- a/src/core/hle/service/am/service/application_functions.h +++ b/src/core/hle/service/am/service/application_functions.h | |||
| @@ -40,6 +40,7 @@ private: | |||
| 40 | Result CreateCacheStorage(Out<u32> out_target_media, Out<u64> out_required_size, u16 index, | 40 | Result CreateCacheStorage(Out<u32> out_target_media, Out<u64> out_required_size, u16 index, |
| 41 | u64 normal_size, u64 journal_size); | 41 | u64 normal_size, u64 journal_size); |
| 42 | Result GetSaveDataSizeMax(Out<u64> out_max_normal_size, Out<u64> out_max_journal_size); | 42 | Result GetSaveDataSizeMax(Out<u64> out_max_normal_size, Out<u64> out_max_journal_size); |
| 43 | Result GetCacheStorageMax(Out<u32> out_cache_storage_index_max, Out<u64> out_max_journal_size); | ||
| 43 | Result BeginBlockingHomeButtonShortAndLongPressed(s64 unused); | 44 | Result BeginBlockingHomeButtonShortAndLongPressed(s64 unused); |
| 44 | Result EndBlockingHomeButtonShortAndLongPressed(); | 45 | Result EndBlockingHomeButtonShortAndLongPressed(); |
| 45 | Result BeginBlockingHomeButton(s64 timeout_ns); | 46 | Result BeginBlockingHomeButton(s64 timeout_ns); |
diff --git a/src/core/hle/service/filesystem/fsp/fsp_srv.cpp b/src/core/hle/service/filesystem/fsp/fsp_srv.cpp index 63c2d3a58..2d49f30c8 100644 --- a/src/core/hle/service/filesystem/fsp/fsp_srv.cpp +++ b/src/core/hle/service/filesystem/fsp/fsp_srv.cpp | |||
| @@ -336,7 +336,7 @@ FSP_SRV::FSP_SRV(Core::System& system_) | |||
| 336 | {1012, nullptr, "GetFsStackUsage"}, | 336 | {1012, nullptr, "GetFsStackUsage"}, |
| 337 | {1013, nullptr, "UnsetSaveDataRootPath"}, | 337 | {1013, nullptr, "UnsetSaveDataRootPath"}, |
| 338 | {1014, nullptr, "OutputMultiProgramTagAccessLog"}, | 338 | {1014, nullptr, "OutputMultiProgramTagAccessLog"}, |
| 339 | {1016, nullptr, "FlushAccessLogOnSdCard"}, | 339 | {1016, &FSP_SRV::FlushAccessLogOnSdCard, "FlushAccessLogOnSdCard"}, |
| 340 | {1017, nullptr, "OutputApplicationInfoAccessLog"}, | 340 | {1017, nullptr, "OutputApplicationInfoAccessLog"}, |
| 341 | {1018, nullptr, "SetDebugOption"}, | 341 | {1018, nullptr, "SetDebugOption"}, |
| 342 | {1019, nullptr, "UnsetDebugOption"}, | 342 | {1019, nullptr, "UnsetDebugOption"}, |
| @@ -706,6 +706,13 @@ void FSP_SRV::GetProgramIndexForAccessLog(HLERequestContext& ctx) { | |||
| 706 | rb.Push(access_log_program_index); | 706 | rb.Push(access_log_program_index); |
| 707 | } | 707 | } |
| 708 | 708 | ||
| 709 | void FSP_SRV::FlushAccessLogOnSdCard(HLERequestContext& ctx) { | ||
| 710 | LOG_DEBUG(Service_FS, "(STUBBED) called"); | ||
| 711 | |||
| 712 | IPC::ResponseBuilder rb{ctx, 2}; | ||
| 713 | rb.Push(ResultSuccess); | ||
| 714 | } | ||
| 715 | |||
| 709 | void FSP_SRV::GetCacheStorageSize(HLERequestContext& ctx) { | 716 | void FSP_SRV::GetCacheStorageSize(HLERequestContext& ctx) { |
| 710 | IPC::RequestParser rp{ctx}; | 717 | IPC::RequestParser rp{ctx}; |
| 711 | const auto index{rp.Pop<s32>()}; | 718 | const auto index{rp.Pop<s32>()}; |
diff --git a/src/core/hle/service/filesystem/fsp/fsp_srv.h b/src/core/hle/service/filesystem/fsp/fsp_srv.h index 26980af99..59406e6f9 100644 --- a/src/core/hle/service/filesystem/fsp/fsp_srv.h +++ b/src/core/hle/service/filesystem/fsp/fsp_srv.h | |||
| @@ -58,6 +58,7 @@ private: | |||
| 58 | void SetGlobalAccessLogMode(HLERequestContext& ctx); | 58 | void SetGlobalAccessLogMode(HLERequestContext& ctx); |
| 59 | void GetGlobalAccessLogMode(HLERequestContext& ctx); | 59 | void GetGlobalAccessLogMode(HLERequestContext& ctx); |
| 60 | void OutputAccessLogToSdCard(HLERequestContext& ctx); | 60 | void OutputAccessLogToSdCard(HLERequestContext& ctx); |
| 61 | void FlushAccessLogOnSdCard(HLERequestContext& ctx); | ||
| 61 | void GetProgramIndexForAccessLog(HLERequestContext& ctx); | 62 | void GetProgramIndexForAccessLog(HLERequestContext& ctx); |
| 62 | void OpenMultiCommitManager(HLERequestContext& ctx); | 63 | void OpenMultiCommitManager(HLERequestContext& ctx); |
| 63 | void GetCacheStorageSize(HLERequestContext& ctx); | 64 | void GetCacheStorageSize(HLERequestContext& ctx); |
diff --git a/src/core/hle/service/glue/time/manager.cpp b/src/core/hle/service/glue/time/manager.cpp index cad755fa7..059ac3fc9 100644 --- a/src/core/hle/service/glue/time/manager.cpp +++ b/src/core/hle/service/glue/time/manager.cpp | |||
| @@ -186,6 +186,10 @@ TimeManager::TimeManager(Core::System& system) | |||
| 186 | } | 186 | } |
| 187 | } | 187 | } |
| 188 | 188 | ||
| 189 | TimeManager::~TimeManager() { | ||
| 190 | ResetTimeZoneBinary(); | ||
| 191 | } | ||
| 192 | |||
| 189 | Result TimeManager::SetupStandardSteadyClockCore() { | 193 | Result TimeManager::SetupStandardSteadyClockCore() { |
| 190 | Common::UUID external_clock_source_id{}; | 194 | Common::UUID external_clock_source_id{}; |
| 191 | auto res = m_set_sys->GetExternalSteadyClockSourceId(&external_clock_source_id); | 195 | auto res = m_set_sys->GetExternalSteadyClockSourceId(&external_clock_source_id); |
diff --git a/src/core/hle/service/glue/time/manager.h b/src/core/hle/service/glue/time/manager.h index 1de93f8f9..bb4b65049 100644 --- a/src/core/hle/service/glue/time/manager.h +++ b/src/core/hle/service/glue/time/manager.h | |||
| @@ -26,6 +26,7 @@ namespace Service::Glue::Time { | |||
| 26 | class TimeManager { | 26 | class TimeManager { |
| 27 | public: | 27 | public: |
| 28 | explicit TimeManager(Core::System& system); | 28 | explicit TimeManager(Core::System& system); |
| 29 | ~TimeManager(); | ||
| 29 | 30 | ||
| 30 | std::shared_ptr<Service::Set::ISystemSettingsServer> m_set_sys; | 31 | std::shared_ptr<Service::Set::ISystemSettingsServer> m_set_sys; |
| 31 | 32 | ||
diff --git a/src/core/memory/cheat_engine.cpp b/src/core/memory/cheat_engine.cpp index b84b57d92..d8921e565 100644 --- a/src/core/memory/cheat_engine.cpp +++ b/src/core/memory/cheat_engine.cpp | |||
| @@ -117,9 +117,9 @@ bool StandardVmCallbacks::IsAddressInRange(VAddr in) const { | |||
| 117 | (in < metadata.heap_extents.base || | 117 | (in < metadata.heap_extents.base || |
| 118 | in >= metadata.heap_extents.base + metadata.heap_extents.size) && | 118 | in >= metadata.heap_extents.base + metadata.heap_extents.size) && |
| 119 | (in < metadata.alias_extents.base || | 119 | (in < metadata.alias_extents.base || |
| 120 | in >= metadata.heap_extents.base + metadata.alias_extents.size) && | 120 | in >= metadata.alias_extents.base + metadata.alias_extents.size) && |
| 121 | (in < metadata.aslr_extents.base || | 121 | (in < metadata.aslr_extents.base || |
| 122 | in >= metadata.heap_extents.base + metadata.aslr_extents.size)) { | 122 | in >= metadata.aslr_extents.base + metadata.aslr_extents.size)) { |
| 123 | LOG_DEBUG(CheatEngine, | 123 | LOG_DEBUG(CheatEngine, |
| 124 | "Cheat attempting to access memory at invalid address={:016X}, if this " | 124 | "Cheat attempting to access memory at invalid address={:016X}, if this " |
| 125 | "persists, " | 125 | "persists, " |
diff --git a/src/frontend_common/config.cpp b/src/frontend_common/config.cpp index 2bebfeef9..95f8c8c36 100644 --- a/src/frontend_common/config.cpp +++ b/src/frontend_common/config.cpp | |||
| @@ -138,6 +138,7 @@ void Config::ReadPlayerValues(const std::size_t player_index) { | |||
| 138 | if (profile_name.empty()) { | 138 | if (profile_name.empty()) { |
| 139 | // Use the global input config | 139 | // Use the global input config |
| 140 | player = Settings::values.players.GetValue(true)[player_index]; | 140 | player = Settings::values.players.GetValue(true)[player_index]; |
| 141 | player.profile_name = ""; | ||
| 141 | return; | 142 | return; |
| 142 | } | 143 | } |
| 143 | player.profile_name = profile_name; | 144 | player.profile_name = profile_name; |
diff --git a/src/frontend_common/content_manager.h b/src/frontend_common/content_manager.h index f3efe3465..c4e97a47b 100644 --- a/src/frontend_common/content_manager.h +++ b/src/frontend_common/content_manager.h | |||
| @@ -251,11 +251,12 @@ inline InstallResult InstallNCA(FileSys::VfsFilesystem& vfs, const std::string& | |||
| 251 | * \param callback Callback to report the progress of the installation. The first size_t | 251 | * \param callback Callback to report the progress of the installation. The first size_t |
| 252 | * parameter is the total size of the installed contents and the second is the current progress. If | 252 | * parameter is the total size of the installed contents and the second is the current progress. If |
| 253 | * you return true to the callback, it will cancel the installation as soon as possible. | 253 | * you return true to the callback, it will cancel the installation as soon as possible. |
| 254 | * \param firmware_only Set to true to only scan system nand NCAs (firmware), post firmware install. | ||
| 254 | * \return A list of entries that failed to install. Returns an empty vector if successful. | 255 | * \return A list of entries that failed to install. Returns an empty vector if successful. |
| 255 | */ | 256 | */ |
| 256 | inline std::vector<std::string> VerifyInstalledContents( | 257 | inline std::vector<std::string> VerifyInstalledContents( |
| 257 | Core::System& system, FileSys::ManualContentProvider& provider, | 258 | Core::System& system, FileSys::ManualContentProvider& provider, |
| 258 | const std::function<bool(size_t, size_t)>& callback) { | 259 | const std::function<bool(size_t, size_t)>& callback, bool firmware_only = false) { |
| 259 | // Get content registries. | 260 | // Get content registries. |
| 260 | auto bis_contents = system.GetFileSystemController().GetSystemNANDContents(); | 261 | auto bis_contents = system.GetFileSystemController().GetSystemNANDContents(); |
| 261 | auto user_contents = system.GetFileSystemController().GetUserNANDContents(); | 262 | auto user_contents = system.GetFileSystemController().GetUserNANDContents(); |
| @@ -264,7 +265,7 @@ inline std::vector<std::string> VerifyInstalledContents( | |||
| 264 | if (bis_contents) { | 265 | if (bis_contents) { |
| 265 | content_providers.push_back(bis_contents); | 266 | content_providers.push_back(bis_contents); |
| 266 | } | 267 | } |
| 267 | if (user_contents) { | 268 | if (user_contents && !firmware_only) { |
| 268 | content_providers.push_back(user_contents); | 269 | content_providers.push_back(user_contents); |
| 269 | } | 270 | } |
| 270 | 271 | ||
diff --git a/src/hid_core/frontend/emulated_controller.cpp b/src/hid_core/frontend/emulated_controller.cpp index 819460eb5..3fa06d188 100644 --- a/src/hid_core/frontend/emulated_controller.cpp +++ b/src/hid_core/frontend/emulated_controller.cpp | |||
| @@ -176,16 +176,19 @@ void EmulatedController::LoadDevices() { | |||
| 176 | camera_params[1] = Common::ParamPackage{"engine:camera,camera:1"}; | 176 | camera_params[1] = Common::ParamPackage{"engine:camera,camera:1"}; |
| 177 | ring_params[1] = Common::ParamPackage{"engine:joycon,axis_x:100,axis_y:101"}; | 177 | ring_params[1] = Common::ParamPackage{"engine:joycon,axis_x:100,axis_y:101"}; |
| 178 | nfc_params[0] = Common::ParamPackage{"engine:virtual_amiibo,nfc:1"}; | 178 | nfc_params[0] = Common::ParamPackage{"engine:virtual_amiibo,nfc:1"}; |
| 179 | android_params = Common::ParamPackage{"engine:android,port:100"}; | ||
| 179 | } | 180 | } |
| 180 | 181 | ||
| 181 | output_params[LeftIndex] = left_joycon; | 182 | output_params[LeftIndex] = left_joycon; |
| 182 | output_params[RightIndex] = right_joycon; | 183 | output_params[RightIndex] = right_joycon; |
| 183 | output_params[2] = camera_params[1]; | 184 | output_params[2] = camera_params[1]; |
| 184 | output_params[3] = nfc_params[0]; | 185 | output_params[3] = nfc_params[0]; |
| 186 | output_params[4] = android_params; | ||
| 185 | output_params[LeftIndex].Set("output", true); | 187 | output_params[LeftIndex].Set("output", true); |
| 186 | output_params[RightIndex].Set("output", true); | 188 | output_params[RightIndex].Set("output", true); |
| 187 | output_params[2].Set("output", true); | 189 | output_params[2].Set("output", true); |
| 188 | output_params[3].Set("output", true); | 190 | output_params[3].Set("output", true); |
| 191 | output_params[4].Set("output", true); | ||
| 189 | 192 | ||
| 190 | LoadTASParams(); | 193 | LoadTASParams(); |
| 191 | LoadVirtualGamepadParams(); | 194 | LoadVirtualGamepadParams(); |
| @@ -578,6 +581,9 @@ void EmulatedController::DisableConfiguration() { | |||
| 578 | 581 | ||
| 579 | // Get Joycon colors before turning on the controller | 582 | // Get Joycon colors before turning on the controller |
| 580 | for (const auto& color_device : color_devices) { | 583 | for (const auto& color_device : color_devices) { |
| 584 | if (color_device == nullptr) { | ||
| 585 | continue; | ||
| 586 | } | ||
| 581 | color_device->ForceUpdate(); | 587 | color_device->ForceUpdate(); |
| 582 | } | 588 | } |
| 583 | 589 | ||
| @@ -1277,6 +1283,10 @@ bool EmulatedController::SetVibration(DeviceIndex device_index, const VibrationV | |||
| 1277 | .high_frequency = vibration.high_frequency, | 1283 | .high_frequency = vibration.high_frequency, |
| 1278 | .type = type, | 1284 | .type = type, |
| 1279 | }; | 1285 | }; |
| 1286 | |||
| 1287 | // Send vibrations to Android's input overlay | ||
| 1288 | output_devices[4]->SetVibration(status); | ||
| 1289 | |||
| 1280 | return output_devices[index]->SetVibration(status) == Common::Input::DriverResult::Success; | 1290 | return output_devices[index]->SetVibration(status) == Common::Input::DriverResult::Success; |
| 1281 | } | 1291 | } |
| 1282 | 1292 | ||
diff --git a/src/hid_core/frontend/emulated_controller.h b/src/hid_core/frontend/emulated_controller.h index 701b38300..ab3c6fcd3 100644 --- a/src/hid_core/frontend/emulated_controller.h +++ b/src/hid_core/frontend/emulated_controller.h | |||
| @@ -21,7 +21,7 @@ | |||
| 21 | 21 | ||
| 22 | namespace Core::HID { | 22 | namespace Core::HID { |
| 23 | const std::size_t max_emulated_controllers = 2; | 23 | const std::size_t max_emulated_controllers = 2; |
| 24 | const std::size_t output_devices_size = 4; | 24 | const std::size_t output_devices_size = 5; |
| 25 | struct ControllerMotionInfo { | 25 | struct ControllerMotionInfo { |
| 26 | Common::Input::MotionStatus raw_status{}; | 26 | Common::Input::MotionStatus raw_status{}; |
| 27 | MotionInput emulated{}; | 27 | MotionInput emulated{}; |
| @@ -597,6 +597,7 @@ private: | |||
| 597 | CameraParams camera_params; | 597 | CameraParams camera_params; |
| 598 | RingAnalogParams ring_params; | 598 | RingAnalogParams ring_params; |
| 599 | NfcParams nfc_params; | 599 | NfcParams nfc_params; |
| 600 | Common::ParamPackage android_params; | ||
| 600 | OutputParams output_params; | 601 | OutputParams output_params; |
| 601 | 602 | ||
| 602 | ButtonDevices button_devices; | 603 | ButtonDevices button_devices; |
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(); |
diff --git a/src/yuzu/configuration/qt_config.cpp b/src/yuzu/configuration/qt_config.cpp index 1051031f2..37951b9c8 100644 --- a/src/yuzu/configuration/qt_config.cpp +++ b/src/yuzu/configuration/qt_config.cpp | |||
| @@ -90,6 +90,7 @@ void QtConfig::ReadQtPlayerValues(const std::size_t player_index) { | |||
| 90 | if (profile_name.empty()) { | 90 | if (profile_name.empty()) { |
| 91 | // Use the global input config | 91 | // Use the global input config |
| 92 | player = Settings::values.players.GetValue(true)[player_index]; | 92 | player = Settings::values.players.GetValue(true)[player_index]; |
| 93 | player.profile_name = ""; | ||
| 93 | return; | 94 | return; |
| 94 | } | 95 | } |
| 95 | } | 96 | } |
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index dfa50006a..0d16bfd65 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp | |||
| @@ -1603,6 +1603,7 @@ void GMainWindow::ConnectMenuEvents() { | |||
| 1603 | // Help | 1603 | // Help |
| 1604 | connect_menu(ui->action_Open_yuzu_Folder, &GMainWindow::OnOpenYuzuFolder); | 1604 | connect_menu(ui->action_Open_yuzu_Folder, &GMainWindow::OnOpenYuzuFolder); |
| 1605 | connect_menu(ui->action_Verify_installed_contents, &GMainWindow::OnVerifyInstalledContents); | 1605 | connect_menu(ui->action_Verify_installed_contents, &GMainWindow::OnVerifyInstalledContents); |
| 1606 | connect_menu(ui->action_Install_Firmware, &GMainWindow::OnInstallFirmware); | ||
| 1606 | connect_menu(ui->action_About, &GMainWindow::OnAbout); | 1607 | connect_menu(ui->action_About, &GMainWindow::OnAbout); |
| 1607 | } | 1608 | } |
| 1608 | 1609 | ||
| @@ -1631,6 +1632,8 @@ void GMainWindow::UpdateMenuState() { | |||
| 1631 | action->setEnabled(emulation_running); | 1632 | action->setEnabled(emulation_running); |
| 1632 | } | 1633 | } |
| 1633 | 1634 | ||
| 1635 | ui->action_Install_Firmware->setEnabled(!emulation_running); | ||
| 1636 | |||
| 1634 | for (QAction* action : applet_actions) { | 1637 | for (QAction* action : applet_actions) { |
| 1635 | action->setEnabled(is_firmware_available && !emulation_running); | 1638 | action->setEnabled(is_firmware_available && !emulation_running); |
| 1636 | } | 1639 | } |
| @@ -4150,6 +4153,146 @@ void GMainWindow::OnVerifyInstalledContents() { | |||
| 4150 | } | 4153 | } |
| 4151 | } | 4154 | } |
| 4152 | 4155 | ||
| 4156 | void GMainWindow::OnInstallFirmware() { | ||
| 4157 | // Don't do this while emulation is running, that'd probably be a bad idea. | ||
| 4158 | if (emu_thread != nullptr && emu_thread->IsRunning()) { | ||
| 4159 | return; | ||
| 4160 | } | ||
| 4161 | |||
| 4162 | // Check for installed keys, error out, suggest restart? | ||
| 4163 | if (!ContentManager::AreKeysPresent()) { | ||
| 4164 | QMessageBox::information( | ||
| 4165 | this, tr("Keys not installed"), | ||
| 4166 | tr("Install decryption keys and restart yuzu before attempting to install firmware.")); | ||
| 4167 | return; | ||
| 4168 | } | ||
| 4169 | |||
| 4170 | QString firmware_source_location = | ||
| 4171 | QFileDialog::getExistingDirectory(this, tr("Select Dumped Firmware Source Location"), | ||
| 4172 | QString::fromStdString(""), QFileDialog::ShowDirsOnly); | ||
| 4173 | if (firmware_source_location.isEmpty()) { | ||
| 4174 | return; | ||
| 4175 | } | ||
| 4176 | |||
| 4177 | QProgressDialog progress(tr("Installing Firmware..."), tr("Cancel"), 0, 100, this); | ||
| 4178 | progress.setWindowModality(Qt::WindowModal); | ||
| 4179 | progress.setMinimumDuration(100); | ||
| 4180 | progress.setAutoClose(false); | ||
| 4181 | progress.setAutoReset(false); | ||
| 4182 | progress.show(); | ||
| 4183 | |||
| 4184 | // Declare progress callback. | ||
| 4185 | auto QtProgressCallback = [&](size_t total_size, size_t processed_size) { | ||
| 4186 | progress.setValue(static_cast<int>((processed_size * 100) / total_size)); | ||
| 4187 | return progress.wasCanceled(); | ||
| 4188 | }; | ||
| 4189 | |||
| 4190 | LOG_INFO(Frontend, "Installing firmware from {}", firmware_source_location.toStdString()); | ||
| 4191 | |||
| 4192 | // Check for a reasonable number of .nca files (don't hardcode them, just see if there's some in | ||
| 4193 | // there.) | ||
| 4194 | std::filesystem::path firmware_source_path = firmware_source_location.toStdString(); | ||
| 4195 | if (!Common::FS::IsDir(firmware_source_path)) { | ||
| 4196 | progress.close(); | ||
| 4197 | return; | ||
| 4198 | } | ||
| 4199 | |||
| 4200 | std::vector<std::filesystem::path> out; | ||
| 4201 | const Common::FS::DirEntryCallable callback = | ||
| 4202 | [&out](const std::filesystem::directory_entry& entry) { | ||
| 4203 | if (entry.path().has_extension() && entry.path().extension() == ".nca") | ||
| 4204 | out.emplace_back(entry.path()); | ||
| 4205 | |||
| 4206 | return true; | ||
| 4207 | }; | ||
| 4208 | |||
| 4209 | QtProgressCallback(100, 10); | ||
| 4210 | |||
| 4211 | Common::FS::IterateDirEntries(firmware_source_path, callback, Common::FS::DirEntryFilter::File); | ||
| 4212 | if (out.size() <= 0) { | ||
| 4213 | progress.close(); | ||
| 4214 | QMessageBox::warning(this, tr("Firmware install failed"), | ||
| 4215 | tr("Unable to locate potential firmware NCA files")); | ||
| 4216 | return; | ||
| 4217 | } | ||
| 4218 | |||
| 4219 | // Locate and erase the content of nand/system/Content/registered/*.nca, if any. | ||
| 4220 | auto sysnand_content_vdir = system->GetFileSystemController().GetSystemNANDContentDirectory(); | ||
| 4221 | if (!sysnand_content_vdir->CleanSubdirectoryRecursive("registered")) { | ||
| 4222 | progress.close(); | ||
| 4223 | QMessageBox::critical(this, tr("Firmware install failed"), | ||
| 4224 | tr("Failed to delete one or more firmware file.")); | ||
| 4225 | return; | ||
| 4226 | } | ||
| 4227 | |||
| 4228 | LOG_INFO(Frontend, | ||
| 4229 | "Cleaned nand/system/Content/registered folder in preparation for new firmware."); | ||
| 4230 | |||
| 4231 | QtProgressCallback(100, 20); | ||
| 4232 | |||
| 4233 | auto firmware_vdir = sysnand_content_vdir->GetDirectoryRelative("registered"); | ||
| 4234 | |||
| 4235 | bool success = true; | ||
| 4236 | bool cancelled = false; | ||
| 4237 | int i = 0; | ||
| 4238 | for (const auto& firmware_src_path : out) { | ||
| 4239 | i++; | ||
| 4240 | auto firmware_src_vfile = | ||
| 4241 | vfs->OpenFile(firmware_src_path.generic_string(), FileSys::OpenMode::Read); | ||
| 4242 | auto firmware_dst_vfile = | ||
| 4243 | firmware_vdir->CreateFileRelative(firmware_src_path.filename().string()); | ||
| 4244 | |||
| 4245 | if (!VfsRawCopy(firmware_src_vfile, firmware_dst_vfile)) { | ||
| 4246 | LOG_ERROR(Frontend, "Failed to copy firmware file {} to {} in registered folder!", | ||
| 4247 | firmware_src_path.generic_string(), firmware_src_path.filename().string()); | ||
| 4248 | success = false; | ||
| 4249 | } | ||
| 4250 | |||
| 4251 | if (QtProgressCallback(100, 20 + (int)(((float)(i) / (float)out.size()) * 70.0))) { | ||
| 4252 | success = false; | ||
| 4253 | cancelled = true; | ||
| 4254 | break; | ||
| 4255 | } | ||
| 4256 | } | ||
| 4257 | |||
| 4258 | if (!success && !cancelled) { | ||
| 4259 | progress.close(); | ||
| 4260 | QMessageBox::critical(this, tr("Firmware install failed"), | ||
| 4261 | tr("One or more firmware files failed to copy into NAND.")); | ||
| 4262 | return; | ||
| 4263 | } else if (cancelled) { | ||
| 4264 | progress.close(); | ||
| 4265 | QMessageBox::warning(this, tr("Firmware install failed"), | ||
| 4266 | tr("Firmware installation cancelled, firmware may be in bad state, " | ||
| 4267 | "restart yuzu or re-install firmware.")); | ||
| 4268 | return; | ||
| 4269 | } | ||
| 4270 | |||
| 4271 | // Re-scan VFS for the newly placed firmware files. | ||
| 4272 | system->GetFileSystemController().CreateFactories(*vfs); | ||
| 4273 | |||
| 4274 | auto VerifyFirmwareCallback = [&](size_t total_size, size_t processed_size) { | ||
| 4275 | progress.setValue(90 + static_cast<int>((processed_size * 10) / total_size)); | ||
| 4276 | return progress.wasCanceled(); | ||
| 4277 | }; | ||
| 4278 | |||
| 4279 | auto result = | ||
| 4280 | ContentManager::VerifyInstalledContents(*system, *provider, VerifyFirmwareCallback, true); | ||
| 4281 | |||
| 4282 | if (result.size() > 0) { | ||
| 4283 | const auto failed_names = | ||
| 4284 | QString::fromStdString(fmt::format("{}", fmt::join(result, "\n"))); | ||
| 4285 | progress.close(); | ||
| 4286 | QMessageBox::critical( | ||
| 4287 | this, tr("Firmware integrity verification failed!"), | ||
| 4288 | tr("Verification failed for the following files:\n\n%1").arg(failed_names)); | ||
| 4289 | return; | ||
| 4290 | } | ||
| 4291 | |||
| 4292 | progress.close(); | ||
| 4293 | OnCheckFirmwareDecryption(); | ||
| 4294 | } | ||
| 4295 | |||
| 4153 | void GMainWindow::OnAbout() { | 4296 | void GMainWindow::OnAbout() { |
| 4154 | AboutDialog aboutDialog(this); | 4297 | AboutDialog aboutDialog(this); |
| 4155 | aboutDialog.exec(); | 4298 | aboutDialog.exec(); |
diff --git a/src/yuzu/main.h b/src/yuzu/main.h index aba61e388..1f0e35c67 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h | |||
| @@ -380,6 +380,7 @@ private slots: | |||
| 380 | void OnLoadAmiibo(); | 380 | void OnLoadAmiibo(); |
| 381 | void OnOpenYuzuFolder(); | 381 | void OnOpenYuzuFolder(); |
| 382 | void OnVerifyInstalledContents(); | 382 | void OnVerifyInstalledContents(); |
| 383 | void OnInstallFirmware(); | ||
| 383 | void OnAbout(); | 384 | void OnAbout(); |
| 384 | void OnToggleFilterBar(); | 385 | void OnToggleFilterBar(); |
| 385 | void OnToggleStatusBar(); | 386 | void OnToggleStatusBar(); |
diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index 6a6b0821f..6ff444a22 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui | |||
| @@ -25,7 +25,16 @@ | |||
| 25 | </property> | 25 | </property> |
| 26 | <widget class="QWidget" name="centralwidget"> | 26 | <widget class="QWidget" name="centralwidget"> |
| 27 | <layout class="QHBoxLayout" name="horizontalLayout"> | 27 | <layout class="QHBoxLayout" name="horizontalLayout"> |
| 28 | <property name="margin" stdset="0"> | 28 | <property name="leftMargin"> |
| 29 | <number>0</number> | ||
| 30 | </property> | ||
| 31 | <property name="topMargin"> | ||
| 32 | <number>0</number> | ||
| 33 | </property> | ||
| 34 | <property name="rightMargin"> | ||
| 35 | <number>0</number> | ||
| 36 | </property> | ||
| 37 | <property name="bottomMargin"> | ||
| 29 | <number>0</number> | 38 | <number>0</number> |
| 30 | </property> | 39 | </property> |
| 31 | </layout> | 40 | </layout> |
| @@ -156,8 +165,8 @@ | |||
| 156 | <addaction name="separator"/> | 165 | <addaction name="separator"/> |
| 157 | <addaction name="action_Configure_Tas"/> | 166 | <addaction name="action_Configure_Tas"/> |
| 158 | </widget> | 167 | </widget> |
| 159 | <addaction name="action_Rederive"/> | ||
| 160 | <addaction name="action_Verify_installed_contents"/> | 168 | <addaction name="action_Verify_installed_contents"/> |
| 169 | <addaction name="action_Install_Firmware"/> | ||
| 161 | <addaction name="separator"/> | 170 | <addaction name="separator"/> |
| 162 | <addaction name="menu_cabinet_applet"/> | 171 | <addaction name="menu_cabinet_applet"/> |
| 163 | <addaction name="action_Load_Album"/> | 172 | <addaction name="action_Load_Album"/> |
| @@ -455,6 +464,11 @@ | |||
| 455 | <string>Open &Controller Menu</string> | 464 | <string>Open &Controller Menu</string> |
| 456 | </property> | 465 | </property> |
| 457 | </action> | 466 | </action> |
| 467 | <action name="action_Install_Firmware"> | ||
| 468 | <property name="text"> | ||
| 469 | <string>Install Firmware</string> | ||
| 470 | </property> | ||
| 471 | </action> | ||
| 458 | </widget> | 472 | </widget> |
| 459 | <resources> | 473 | <resources> |
| 460 | <include location="yuzu.qrc"/> | 474 | <include location="yuzu.qrc"/> |
diff --git a/src/yuzu_cmd/sdl_config.cpp b/src/yuzu_cmd/sdl_config.cpp index 995114510..6e0f254b6 100644 --- a/src/yuzu_cmd/sdl_config.cpp +++ b/src/yuzu_cmd/sdl_config.cpp | |||
| @@ -103,6 +103,7 @@ void SdlConfig::ReadSdlPlayerValues(const std::size_t player_index) { | |||
| 103 | if (profile_name.empty()) { | 103 | if (profile_name.empty()) { |
| 104 | // Use the global input config | 104 | // Use the global input config |
| 105 | player = Settings::values.players.GetValue(true)[player_index]; | 105 | player = Settings::values.players.GetValue(true)[player_index]; |
| 106 | player.profile_name = ""; | ||
| 106 | return; | 107 | return; |
| 107 | } | 108 | } |
| 108 | } | 109 | } |