mirror of
				https://git.suyu.dev/suyu/suyu
				synced 2025-11-04 00:49:02 -06:00 
			
		
		
		
	Merge pull request #13034 from t895/map-all-the-inputs
android: Input mapping
This commit is contained in:
		@@ -14,6 +14,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
    <uses-permission android:name="android.permission.INTERNET" />
 | 
			
		||||
    <uses-permission android:name="android.permission.NFC" />
 | 
			
		||||
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
 | 
			
		||||
    <uses-permission android:name="android.permission.VIBRATE" />
 | 
			
		||||
 | 
			
		||||
    <application
 | 
			
		||||
        android:name="org.yuzu.yuzu_emu.YuzuApplication"
 | 
			
		||||
 
 | 
			
		||||
@@ -30,34 +30,6 @@ import org.yuzu.yuzu_emu.model.GameVerificationResult
 | 
			
		||||
 * with the native side of the Yuzu code.
 | 
			
		||||
 */
 | 
			
		||||
object NativeLibrary {
 | 
			
		||||
    /**
 | 
			
		||||
     * Default controller id for each device
 | 
			
		||||
     */
 | 
			
		||||
    const val Player1Device = 0
 | 
			
		||||
    const val Player2Device = 1
 | 
			
		||||
    const val Player3Device = 2
 | 
			
		||||
    const val Player4Device = 3
 | 
			
		||||
    const val Player5Device = 4
 | 
			
		||||
    const val Player6Device = 5
 | 
			
		||||
    const val Player7Device = 6
 | 
			
		||||
    const val Player8Device = 7
 | 
			
		||||
    const val ConsoleDevice = 8
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Controller type for each device
 | 
			
		||||
     */
 | 
			
		||||
    const val ProController = 3
 | 
			
		||||
    const val Handheld = 4
 | 
			
		||||
    const val JoyconDual = 5
 | 
			
		||||
    const val JoyconLeft = 6
 | 
			
		||||
    const val JoyconRight = 7
 | 
			
		||||
    const val GameCube = 8
 | 
			
		||||
    const val Pokeball = 9
 | 
			
		||||
    const val NES = 10
 | 
			
		||||
    const val SNES = 11
 | 
			
		||||
    const val N64 = 12
 | 
			
		||||
    const val SegaGenesis = 13
 | 
			
		||||
 | 
			
		||||
    @JvmField
 | 
			
		||||
    var sEmulationActivity = WeakReference<EmulationActivity?>(null)
 | 
			
		||||
 | 
			
		||||
@@ -127,112 +99,6 @@ object NativeLibrary {
 | 
			
		||||
            FileUtil.getFilename(Uri.parse(path))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns true if pro controller isn't available and handheld is
 | 
			
		||||
     */
 | 
			
		||||
    external fun isHandheldOnly(): Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Changes controller type for a specific device.
 | 
			
		||||
     *
 | 
			
		||||
     * @param Device The input descriptor of the gamepad.
 | 
			
		||||
     * @param Type The NpadStyleIndex of the gamepad.
 | 
			
		||||
     */
 | 
			
		||||
    external fun setDeviceType(Device: Int, Type: Int): Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handles event when a gamepad is connected.
 | 
			
		||||
     *
 | 
			
		||||
     * @param Device The input descriptor of the gamepad.
 | 
			
		||||
     */
 | 
			
		||||
    external fun onGamePadConnectEvent(Device: Int): Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handles event when a gamepad is disconnected.
 | 
			
		||||
     *
 | 
			
		||||
     * @param Device The input descriptor of the gamepad.
 | 
			
		||||
     */
 | 
			
		||||
    external fun onGamePadDisconnectEvent(Device: Int): Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handles button press events for a gamepad.
 | 
			
		||||
     *
 | 
			
		||||
     * @param Device The input descriptor of the gamepad.
 | 
			
		||||
     * @param Button Key code identifying which button was pressed.
 | 
			
		||||
     * @param Action Mask identifying which action is happening (button pressed down, or button released).
 | 
			
		||||
     * @return If we handled the button press.
 | 
			
		||||
     */
 | 
			
		||||
    external fun onGamePadButtonEvent(Device: Int, Button: Int, Action: Int): Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handles joystick movement events.
 | 
			
		||||
     *
 | 
			
		||||
     * @param Device The device ID of the gamepad.
 | 
			
		||||
     * @param Axis   The axis ID
 | 
			
		||||
     * @param x_axis The value of the x-axis represented by the given ID.
 | 
			
		||||
     * @param y_axis The value of the y-axis represented by the given ID.
 | 
			
		||||
     */
 | 
			
		||||
    external fun onGamePadJoystickEvent(
 | 
			
		||||
        Device: Int,
 | 
			
		||||
        Axis: Int,
 | 
			
		||||
        x_axis: Float,
 | 
			
		||||
        y_axis: Float
 | 
			
		||||
    ): Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handles motion events.
 | 
			
		||||
     *
 | 
			
		||||
     * @param delta_timestamp         The finger id corresponding to this event
 | 
			
		||||
     * @param gyro_x,gyro_y,gyro_z    The value of the accelerometer sensor.
 | 
			
		||||
     * @param accel_x,accel_y,accel_z The value of the y-axis
 | 
			
		||||
     */
 | 
			
		||||
    external fun onGamePadMotionEvent(
 | 
			
		||||
        Device: Int,
 | 
			
		||||
        delta_timestamp: Long,
 | 
			
		||||
        gyro_x: Float,
 | 
			
		||||
        gyro_y: Float,
 | 
			
		||||
        gyro_z: Float,
 | 
			
		||||
        accel_x: Float,
 | 
			
		||||
        accel_y: Float,
 | 
			
		||||
        accel_z: Float
 | 
			
		||||
    ): Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Signals and load a nfc tag
 | 
			
		||||
     *
 | 
			
		||||
     * @param data         Byte array containing all the data from a nfc tag
 | 
			
		||||
     */
 | 
			
		||||
    external fun onReadNfcTag(data: ByteArray?): Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Removes current loaded nfc tag
 | 
			
		||||
     */
 | 
			
		||||
    external fun onRemoveNfcTag(): Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handles touch press events.
 | 
			
		||||
     *
 | 
			
		||||
     * @param finger_id The finger id corresponding to this event
 | 
			
		||||
     * @param x_axis    The value of the x-axis.
 | 
			
		||||
     * @param y_axis    The value of the y-axis.
 | 
			
		||||
     */
 | 
			
		||||
    external fun onTouchPressed(finger_id: Int, x_axis: Float, y_axis: Float)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handles touch movement.
 | 
			
		||||
     *
 | 
			
		||||
     * @param x_axis The value of the instantaneous x-axis.
 | 
			
		||||
     * @param y_axis The value of the instantaneous y-axis.
 | 
			
		||||
     */
 | 
			
		||||
    external fun onTouchMoved(finger_id: Int, x_axis: Float, y_axis: Float)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handles touch release events.
 | 
			
		||||
     *
 | 
			
		||||
     * @param finger_id The finger id corresponding to this event
 | 
			
		||||
     */
 | 
			
		||||
    external fun onTouchReleased(finger_id: Int)
 | 
			
		||||
 | 
			
		||||
    external fun setAppDirectory(directory: String)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -629,46 +495,4 @@ object NativeLibrary {
 | 
			
		||||
     * Checks if all necessary keys are present for decryption
 | 
			
		||||
     */
 | 
			
		||||
    external fun areKeysPresent(): Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Button type for use in onTouchEvent
 | 
			
		||||
     */
 | 
			
		||||
    object ButtonType {
 | 
			
		||||
        const val BUTTON_A = 0
 | 
			
		||||
        const val BUTTON_B = 1
 | 
			
		||||
        const val BUTTON_X = 2
 | 
			
		||||
        const val BUTTON_Y = 3
 | 
			
		||||
        const val STICK_L = 4
 | 
			
		||||
        const val STICK_R = 5
 | 
			
		||||
        const val TRIGGER_L = 6
 | 
			
		||||
        const val TRIGGER_R = 7
 | 
			
		||||
        const val TRIGGER_ZL = 8
 | 
			
		||||
        const val TRIGGER_ZR = 9
 | 
			
		||||
        const val BUTTON_PLUS = 10
 | 
			
		||||
        const val BUTTON_MINUS = 11
 | 
			
		||||
        const val DPAD_LEFT = 12
 | 
			
		||||
        const val DPAD_UP = 13
 | 
			
		||||
        const val DPAD_RIGHT = 14
 | 
			
		||||
        const val DPAD_DOWN = 15
 | 
			
		||||
        const val BUTTON_SL = 16
 | 
			
		||||
        const val BUTTON_SR = 17
 | 
			
		||||
        const val BUTTON_HOME = 18
 | 
			
		||||
        const val BUTTON_CAPTURE = 19
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Stick type for use in onTouchEvent
 | 
			
		||||
     */
 | 
			
		||||
    object StickType {
 | 
			
		||||
        const val STICK_L = 0
 | 
			
		||||
        const val STICK_R = 1
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Button states
 | 
			
		||||
     */
 | 
			
		||||
    object ButtonState {
 | 
			
		||||
        const val RELEASED = 0
 | 
			
		||||
        const val PRESSED = 1
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import android.app.Application
 | 
			
		||||
import android.app.NotificationChannel
 | 
			
		||||
import android.app.NotificationManager
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput
 | 
			
		||||
import java.io.File
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.DocumentsTree
 | 
			
		||||
@@ -37,6 +38,7 @@ class YuzuApplication : Application() {
 | 
			
		||||
        documentsTree = DocumentsTree()
 | 
			
		||||
        DirectoryInitialization.start()
 | 
			
		||||
        GpuDriverHelper.initializeDriverParameters()
 | 
			
		||||
        NativeInput.reloadInputDevices()
 | 
			
		||||
        NativeLibrary.logDeviceInfo()
 | 
			
		||||
        Log.logDeviceInfo()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,7 @@ import org.yuzu.yuzu_emu.NativeLibrary
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
import org.yuzu.yuzu_emu.YuzuApplication
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
 | 
			
		||||
@@ -47,7 +48,9 @@ import org.yuzu.yuzu_emu.model.Game
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.InputHandler
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.Log
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.MemoryUtil
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.NativeConfig
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.NfcReader
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.ParamPackage
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.ThemeHelper
 | 
			
		||||
import java.text.NumberFormat
 | 
			
		||||
import kotlin.math.roundToInt
 | 
			
		||||
@@ -63,8 +66,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
 | 
			
		||||
    private var motionTimestamp: Long = 0
 | 
			
		||||
    private var flipMotionOrientation: Boolean = false
 | 
			
		||||
 | 
			
		||||
    private var controllerIds = InputHandler.getGameControllerIds()
 | 
			
		||||
 | 
			
		||||
    private val actionPause = "ACTION_EMULATOR_PAUSE"
 | 
			
		||||
    private val actionPlay = "ACTION_EMULATOR_PLAY"
 | 
			
		||||
    private val actionMute = "ACTION_EMULATOR_MUTE"
 | 
			
		||||
@@ -78,6 +79,27 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
 | 
			
		||||
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
 | 
			
		||||
        InputHandler.updateControllerData()
 | 
			
		||||
        val playerOne = NativeConfig.getInputSettings(true)[0]
 | 
			
		||||
        if (!playerOne.hasMapping() && InputHandler.androidControllers.isNotEmpty()) {
 | 
			
		||||
            var params: ParamPackage? = null
 | 
			
		||||
            for (controller in InputHandler.registeredControllers) {
 | 
			
		||||
                if (controller.get("port", -1) == 0) {
 | 
			
		||||
                    params = controller
 | 
			
		||||
                    break
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (params != null) {
 | 
			
		||||
                NativeInput.updateMappingsWithDefault(
 | 
			
		||||
                    0,
 | 
			
		||||
                    params,
 | 
			
		||||
                    params.get("display", getString(R.string.unknown))
 | 
			
		||||
                )
 | 
			
		||||
                NativeConfig.saveGlobalConfig()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding = ActivityEmulationBinding.inflate(layoutInflater)
 | 
			
		||||
        setContentView(binding.root)
 | 
			
		||||
 | 
			
		||||
@@ -95,8 +117,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
 | 
			
		||||
        nfcReader = NfcReader(this)
 | 
			
		||||
        nfcReader.initialize()
 | 
			
		||||
 | 
			
		||||
        InputHandler.initialize()
 | 
			
		||||
 | 
			
		||||
        val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
 | 
			
		||||
        if (!preferences.getBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, false)) {
 | 
			
		||||
            if (MemoryUtil.isLessThan(MemoryUtil.REQUIRED_MEMORY, MemoryUtil.totalMemory)) {
 | 
			
		||||
@@ -147,7 +167,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
 | 
			
		||||
        super.onResume()
 | 
			
		||||
        nfcReader.startScanning()
 | 
			
		||||
        startMotionSensorListener()
 | 
			
		||||
        InputHandler.updateControllerIds()
 | 
			
		||||
        InputHandler.updateControllerData()
 | 
			
		||||
 | 
			
		||||
        buildPictureInPictureParams()
 | 
			
		||||
    }
 | 
			
		||||
@@ -172,6 +192,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
 | 
			
		||||
        super.onNewIntent(intent)
 | 
			
		||||
        setIntent(intent)
 | 
			
		||||
        nfcReader.onNewIntent(intent)
 | 
			
		||||
        InputHandler.updateControllerData()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
 | 
			
		||||
@@ -244,8 +265,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
 | 
			
		||||
        }
 | 
			
		||||
        val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000
 | 
			
		||||
        motionTimestamp = event.timestamp
 | 
			
		||||
        NativeLibrary.onGamePadMotionEvent(
 | 
			
		||||
            NativeLibrary.Player1Device,
 | 
			
		||||
        NativeInput.onDeviceMotionEvent(
 | 
			
		||||
            NativeInput.Player1Device,
 | 
			
		||||
            deltaTimestamp,
 | 
			
		||||
            gyro[0],
 | 
			
		||||
            gyro[1],
 | 
			
		||||
@@ -254,8 +275,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
 | 
			
		||||
            accel[1],
 | 
			
		||||
            accel[2]
 | 
			
		||||
        )
 | 
			
		||||
        NativeLibrary.onGamePadMotionEvent(
 | 
			
		||||
            NativeLibrary.ConsoleDevice,
 | 
			
		||||
        NativeInput.onDeviceMotionEvent(
 | 
			
		||||
            NativeInput.ConsoleDevice,
 | 
			
		||||
            deltaTimestamp,
 | 
			
		||||
            gyro[0],
 | 
			
		||||
            gyro[1],
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,416 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.input
 | 
			
		||||
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.NativeButton
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.InputType
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.ButtonName
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.NativeConfig
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.ParamPackage
 | 
			
		||||
import android.view.InputDevice
 | 
			
		||||
 | 
			
		||||
object NativeInput {
 | 
			
		||||
    /**
 | 
			
		||||
     * Default controller id for each device
 | 
			
		||||
     */
 | 
			
		||||
    const val Player1Device = 0
 | 
			
		||||
    const val Player2Device = 1
 | 
			
		||||
    const val Player3Device = 2
 | 
			
		||||
    const val Player4Device = 3
 | 
			
		||||
    const val Player5Device = 4
 | 
			
		||||
    const val Player6Device = 5
 | 
			
		||||
    const val Player7Device = 6
 | 
			
		||||
    const val Player8Device = 7
 | 
			
		||||
    const val ConsoleDevice = 8
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Button states
 | 
			
		||||
     */
 | 
			
		||||
    object ButtonState {
 | 
			
		||||
        const val RELEASED = 0
 | 
			
		||||
        const val PRESSED = 1
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns true if pro controller isn't available and handheld is.
 | 
			
		||||
     * Intended to check where the input overlay should direct its inputs.
 | 
			
		||||
     */
 | 
			
		||||
    external fun isHandheldOnly(): Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handles button press events for a gamepad.
 | 
			
		||||
     * @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
 | 
			
		||||
     * @param port Port determined by controller connection order.
 | 
			
		||||
     * @param buttonId The Android Keycode corresponding to this event.
 | 
			
		||||
     * @param action Mask identifying which action is happening (button pressed down, or button released).
 | 
			
		||||
     */
 | 
			
		||||
    external fun onGamePadButtonEvent(
 | 
			
		||||
        guid: String,
 | 
			
		||||
        port: Int,
 | 
			
		||||
        buttonId: Int,
 | 
			
		||||
        action: Int
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handles axis movement events.
 | 
			
		||||
     * @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
 | 
			
		||||
     * @param port Port determined by controller connection order.
 | 
			
		||||
     * @param axis The axis ID.
 | 
			
		||||
     * @param value Value along the given axis.
 | 
			
		||||
     */
 | 
			
		||||
    external fun onGamePadAxisEvent(guid: String, port: Int, axis: Int, value: Float)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handles motion events.
 | 
			
		||||
     * @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
 | 
			
		||||
     * @param port Port determined by controller connection order.
 | 
			
		||||
     * @param deltaTimestamp The finger id corresponding to this event.
 | 
			
		||||
     * @param xGyro The value of the x-axis for the gyroscope.
 | 
			
		||||
     * @param yGyro The value of the y-axis for the gyroscope.
 | 
			
		||||
     * @param zGyro The value of the z-axis for the gyroscope.
 | 
			
		||||
     * @param xAccel The value of the x-axis for the accelerometer.
 | 
			
		||||
     * @param yAccel The value of the y-axis for the accelerometer.
 | 
			
		||||
     * @param zAccel The value of the z-axis for the accelerometer.
 | 
			
		||||
     */
 | 
			
		||||
    external fun onGamePadMotionEvent(
 | 
			
		||||
        guid: String,
 | 
			
		||||
        port: Int,
 | 
			
		||||
        deltaTimestamp: Long,
 | 
			
		||||
        xGyro: Float,
 | 
			
		||||
        yGyro: Float,
 | 
			
		||||
        zGyro: Float,
 | 
			
		||||
        xAccel: Float,
 | 
			
		||||
        yAccel: Float,
 | 
			
		||||
        zAccel: Float
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Signals and load a nfc tag
 | 
			
		||||
     * @param data Byte array containing all the data from a nfc tag.
 | 
			
		||||
     */
 | 
			
		||||
    external fun onReadNfcTag(data: ByteArray?)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Removes current loaded nfc tag.
 | 
			
		||||
     */
 | 
			
		||||
    external fun onRemoveNfcTag()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handles touch press events.
 | 
			
		||||
     * @param fingerId The finger id corresponding to this event.
 | 
			
		||||
     * @param xAxis The value of the x-axis on the touchscreen.
 | 
			
		||||
     * @param yAxis The value of the y-axis on the touchscreen.
 | 
			
		||||
     */
 | 
			
		||||
    external fun onTouchPressed(fingerId: Int, xAxis: Float, yAxis: Float)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handles touch movement.
 | 
			
		||||
     * @param fingerId The finger id corresponding to this event.
 | 
			
		||||
     * @param xAxis The value of the x-axis on the touchscreen.
 | 
			
		||||
     * @param yAxis The value of the y-axis on the touchscreen.
 | 
			
		||||
     */
 | 
			
		||||
    external fun onTouchMoved(fingerId: Int, xAxis: Float, yAxis: Float)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handles touch release events.
 | 
			
		||||
     * @param fingerId The finger id corresponding to this event
 | 
			
		||||
     */
 | 
			
		||||
    external fun onTouchReleased(fingerId: Int)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sends a button input to the global virtual controllers.
 | 
			
		||||
     * @param port Port determined by controller connection order.
 | 
			
		||||
     * @param button The [NativeButton] corresponding to this event.
 | 
			
		||||
     * @param action Mask identifying which action is happening (button pressed down, or button released).
 | 
			
		||||
     */
 | 
			
		||||
    fun onOverlayButtonEvent(port: Int, button: NativeButton, action: Int) =
 | 
			
		||||
        onOverlayButtonEventImpl(port, button.int, action)
 | 
			
		||||
 | 
			
		||||
    private external fun onOverlayButtonEventImpl(port: Int, buttonId: Int, action: Int)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sends a joystick input to the global virtual controllers.
 | 
			
		||||
     * @param port Port determined by controller connection order.
 | 
			
		||||
     * @param stick The [NativeAnalog] corresponding to this event.
 | 
			
		||||
     * @param xAxis Value along the X axis.
 | 
			
		||||
     * @param yAxis Value along the Y axis.
 | 
			
		||||
     */
 | 
			
		||||
    fun onOverlayJoystickEvent(port: Int, stick: NativeAnalog, xAxis: Float, yAxis: Float) =
 | 
			
		||||
        onOverlayJoystickEventImpl(port, stick.int, xAxis, yAxis)
 | 
			
		||||
 | 
			
		||||
    private external fun onOverlayJoystickEventImpl(
 | 
			
		||||
        port: Int,
 | 
			
		||||
        stickId: Int,
 | 
			
		||||
        xAxis: Float,
 | 
			
		||||
        yAxis: Float
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handles motion events for the global virtual controllers.
 | 
			
		||||
     * @param port Port determined by controller connection order
 | 
			
		||||
     * @param deltaTimestamp The finger id corresponding to this event.
 | 
			
		||||
     * @param xGyro The value of the x-axis for the gyroscope.
 | 
			
		||||
     * @param yGyro The value of the y-axis for the gyroscope.
 | 
			
		||||
     * @param zGyro The value of the z-axis for the gyroscope.
 | 
			
		||||
     * @param xAccel The value of the x-axis for the accelerometer.
 | 
			
		||||
     * @param yAccel The value of the y-axis for the accelerometer.
 | 
			
		||||
     * @param zAccel The value of the z-axis for the accelerometer.
 | 
			
		||||
     */
 | 
			
		||||
    external fun onDeviceMotionEvent(
 | 
			
		||||
        port: Int,
 | 
			
		||||
        deltaTimestamp: Long,
 | 
			
		||||
        xGyro: Float,
 | 
			
		||||
        yGyro: Float,
 | 
			
		||||
        zGyro: Float,
 | 
			
		||||
        xAccel: Float,
 | 
			
		||||
        yAccel: Float,
 | 
			
		||||
        zAccel: Float
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reloads all input devices from the currently loaded Settings::values.players into HID Core
 | 
			
		||||
     */
 | 
			
		||||
    external fun reloadInputDevices()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Registers a controller to be used with mapping
 | 
			
		||||
     * @param device An [InputDevice] or the input overlay wrapped with [YuzuInputDevice]
 | 
			
		||||
     */
 | 
			
		||||
    external fun registerController(device: YuzuInputDevice)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the names of input devices that have been registered with the input subsystem via [registerController]
 | 
			
		||||
     */
 | 
			
		||||
    external fun getInputDevices(): Array<String>
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reads all input profiles from disk. Must be called before creating a profile picker.
 | 
			
		||||
     */
 | 
			
		||||
    external fun loadInputProfiles()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the names of each available input profile.
 | 
			
		||||
     */
 | 
			
		||||
    external fun getInputProfileNames(): Array<String>
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if the user-provided name for an input profile is valid.
 | 
			
		||||
     * @param name User-provided name for an input profile.
 | 
			
		||||
     * @return Whether [name] is valid or not.
 | 
			
		||||
     */
 | 
			
		||||
    external fun isProfileNameValid(name: String): Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new input profile.
 | 
			
		||||
     * @param name The new profile's name.
 | 
			
		||||
     * @param playerIndex Index of the player that's currently being edited. Used to write the profile
 | 
			
		||||
     * name to this player's config.
 | 
			
		||||
     * @return Whether creating the profile was successful or not.
 | 
			
		||||
     */
 | 
			
		||||
    external fun createProfile(name: String, playerIndex: Int): Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Deletes an input profile.
 | 
			
		||||
     * @param name Name of the profile to delete.
 | 
			
		||||
     * @param playerIndex Index of the player that's currently being edited. Used to remove the profile
 | 
			
		||||
     * name from this player's config if they have it loaded.
 | 
			
		||||
     * @return Whether deleting this profile was successful or not.
 | 
			
		||||
     */
 | 
			
		||||
    external fun deleteProfile(name: String, playerIndex: Int): Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Loads an input profile.
 | 
			
		||||
     * @param name Name of the input profile to load.
 | 
			
		||||
     * @param playerIndex Index of the player that will have this profile loaded.
 | 
			
		||||
     * @return Whether loading this profile was successful or not.
 | 
			
		||||
     */
 | 
			
		||||
    external fun loadProfile(name: String, playerIndex: Int): Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Saves an input profile.
 | 
			
		||||
     * @param name Name of the profile to save.
 | 
			
		||||
     * @param playerIndex Index of the player that's currently being edited. Used to write the profile
 | 
			
		||||
     * name to this player's config.
 | 
			
		||||
     * @return Whether saving the profile was successful or not.
 | 
			
		||||
     */
 | 
			
		||||
    external fun saveProfile(name: String, playerIndex: Int): Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Intended to be used immediately before a call to [NativeConfig.saveControlPlayerValues]
 | 
			
		||||
     * Must be used while per-game config is loaded.
 | 
			
		||||
     */
 | 
			
		||||
    external fun loadPerGameConfiguration(
 | 
			
		||||
        playerIndex: Int,
 | 
			
		||||
        selectedIndex: Int,
 | 
			
		||||
        selectedProfileName: String
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Tells the input subsystem to start listening for inputs to map.
 | 
			
		||||
     * @param type Type of input to map as shown by the int property in each [InputType].
 | 
			
		||||
     */
 | 
			
		||||
    external fun beginMapping(type: Int)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets an input's [ParamPackage] as a serialized string. Used for input verification before mapping.
 | 
			
		||||
     * Must be run after [beginMapping] and before [stopMapping].
 | 
			
		||||
     */
 | 
			
		||||
    external fun getNextInput(): String
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Tells the input subsystem to stop listening for inputs to map.
 | 
			
		||||
     */
 | 
			
		||||
    external fun stopMapping()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Updates a controller's mappings with auto-mapping params.
 | 
			
		||||
     * @param playerIndex Index of the player to auto-map.
 | 
			
		||||
     * @param deviceParams [ParamPackage] representing the device to auto-map as received
 | 
			
		||||
     * from [getInputDevices].
 | 
			
		||||
     * @param displayName Name of the device to auto-map as received from the "display" param in [deviceParams].
 | 
			
		||||
     * Intended to be a way to provide a default name for a controller if the "display" param is empty.
 | 
			
		||||
     */
 | 
			
		||||
    fun updateMappingsWithDefault(
 | 
			
		||||
        playerIndex: Int,
 | 
			
		||||
        deviceParams: ParamPackage,
 | 
			
		||||
        displayName: String
 | 
			
		||||
    ) = updateMappingsWithDefaultImpl(playerIndex, deviceParams.serialize(), displayName)
 | 
			
		||||
 | 
			
		||||
    private external fun updateMappingsWithDefaultImpl(
 | 
			
		||||
        playerIndex: Int,
 | 
			
		||||
        deviceParams: String,
 | 
			
		||||
        displayName: String
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the params for a specific button.
 | 
			
		||||
     * @param playerIndex Index of the player to get params from.
 | 
			
		||||
     * @param button The [NativeButton] to get params for.
 | 
			
		||||
     * @return A [ParamPackage] representing a player's specific button.
 | 
			
		||||
     */
 | 
			
		||||
    fun getButtonParam(playerIndex: Int, button: NativeButton): ParamPackage =
 | 
			
		||||
        ParamPackage(getButtonParamImpl(playerIndex, button.int))
 | 
			
		||||
 | 
			
		||||
    private external fun getButtonParamImpl(playerIndex: Int, buttonId: Int): String
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the params for a specific button.
 | 
			
		||||
     * @param playerIndex Index of the player to set params for.
 | 
			
		||||
     * @param button The [NativeButton] to set params for.
 | 
			
		||||
     * @param param A [ParamPackage] to set.
 | 
			
		||||
     */
 | 
			
		||||
    fun setButtonParam(playerIndex: Int, button: NativeButton, param: ParamPackage) =
 | 
			
		||||
        setButtonParamImpl(playerIndex, button.int, param.serialize())
 | 
			
		||||
 | 
			
		||||
    private external fun setButtonParamImpl(playerIndex: Int, buttonId: Int, param: String)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the params for a specific stick.
 | 
			
		||||
     * @param playerIndex Index of the player to get params from.
 | 
			
		||||
     * @param stick The [NativeAnalog] to get params for.
 | 
			
		||||
     * @return A [ParamPackage] representing a player's specific stick.
 | 
			
		||||
     */
 | 
			
		||||
    fun getStickParam(playerIndex: Int, stick: NativeAnalog): ParamPackage =
 | 
			
		||||
        ParamPackage(getStickParamImpl(playerIndex, stick.int))
 | 
			
		||||
 | 
			
		||||
    private external fun getStickParamImpl(playerIndex: Int, stickId: Int): String
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the params for a specific stick.
 | 
			
		||||
     * @param playerIndex Index of the player to set params for.
 | 
			
		||||
     * @param stick The [NativeAnalog] to set params for.
 | 
			
		||||
     * @param param A [ParamPackage] to set.
 | 
			
		||||
     */
 | 
			
		||||
    fun setStickParam(playerIndex: Int, stick: NativeAnalog, param: ParamPackage) =
 | 
			
		||||
        setStickParamImpl(playerIndex, stick.int, param.serialize())
 | 
			
		||||
 | 
			
		||||
    private external fun setStickParamImpl(playerIndex: Int, stickId: Int, param: String)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the int representation of a [ButtonName]. Tells you what to show as the mapped input for
 | 
			
		||||
     * a button/analog/other.
 | 
			
		||||
     * @param param A [ParamPackage] that represents a specific button's params.
 | 
			
		||||
     * @return The [ButtonName] for [param].
 | 
			
		||||
     */
 | 
			
		||||
    fun getButtonName(param: ParamPackage): ButtonName =
 | 
			
		||||
        ButtonName.from(getButtonNameImpl(param.serialize()))
 | 
			
		||||
 | 
			
		||||
    private external fun getButtonNameImpl(param: String): Int
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets each supported [NpadStyleIndex] for a given player.
 | 
			
		||||
     * @param playerIndex Index of the player to get supported indexes for.
 | 
			
		||||
     * @return List of each supported [NpadStyleIndex].
 | 
			
		||||
     */
 | 
			
		||||
    fun getSupportedStyleTags(playerIndex: Int): List<NpadStyleIndex> =
 | 
			
		||||
        getSupportedStyleTagsImpl(playerIndex).map { NpadStyleIndex.from(it) }
 | 
			
		||||
 | 
			
		||||
    private external fun getSupportedStyleTagsImpl(playerIndex: Int): IntArray
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the [NpadStyleIndex] for a given player.
 | 
			
		||||
     * @param playerIndex Index of the player to get an [NpadStyleIndex] from.
 | 
			
		||||
     * @return The [NpadStyleIndex] for a given player.
 | 
			
		||||
     */
 | 
			
		||||
    fun getStyleIndex(playerIndex: Int): NpadStyleIndex =
 | 
			
		||||
        NpadStyleIndex.from(getStyleIndexImpl(playerIndex))
 | 
			
		||||
 | 
			
		||||
    private external fun getStyleIndexImpl(playerIndex: Int): Int
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the [NpadStyleIndex] for a given player.
 | 
			
		||||
     * @param playerIndex Index of the player to change.
 | 
			
		||||
     * @param style The new style to set.
 | 
			
		||||
     */
 | 
			
		||||
    fun setStyleIndex(playerIndex: Int, style: NpadStyleIndex) =
 | 
			
		||||
        setStyleIndexImpl(playerIndex, style.int)
 | 
			
		||||
 | 
			
		||||
    private external fun setStyleIndexImpl(playerIndex: Int, styleIndex: Int)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if a device is a controller.
 | 
			
		||||
     * @param params [ParamPackage] for an input device retrieved from [getInputDevices]
 | 
			
		||||
     * @return Whether the device is a controller or not.
 | 
			
		||||
     */
 | 
			
		||||
    fun isController(params: ParamPackage): Boolean = isControllerImpl(params.serialize())
 | 
			
		||||
 | 
			
		||||
    private external fun isControllerImpl(params: String): Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if a controller is connected
 | 
			
		||||
     * @param playerIndex Index of the player to check.
 | 
			
		||||
     * @return Whether the player is connected or not.
 | 
			
		||||
     */
 | 
			
		||||
    external fun getIsConnected(playerIndex: Int): Boolean
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Connects/disconnects a controller and ensures that connection order stays in-tact.
 | 
			
		||||
     * @param playerIndex Index of the player to connect/disconnect.
 | 
			
		||||
     * @param connected Whether to connect or disconnect this controller.
 | 
			
		||||
     */
 | 
			
		||||
    fun connectControllers(playerIndex: Int, connected: Boolean = true) {
 | 
			
		||||
        val connectedControllers = mutableListOf<Boolean>().apply {
 | 
			
		||||
            if (connected) {
 | 
			
		||||
                for (i in 0 until 8) {
 | 
			
		||||
                    add(i <= playerIndex)
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                for (i in 0 until 8) {
 | 
			
		||||
                    add(i < playerIndex)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        connectControllersImpl(connectedControllers.toBooleanArray())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private external fun connectControllersImpl(connected: BooleanArray)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Resets all of the button and analog mappings for a player.
 | 
			
		||||
     * @param playerIndex Index of the player that will have its mappings reset.
 | 
			
		||||
     */
 | 
			
		||||
    external fun resetControllerMappings(playerIndex: Int)
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,93 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.input
 | 
			
		||||
 | 
			
		||||
import android.view.InputDevice
 | 
			
		||||
import androidx.annotation.Keep
 | 
			
		||||
import org.yuzu.yuzu_emu.YuzuApplication
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.InputHandler.getGUID
 | 
			
		||||
 | 
			
		||||
@Keep
 | 
			
		||||
interface YuzuInputDevice {
 | 
			
		||||
    fun getName(): String
 | 
			
		||||
 | 
			
		||||
    fun getGUID(): String
 | 
			
		||||
 | 
			
		||||
    fun getPort(): Int
 | 
			
		||||
 | 
			
		||||
    fun getSupportsVibration(): Boolean
 | 
			
		||||
 | 
			
		||||
    fun vibrate(intensity: Float)
 | 
			
		||||
 | 
			
		||||
    fun getAxes(): Array<Int> = arrayOf()
 | 
			
		||||
    fun hasKeys(keys: IntArray): BooleanArray = BooleanArray(0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class YuzuPhysicalDevice(
 | 
			
		||||
    private val device: InputDevice,
 | 
			
		||||
    private val port: Int,
 | 
			
		||||
    useSystemVibrator: Boolean
 | 
			
		||||
) : YuzuInputDevice {
 | 
			
		||||
    private val vibrator = if (useSystemVibrator) {
 | 
			
		||||
        YuzuVibrator.getSystemVibrator()
 | 
			
		||||
    } else {
 | 
			
		||||
        YuzuVibrator.getControllerVibrator(device)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getName(): String {
 | 
			
		||||
        return device.name
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getGUID(): String {
 | 
			
		||||
        return device.getGUID()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getPort(): Int {
 | 
			
		||||
        return port
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getSupportsVibration(): Boolean {
 | 
			
		||||
        return vibrator.supportsVibration()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun vibrate(intensity: Float) {
 | 
			
		||||
        vibrator.vibrate(intensity)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getAxes(): Array<Int> = device.motionRanges.map { it.axis }.toTypedArray()
 | 
			
		||||
    override fun hasKeys(keys: IntArray): BooleanArray = device.hasKeys(*keys)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class YuzuInputOverlayDevice(
 | 
			
		||||
    private val vibration: Boolean,
 | 
			
		||||
    private val port: Int
 | 
			
		||||
) : YuzuInputDevice {
 | 
			
		||||
    private val vibrator = YuzuVibrator.getSystemVibrator()
 | 
			
		||||
 | 
			
		||||
    override fun getName(): String {
 | 
			
		||||
        return YuzuApplication.appContext.getString(R.string.input_overlay)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getGUID(): String {
 | 
			
		||||
        return "00000000000000000000000000000000"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getPort(): Int {
 | 
			
		||||
        return port
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getSupportsVibration(): Boolean {
 | 
			
		||||
        if (vibration) {
 | 
			
		||||
            return vibrator.supportsVibration()
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun vibrate(intensity: Float) {
 | 
			
		||||
        if (vibration) {
 | 
			
		||||
            vibrator.vibrate(intensity)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,76 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.input
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.os.CombinedVibration
 | 
			
		||||
import android.os.VibrationEffect
 | 
			
		||||
import android.os.Vibrator
 | 
			
		||||
import android.os.VibratorManager
 | 
			
		||||
import android.view.InputDevice
 | 
			
		||||
import androidx.annotation.Keep
 | 
			
		||||
import androidx.annotation.RequiresApi
 | 
			
		||||
import org.yuzu.yuzu_emu.YuzuApplication
 | 
			
		||||
 | 
			
		||||
@Keep
 | 
			
		||||
@Suppress("DEPRECATION")
 | 
			
		||||
interface YuzuVibrator {
 | 
			
		||||
    fun supportsVibration(): Boolean
 | 
			
		||||
 | 
			
		||||
    fun vibrate(intensity: Float)
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun getControllerVibrator(device: InputDevice): YuzuVibrator =
 | 
			
		||||
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
 | 
			
		||||
                YuzuVibratorManager(device.vibratorManager)
 | 
			
		||||
            } else {
 | 
			
		||||
                YuzuVibratorManagerCompat(device.vibrator)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        fun getSystemVibrator(): YuzuVibrator =
 | 
			
		||||
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
 | 
			
		||||
                val vibratorManager = YuzuApplication.appContext
 | 
			
		||||
                    .getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
 | 
			
		||||
                YuzuVibratorManager(vibratorManager)
 | 
			
		||||
            } else {
 | 
			
		||||
                val vibrator = YuzuApplication.appContext
 | 
			
		||||
                    .getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
 | 
			
		||||
                YuzuVibratorManagerCompat(vibrator)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        fun getVibrationEffect(intensity: Float): VibrationEffect? {
 | 
			
		||||
            if (intensity > 0f) {
 | 
			
		||||
                return VibrationEffect.createOneShot(
 | 
			
		||||
                    50,
 | 
			
		||||
                    (255.0 * intensity).toInt().coerceIn(1, 255)
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            return null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@RequiresApi(Build.VERSION_CODES.S)
 | 
			
		||||
class YuzuVibratorManager(private val vibratorManager: VibratorManager) : YuzuVibrator {
 | 
			
		||||
    override fun supportsVibration(): Boolean {
 | 
			
		||||
        return vibratorManager.vibratorIds.isNotEmpty()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun vibrate(intensity: Float) {
 | 
			
		||||
        val vibration = YuzuVibrator.getVibrationEffect(intensity) ?: return
 | 
			
		||||
        vibratorManager.vibrate(CombinedVibration.createParallel(vibration))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class YuzuVibratorManagerCompat(private val vibrator: Vibrator) : YuzuVibrator {
 | 
			
		||||
    override fun supportsVibration(): Boolean {
 | 
			
		||||
        return vibrator.hasVibrator()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun vibrate(intensity: Float) {
 | 
			
		||||
        val vibration = YuzuVibrator.getVibrationEffect(intensity) ?: return
 | 
			
		||||
        vibrator.vibrate(vibration)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,11 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.input.model
 | 
			
		||||
 | 
			
		||||
enum class AnalogDirection(val int: Int, val param: String) {
 | 
			
		||||
    Up(0, "up"),
 | 
			
		||||
    Down(1, "down"),
 | 
			
		||||
    Left(2, "left"),
 | 
			
		||||
    Right(3, "right")
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.input.model
 | 
			
		||||
 | 
			
		||||
// Loosely matches the enum in common/input.h
 | 
			
		||||
enum class ButtonName(val int: Int) {
 | 
			
		||||
    Invalid(1),
 | 
			
		||||
 | 
			
		||||
    // This will display the engine name instead of the button name
 | 
			
		||||
    Engine(2),
 | 
			
		||||
 | 
			
		||||
    // This will display the button by value instead of the button name
 | 
			
		||||
    Value(3);
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun from(int: Int): ButtonName = entries.firstOrNull { it.int == int } ?: Invalid
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,13 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.input.model
 | 
			
		||||
 | 
			
		||||
// Must match the corresponding enum in input_common/main.h
 | 
			
		||||
enum class InputType(val int: Int) {
 | 
			
		||||
    None(0),
 | 
			
		||||
    Button(1),
 | 
			
		||||
    Stick(2),
 | 
			
		||||
    Motion(3),
 | 
			
		||||
    Touch(4)
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,14 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.input.model
 | 
			
		||||
 | 
			
		||||
// Must match enum in src/common/settings_input.h
 | 
			
		||||
enum class NativeAnalog(val int: Int) {
 | 
			
		||||
    LStick(0),
 | 
			
		||||
    RStick(1);
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun from(int: Int): NativeAnalog = entries.firstOrNull { it.int == int } ?: LStick
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,38 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.input.model
 | 
			
		||||
 | 
			
		||||
// Must match enum in src/common/settings_input.h
 | 
			
		||||
enum class NativeButton(val int: Int) {
 | 
			
		||||
    A(0),
 | 
			
		||||
    B(1),
 | 
			
		||||
    X(2),
 | 
			
		||||
    Y(3),
 | 
			
		||||
    LStick(4),
 | 
			
		||||
    RStick(5),
 | 
			
		||||
    L(6),
 | 
			
		||||
    R(7),
 | 
			
		||||
    ZL(8),
 | 
			
		||||
    ZR(9),
 | 
			
		||||
    Plus(10),
 | 
			
		||||
    Minus(11),
 | 
			
		||||
 | 
			
		||||
    DLeft(12),
 | 
			
		||||
    DUp(13),
 | 
			
		||||
    DRight(14),
 | 
			
		||||
    DDown(15),
 | 
			
		||||
 | 
			
		||||
    SLLeft(16),
 | 
			
		||||
    SRLeft(17),
 | 
			
		||||
 | 
			
		||||
    Home(18),
 | 
			
		||||
    Capture(19),
 | 
			
		||||
 | 
			
		||||
    SLRight(20),
 | 
			
		||||
    SRRight(21);
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun from(int: Int): NativeButton = entries.firstOrNull { it.int == int } ?: A
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,10 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.input.model
 | 
			
		||||
 | 
			
		||||
// Must match enum in src/common/settings_input.h
 | 
			
		||||
enum class NativeTrigger(val int: Int) {
 | 
			
		||||
    LTrigger(0),
 | 
			
		||||
    RTrigger(1)
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.input.model
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.StringRes
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
 | 
			
		||||
// Must match enum in src/core/hid/hid_types.h
 | 
			
		||||
enum class NpadStyleIndex(val int: Int, @StringRes val nameId: Int = 0) {
 | 
			
		||||
    None(0),
 | 
			
		||||
    Fullkey(3, R.string.pro_controller),
 | 
			
		||||
    Handheld(4, R.string.handheld),
 | 
			
		||||
    HandheldNES(4),
 | 
			
		||||
    JoyconDual(5, R.string.dual_joycons),
 | 
			
		||||
    JoyconLeft(6, R.string.left_joycon),
 | 
			
		||||
    JoyconRight(7, R.string.right_joycon),
 | 
			
		||||
    GameCube(8, R.string.gamecube_controller),
 | 
			
		||||
    Pokeball(9),
 | 
			
		||||
    NES(10),
 | 
			
		||||
    SNES(12),
 | 
			
		||||
    N64(13),
 | 
			
		||||
    SegaGenesis(14),
 | 
			
		||||
    SystemExt(32),
 | 
			
		||||
    System(33);
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun from(int: Int): NpadStyleIndex = entries.firstOrNull { it.int == int } ?: None
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,83 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.input.model
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Keep
 | 
			
		||||
 | 
			
		||||
@Keep
 | 
			
		||||
data class PlayerInput(
 | 
			
		||||
    var connected: Boolean,
 | 
			
		||||
    var buttons: Array<String>,
 | 
			
		||||
    var analogs: Array<String>,
 | 
			
		||||
    var motions: Array<String>,
 | 
			
		||||
 | 
			
		||||
    var vibrationEnabled: Boolean,
 | 
			
		||||
    var vibrationStrength: Int,
 | 
			
		||||
 | 
			
		||||
    var bodyColorLeft: Long,
 | 
			
		||||
    var bodyColorRight: Long,
 | 
			
		||||
    var buttonColorLeft: Long,
 | 
			
		||||
    var buttonColorRight: Long,
 | 
			
		||||
    var profileName: String,
 | 
			
		||||
 | 
			
		||||
    var useSystemVibrator: Boolean
 | 
			
		||||
) {
 | 
			
		||||
    // It's recommended to use the generated equals() and hashCode() methods
 | 
			
		||||
    // when using arrays in a data class
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        if (this === other) return true
 | 
			
		||||
        if (javaClass != other?.javaClass) return false
 | 
			
		||||
 | 
			
		||||
        other as PlayerInput
 | 
			
		||||
 | 
			
		||||
        if (connected != other.connected) return false
 | 
			
		||||
        if (!buttons.contentEquals(other.buttons)) return false
 | 
			
		||||
        if (!analogs.contentEquals(other.analogs)) return false
 | 
			
		||||
        if (!motions.contentEquals(other.motions)) return false
 | 
			
		||||
        if (vibrationEnabled != other.vibrationEnabled) return false
 | 
			
		||||
        if (vibrationStrength != other.vibrationStrength) return false
 | 
			
		||||
        if (bodyColorLeft != other.bodyColorLeft) return false
 | 
			
		||||
        if (bodyColorRight != other.bodyColorRight) return false
 | 
			
		||||
        if (buttonColorLeft != other.buttonColorLeft) return false
 | 
			
		||||
        if (buttonColorRight != other.buttonColorRight) return false
 | 
			
		||||
        if (profileName != other.profileName) return false
 | 
			
		||||
        return useSystemVibrator == other.useSystemVibrator
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        var result = connected.hashCode()
 | 
			
		||||
        result = 31 * result + buttons.contentHashCode()
 | 
			
		||||
        result = 31 * result + analogs.contentHashCode()
 | 
			
		||||
        result = 31 * result + motions.contentHashCode()
 | 
			
		||||
        result = 31 * result + vibrationEnabled.hashCode()
 | 
			
		||||
        result = 31 * result + vibrationStrength
 | 
			
		||||
        result = 31 * result + bodyColorLeft.hashCode()
 | 
			
		||||
        result = 31 * result + bodyColorRight.hashCode()
 | 
			
		||||
        result = 31 * result + buttonColorLeft.hashCode()
 | 
			
		||||
        result = 31 * result + buttonColorRight.hashCode()
 | 
			
		||||
        result = 31 * result + profileName.hashCode()
 | 
			
		||||
        result = 31 * result + useSystemVibrator.hashCode()
 | 
			
		||||
        return result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun hasMapping(): Boolean {
 | 
			
		||||
        var hasMapping = false
 | 
			
		||||
        buttons.forEach {
 | 
			
		||||
            if (it != "[empty]") {
 | 
			
		||||
                hasMapping = true
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        analogs.forEach {
 | 
			
		||||
            if (it != "[empty]") {
 | 
			
		||||
                hasMapping = true
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        motions.forEach {
 | 
			
		||||
            if (it != "[empty]") {
 | 
			
		||||
                hasMapping = true
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return hasMapping
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,17 +4,30 @@
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.model
 | 
			
		||||
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
import org.yuzu.yuzu_emu.YuzuApplication
 | 
			
		||||
 | 
			
		||||
object Settings {
 | 
			
		||||
    enum class MenuTag(val titleId: Int) {
 | 
			
		||||
    enum class MenuTag(val titleId: Int = 0) {
 | 
			
		||||
        SECTION_ROOT(R.string.advanced_settings),
 | 
			
		||||
        SECTION_SYSTEM(R.string.preferences_system),
 | 
			
		||||
        SECTION_RENDERER(R.string.preferences_graphics),
 | 
			
		||||
        SECTION_AUDIO(R.string.preferences_audio),
 | 
			
		||||
        SECTION_INPUT(R.string.preferences_controls),
 | 
			
		||||
        SECTION_INPUT_PLAYER_ONE,
 | 
			
		||||
        SECTION_INPUT_PLAYER_TWO,
 | 
			
		||||
        SECTION_INPUT_PLAYER_THREE,
 | 
			
		||||
        SECTION_INPUT_PLAYER_FOUR,
 | 
			
		||||
        SECTION_INPUT_PLAYER_FIVE,
 | 
			
		||||
        SECTION_INPUT_PLAYER_SIX,
 | 
			
		||||
        SECTION_INPUT_PLAYER_SEVEN,
 | 
			
		||||
        SECTION_INPUT_PLAYER_EIGHT,
 | 
			
		||||
        SECTION_THEME(R.string.preferences_theme),
 | 
			
		||||
        SECTION_DEBUG(R.string.preferences_debug);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getPlayerString(player: Int): String =
 | 
			
		||||
        YuzuApplication.appContext.getString(R.string.preferences_player, player)
 | 
			
		||||
 | 
			
		||||
    const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
 | 
			
		||||
    const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,31 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.model.view
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.StringRes
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.InputType
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.ParamPackage
 | 
			
		||||
 | 
			
		||||
class AnalogInputSetting(
 | 
			
		||||
    override val playerIndex: Int,
 | 
			
		||||
    val nativeAnalog: NativeAnalog,
 | 
			
		||||
    val analogDirection: AnalogDirection,
 | 
			
		||||
    @StringRes titleId: Int = 0,
 | 
			
		||||
    titleString: String = ""
 | 
			
		||||
) : InputSetting(titleId, titleString) {
 | 
			
		||||
    override val type = TYPE_INPUT
 | 
			
		||||
    override val inputType = InputType.Stick
 | 
			
		||||
 | 
			
		||||
    override fun getSelectedValue(): String {
 | 
			
		||||
        val params = NativeInput.getStickParam(playerIndex, nativeAnalog)
 | 
			
		||||
        val analog = analogToText(params, analogDirection.param)
 | 
			
		||||
        return getDisplayString(params, analog)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun setSelectedValue(param: ParamPackage) =
 | 
			
		||||
        NativeInput.setStickParam(playerIndex, nativeAnalog, param)
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,29 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.model.view
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.StringRes
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.ParamPackage
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.InputType
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.NativeButton
 | 
			
		||||
 | 
			
		||||
class ButtonInputSetting(
 | 
			
		||||
    override val playerIndex: Int,
 | 
			
		||||
    val nativeButton: NativeButton,
 | 
			
		||||
    @StringRes titleId: Int = 0,
 | 
			
		||||
    titleString: String = ""
 | 
			
		||||
) : InputSetting(titleId, titleString) {
 | 
			
		||||
    override val type = TYPE_INPUT
 | 
			
		||||
    override val inputType = InputType.Button
 | 
			
		||||
 | 
			
		||||
    override fun getSelectedValue(): String {
 | 
			
		||||
        val params = NativeInput.getButtonParam(playerIndex, nativeButton)
 | 
			
		||||
        val button = buttonToText(params)
 | 
			
		||||
        return getDisplayString(params, button)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun setSelectedValue(param: ParamPackage) =
 | 
			
		||||
        NativeInput.setButtonParam(playerIndex, nativeButton, param)
 | 
			
		||||
}
 | 
			
		||||
@@ -3,13 +3,16 @@
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.model.view
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.StringRes
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractLongSetting
 | 
			
		||||
 | 
			
		||||
class DateTimeSetting(
 | 
			
		||||
    private val longSetting: AbstractLongSetting,
 | 
			
		||||
    titleId: Int,
 | 
			
		||||
    descriptionId: Int
 | 
			
		||||
) : SettingsItem(longSetting, titleId, descriptionId) {
 | 
			
		||||
    @StringRes titleId: Int = 0,
 | 
			
		||||
    titleString: String = "",
 | 
			
		||||
    @StringRes descriptionId: Int = 0,
 | 
			
		||||
    descriptionString: String = ""
 | 
			
		||||
) : SettingsItem(longSetting, titleId, titleString, descriptionId, descriptionString) {
 | 
			
		||||
    override val type = TYPE_DATETIME_SETTING
 | 
			
		||||
 | 
			
		||||
    fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal)
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,11 @@
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.model.view
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.StringRes
 | 
			
		||||
 | 
			
		||||
class HeaderSetting(
 | 
			
		||||
    titleId: Int
 | 
			
		||||
) : SettingsItem(emptySetting, titleId, 0) {
 | 
			
		||||
    @StringRes titleId: Int = 0,
 | 
			
		||||
    titleString: String = ""
 | 
			
		||||
) : SettingsItem(emptySetting, titleId, titleString, 0, "") {
 | 
			
		||||
    override val type = TYPE_HEADER
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,32 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.model.view
 | 
			
		||||
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.NativeConfig
 | 
			
		||||
 | 
			
		||||
class InputProfileSetting(private val playerIndex: Int) :
 | 
			
		||||
    SettingsItem(emptySetting, R.string.profile, "", 0, "") {
 | 
			
		||||
    override val type = TYPE_INPUT_PROFILE
 | 
			
		||||
 | 
			
		||||
    fun getCurrentProfile(): String =
 | 
			
		||||
        NativeConfig.getInputSettings(true)[playerIndex].profileName
 | 
			
		||||
 | 
			
		||||
    fun getProfileNames(): Array<String> = NativeInput.getInputProfileNames()
 | 
			
		||||
 | 
			
		||||
    fun isProfileNameValid(name: String): Boolean = NativeInput.isProfileNameValid(name)
 | 
			
		||||
 | 
			
		||||
    fun createProfile(name: String): Boolean = NativeInput.createProfile(name, playerIndex)
 | 
			
		||||
 | 
			
		||||
    fun deleteProfile(name: String): Boolean = NativeInput.deleteProfile(name, playerIndex)
 | 
			
		||||
 | 
			
		||||
    fun loadProfile(name: String): Boolean {
 | 
			
		||||
        val result = NativeInput.loadProfile(name, playerIndex)
 | 
			
		||||
        NativeInput.reloadInputDevices()
 | 
			
		||||
        return result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun saveProfile(name: String): Boolean = NativeInput.saveProfile(name, playerIndex)
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,134 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.model.view
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.StringRes
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
import org.yuzu.yuzu_emu.YuzuApplication
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.ButtonName
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.InputType
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.ParamPackage
 | 
			
		||||
 | 
			
		||||
sealed class InputSetting(
 | 
			
		||||
    @StringRes titleId: Int,
 | 
			
		||||
    titleString: String
 | 
			
		||||
) : SettingsItem(emptySetting, titleId, titleString, 0, "") {
 | 
			
		||||
    override val type = TYPE_INPUT
 | 
			
		||||
    abstract val inputType: InputType
 | 
			
		||||
    abstract val playerIndex: Int
 | 
			
		||||
 | 
			
		||||
    protected val context get() = YuzuApplication.appContext
 | 
			
		||||
 | 
			
		||||
    abstract fun getSelectedValue(): String
 | 
			
		||||
 | 
			
		||||
    abstract fun setSelectedValue(param: ParamPackage)
 | 
			
		||||
 | 
			
		||||
    protected fun getDisplayString(params: ParamPackage, control: String): String {
 | 
			
		||||
        val deviceName = params.get("display", "")
 | 
			
		||||
        deviceName.ifEmpty {
 | 
			
		||||
            return context.getString(R.string.not_set)
 | 
			
		||||
        }
 | 
			
		||||
        return "$deviceName: $control"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getDirectionName(direction: String): String =
 | 
			
		||||
        when (direction) {
 | 
			
		||||
            "up" -> context.getString(R.string.up)
 | 
			
		||||
            "down" -> context.getString(R.string.down)
 | 
			
		||||
            "left" -> context.getString(R.string.left)
 | 
			
		||||
            "right" -> context.getString(R.string.right)
 | 
			
		||||
            else -> direction
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    protected fun buttonToText(param: ParamPackage): String {
 | 
			
		||||
        if (!param.has("engine")) {
 | 
			
		||||
            return context.getString(R.string.not_set)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val toggle = if (param.get("toggle", false)) "~" else ""
 | 
			
		||||
        val inverted = if (param.get("inverted", false)) "!" else ""
 | 
			
		||||
        val invert = if (param.get("invert", "+") == "-") "-" else ""
 | 
			
		||||
        val turbo = if (param.get("turbo", false)) "$" else ""
 | 
			
		||||
        val commonButtonName = NativeInput.getButtonName(param)
 | 
			
		||||
 | 
			
		||||
        if (commonButtonName == ButtonName.Invalid) {
 | 
			
		||||
            return context.getString(R.string.invalid)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (commonButtonName == ButtonName.Engine) {
 | 
			
		||||
            return param.get("engine", "")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (commonButtonName == ButtonName.Value) {
 | 
			
		||||
            if (param.has("hat")) {
 | 
			
		||||
                val hat = getDirectionName(param.get("direction", ""))
 | 
			
		||||
                return context.getString(R.string.qualified_hat, turbo, toggle, inverted, hat)
 | 
			
		||||
            }
 | 
			
		||||
            if (param.has("axis")) {
 | 
			
		||||
                val axis = param.get("axis", "")
 | 
			
		||||
                return context.getString(
 | 
			
		||||
                    R.string.qualified_button_stick_axis,
 | 
			
		||||
                    toggle,
 | 
			
		||||
                    inverted,
 | 
			
		||||
                    invert,
 | 
			
		||||
                    axis
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            if (param.has("button")) {
 | 
			
		||||
                val button = param.get("button", "")
 | 
			
		||||
                return context.getString(R.string.qualified_button, turbo, toggle, inverted, button)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return context.getString(R.string.unknown)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected fun analogToText(param: ParamPackage, direction: String): String {
 | 
			
		||||
        if (!param.has("engine")) {
 | 
			
		||||
            return context.getString(R.string.not_set)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (param.get("engine", "") == "analog_from_button") {
 | 
			
		||||
            return buttonToText(ParamPackage(param.get(direction, "")))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!param.has("axis_x") || !param.has("axis_y")) {
 | 
			
		||||
            return context.getString(R.string.unknown)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val xAxis = param.get("axis_x", "")
 | 
			
		||||
        val yAxis = param.get("axis_y", "")
 | 
			
		||||
        val xInvert = param.get("invert_x", "+") == "-"
 | 
			
		||||
        val yInvert = param.get("invert_y", "+") == "-"
 | 
			
		||||
 | 
			
		||||
        if (direction == "modifier") {
 | 
			
		||||
            return context.getString(R.string.unused)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        when (direction) {
 | 
			
		||||
            "up" -> {
 | 
			
		||||
                val yInvertString = if (yInvert) "+" else "-"
 | 
			
		||||
                return context.getString(R.string.qualified_axis, yAxis, yInvertString)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            "down" -> {
 | 
			
		||||
                val yInvertString = if (yInvert) "-" else "+"
 | 
			
		||||
                return context.getString(R.string.qualified_axis, yAxis, yInvertString)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            "left" -> {
 | 
			
		||||
                val xInvertString = if (xInvert) "+" else "-"
 | 
			
		||||
                return context.getString(R.string.qualified_axis, xAxis, xInvertString)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            "right" -> {
 | 
			
		||||
                val xInvertString = if (xInvert) "-" else "+"
 | 
			
		||||
                return context.getString(R.string.qualified_axis, xAxis, xInvertString)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return context.getString(R.string.unknown)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,38 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.model.view
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.StringRes
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
 | 
			
		||||
 | 
			
		||||
class IntSingleChoiceSetting(
 | 
			
		||||
    private val intSetting: AbstractIntSetting,
 | 
			
		||||
    @StringRes titleId: Int = 0,
 | 
			
		||||
    titleString: String = "",
 | 
			
		||||
    @StringRes descriptionId: Int = 0,
 | 
			
		||||
    descriptionString: String = "",
 | 
			
		||||
    val choices: Array<String>,
 | 
			
		||||
    val values: Array<Int>
 | 
			
		||||
) : SettingsItem(intSetting, titleId, titleString, descriptionId, descriptionString) {
 | 
			
		||||
    override val type = TYPE_INT_SINGLE_CHOICE
 | 
			
		||||
 | 
			
		||||
    fun getValueAt(index: Int): Int =
 | 
			
		||||
        if (values.indices.contains(index)) values[index] else -1
 | 
			
		||||
 | 
			
		||||
    fun getChoiceAt(index: Int): String =
 | 
			
		||||
        if (choices.indices.contains(index)) choices[index] else ""
 | 
			
		||||
 | 
			
		||||
    fun getSelectedValue(needsGlobal: Boolean = false) = intSetting.getInt(needsGlobal)
 | 
			
		||||
    fun setSelectedValue(value: Int) = intSetting.setInt(value)
 | 
			
		||||
 | 
			
		||||
    val selectedValueIndex: Int
 | 
			
		||||
        get() {
 | 
			
		||||
            for (i in values.indices) {
 | 
			
		||||
                if (values[i] == getSelectedValue()) {
 | 
			
		||||
                    return i
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return -1
 | 
			
		||||
        }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,31 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.model.view
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.StringRes
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.InputType
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.ParamPackage
 | 
			
		||||
 | 
			
		||||
class ModifierInputSetting(
 | 
			
		||||
    override val playerIndex: Int,
 | 
			
		||||
    val nativeAnalog: NativeAnalog,
 | 
			
		||||
    @StringRes titleId: Int = 0,
 | 
			
		||||
    titleString: String = ""
 | 
			
		||||
) : InputSetting(titleId, titleString) {
 | 
			
		||||
    override val inputType = InputType.Button
 | 
			
		||||
 | 
			
		||||
    override fun getSelectedValue(): String {
 | 
			
		||||
        val analogParam = NativeInput.getStickParam(playerIndex, nativeAnalog)
 | 
			
		||||
        val modifierParam = ParamPackage(analogParam.get("modifier", ""))
 | 
			
		||||
        return buttonToText(modifierParam)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun setSelectedValue(param: ParamPackage) {
 | 
			
		||||
        val newParam = NativeInput.getStickParam(playerIndex, nativeAnalog)
 | 
			
		||||
        newParam.set("modifier", param.serialize())
 | 
			
		||||
        NativeInput.setStickParam(playerIndex, nativeAnalog, newParam)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,13 +4,16 @@
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.model.view
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.DrawableRes
 | 
			
		||||
import androidx.annotation.StringRes
 | 
			
		||||
 | 
			
		||||
class RunnableSetting(
 | 
			
		||||
    titleId: Int,
 | 
			
		||||
    descriptionId: Int,
 | 
			
		||||
    val isRuntimeRunnable: Boolean,
 | 
			
		||||
    @StringRes titleId: Int = 0,
 | 
			
		||||
    titleString: String = "",
 | 
			
		||||
    @StringRes descriptionId: Int = 0,
 | 
			
		||||
    descriptionString: String = "",
 | 
			
		||||
    val isRunnable: Boolean,
 | 
			
		||||
    @DrawableRes val iconId: Int = 0,
 | 
			
		||||
    val runnable: () -> Unit
 | 
			
		||||
) : SettingsItem(emptySetting, titleId, descriptionId) {
 | 
			
		||||
) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) {
 | 
			
		||||
    override val type = TYPE_RUNNABLE
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,12 @@
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.model.view
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.StringRes
 | 
			
		||||
import org.yuzu.yuzu_emu.NativeLibrary
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
import org.yuzu.yuzu_emu.YuzuApplication
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
 | 
			
		||||
@@ -23,13 +27,34 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
 | 
			
		||||
 */
 | 
			
		||||
abstract class SettingsItem(
 | 
			
		||||
    val setting: AbstractSetting,
 | 
			
		||||
    val nameId: Int,
 | 
			
		||||
    val descriptionId: Int
 | 
			
		||||
    @StringRes val titleId: Int,
 | 
			
		||||
    val titleString: String,
 | 
			
		||||
    @StringRes val descriptionId: Int,
 | 
			
		||||
    val descriptionString: String
 | 
			
		||||
) {
 | 
			
		||||
    abstract val type: Int
 | 
			
		||||
 | 
			
		||||
    val title: String by lazy {
 | 
			
		||||
        if (titleId != 0) {
 | 
			
		||||
            return@lazy YuzuApplication.appContext.getString(titleId)
 | 
			
		||||
        }
 | 
			
		||||
        return@lazy titleString
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val description: String by lazy {
 | 
			
		||||
        if (descriptionId != 0) {
 | 
			
		||||
            return@lazy YuzuApplication.appContext.getString(descriptionId)
 | 
			
		||||
        }
 | 
			
		||||
        return@lazy descriptionString
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val isEditable: Boolean
 | 
			
		||||
        get() {
 | 
			
		||||
            // Can't change docked mode toggle when using handheld mode
 | 
			
		||||
            if (setting.key == BooleanSetting.USE_DOCKED_MODE.key) {
 | 
			
		||||
                return NativeInput.getStyleIndex(0) != NpadStyleIndex.Handheld
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Can't edit settings that aren't saveable in per-game config even if they are switchable
 | 
			
		||||
            if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) {
 | 
			
		||||
                return false
 | 
			
		||||
@@ -59,6 +84,9 @@ abstract class SettingsItem(
 | 
			
		||||
        const val TYPE_STRING_SINGLE_CHOICE = 5
 | 
			
		||||
        const val TYPE_DATETIME_SETTING = 6
 | 
			
		||||
        const val TYPE_RUNNABLE = 7
 | 
			
		||||
        const val TYPE_INPUT = 8
 | 
			
		||||
        const val TYPE_INT_SINGLE_CHOICE = 9
 | 
			
		||||
        const val TYPE_INPUT_PROFILE = 10
 | 
			
		||||
 | 
			
		||||
        const val FASTMEM_COMBINED = "fastmem_combined"
 | 
			
		||||
 | 
			
		||||
@@ -80,237 +108,242 @@ abstract class SettingsItem(
 | 
			
		||||
            put(
 | 
			
		||||
                SwitchSetting(
 | 
			
		||||
                    BooleanSetting.RENDERER_USE_SPEED_LIMIT,
 | 
			
		||||
                    R.string.frame_limit_enable,
 | 
			
		||||
                    R.string.frame_limit_enable_description
 | 
			
		||||
                    titleId = R.string.frame_limit_enable,
 | 
			
		||||
                    descriptionId = R.string.frame_limit_enable_description
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SliderSetting(
 | 
			
		||||
                    ShortSetting.RENDERER_SPEED_LIMIT,
 | 
			
		||||
                    R.string.frame_limit_slider,
 | 
			
		||||
                    R.string.frame_limit_slider_description,
 | 
			
		||||
                    1,
 | 
			
		||||
                    400,
 | 
			
		||||
                    "%"
 | 
			
		||||
                    titleId = R.string.frame_limit_slider,
 | 
			
		||||
                    descriptionId = R.string.frame_limit_slider_description,
 | 
			
		||||
                    min = 1,
 | 
			
		||||
                    max = 400,
 | 
			
		||||
                    units = "%"
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SingleChoiceSetting(
 | 
			
		||||
                    IntSetting.CPU_BACKEND,
 | 
			
		||||
                    R.string.cpu_backend,
 | 
			
		||||
                    0,
 | 
			
		||||
                    R.array.cpuBackendArm64Names,
 | 
			
		||||
                    R.array.cpuBackendArm64Values
 | 
			
		||||
                    titleId = R.string.cpu_backend,
 | 
			
		||||
                    choicesId = R.array.cpuBackendArm64Names,
 | 
			
		||||
                    valuesId = R.array.cpuBackendArm64Values
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SingleChoiceSetting(
 | 
			
		||||
                    IntSetting.CPU_ACCURACY,
 | 
			
		||||
                    R.string.cpu_accuracy,
 | 
			
		||||
                    0,
 | 
			
		||||
                    R.array.cpuAccuracyNames,
 | 
			
		||||
                    R.array.cpuAccuracyValues
 | 
			
		||||
                    titleId = R.string.cpu_accuracy,
 | 
			
		||||
                    choicesId = R.array.cpuAccuracyNames,
 | 
			
		||||
                    valuesId = R.array.cpuAccuracyValues
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SwitchSetting(
 | 
			
		||||
                    BooleanSetting.PICTURE_IN_PICTURE,
 | 
			
		||||
                    R.string.picture_in_picture,
 | 
			
		||||
                    R.string.picture_in_picture_description
 | 
			
		||||
                    titleId = R.string.picture_in_picture,
 | 
			
		||||
                    descriptionId = R.string.picture_in_picture_description
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            val dockedModeSetting = object : AbstractBooleanSetting {
 | 
			
		||||
                override val key = BooleanSetting.USE_DOCKED_MODE.key
 | 
			
		||||
 | 
			
		||||
                override fun getBoolean(needsGlobal: Boolean): Boolean {
 | 
			
		||||
                    if (NativeInput.getStyleIndex(0) == NpadStyleIndex.Handheld) {
 | 
			
		||||
                        return false
 | 
			
		||||
                    }
 | 
			
		||||
                    return BooleanSetting.USE_DOCKED_MODE.getBoolean(needsGlobal)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun setBoolean(value: Boolean) =
 | 
			
		||||
                    BooleanSetting.USE_DOCKED_MODE.setBoolean(value)
 | 
			
		||||
 | 
			
		||||
                override val defaultValue = BooleanSetting.USE_DOCKED_MODE.defaultValue
 | 
			
		||||
 | 
			
		||||
                override fun getValueAsString(needsGlobal: Boolean): String =
 | 
			
		||||
                    BooleanSetting.USE_DOCKED_MODE.getValueAsString(needsGlobal)
 | 
			
		||||
 | 
			
		||||
                override fun reset() = BooleanSetting.USE_DOCKED_MODE.reset()
 | 
			
		||||
            }
 | 
			
		||||
            put(
 | 
			
		||||
                SwitchSetting(
 | 
			
		||||
                    BooleanSetting.USE_DOCKED_MODE,
 | 
			
		||||
                    R.string.use_docked_mode,
 | 
			
		||||
                    R.string.use_docked_mode_description
 | 
			
		||||
                    dockedModeSetting,
 | 
			
		||||
                    titleId = R.string.use_docked_mode,
 | 
			
		||||
                    descriptionId = R.string.use_docked_mode_description
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            put(
 | 
			
		||||
                SingleChoiceSetting(
 | 
			
		||||
                    IntSetting.REGION_INDEX,
 | 
			
		||||
                    R.string.emulated_region,
 | 
			
		||||
                    0,
 | 
			
		||||
                    R.array.regionNames,
 | 
			
		||||
                    R.array.regionValues
 | 
			
		||||
                    titleId = R.string.emulated_region,
 | 
			
		||||
                    choicesId = R.array.regionNames,
 | 
			
		||||
                    valuesId = R.array.regionValues
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SingleChoiceSetting(
 | 
			
		||||
                    IntSetting.LANGUAGE_INDEX,
 | 
			
		||||
                    R.string.emulated_language,
 | 
			
		||||
                    0,
 | 
			
		||||
                    R.array.languageNames,
 | 
			
		||||
                    R.array.languageValues
 | 
			
		||||
                    titleId = R.string.emulated_language,
 | 
			
		||||
                    choicesId = R.array.languageNames,
 | 
			
		||||
                    valuesId = R.array.languageValues
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SwitchSetting(
 | 
			
		||||
                    BooleanSetting.USE_CUSTOM_RTC,
 | 
			
		||||
                    R.string.use_custom_rtc,
 | 
			
		||||
                    R.string.use_custom_rtc_description
 | 
			
		||||
                    titleId = R.string.use_custom_rtc,
 | 
			
		||||
                    descriptionId = R.string.use_custom_rtc_description
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(DateTimeSetting(LongSetting.CUSTOM_RTC, R.string.set_custom_rtc, 0))
 | 
			
		||||
            put(DateTimeSetting(LongSetting.CUSTOM_RTC, titleId = R.string.set_custom_rtc))
 | 
			
		||||
            put(
 | 
			
		||||
                SingleChoiceSetting(
 | 
			
		||||
                    IntSetting.RENDERER_ACCURACY,
 | 
			
		||||
                    R.string.renderer_accuracy,
 | 
			
		||||
                    0,
 | 
			
		||||
                    R.array.rendererAccuracyNames,
 | 
			
		||||
                    R.array.rendererAccuracyValues
 | 
			
		||||
                    titleId = R.string.renderer_accuracy,
 | 
			
		||||
                    choicesId = R.array.rendererAccuracyNames,
 | 
			
		||||
                    valuesId = R.array.rendererAccuracyValues
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SingleChoiceSetting(
 | 
			
		||||
                    IntSetting.RENDERER_RESOLUTION,
 | 
			
		||||
                    R.string.renderer_resolution,
 | 
			
		||||
                    0,
 | 
			
		||||
                    R.array.rendererResolutionNames,
 | 
			
		||||
                    R.array.rendererResolutionValues
 | 
			
		||||
                    titleId = R.string.renderer_resolution,
 | 
			
		||||
                    choicesId = R.array.rendererResolutionNames,
 | 
			
		||||
                    valuesId = R.array.rendererResolutionValues
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SingleChoiceSetting(
 | 
			
		||||
                    IntSetting.RENDERER_VSYNC,
 | 
			
		||||
                    R.string.renderer_vsync,
 | 
			
		||||
                    0,
 | 
			
		||||
                    R.array.rendererVSyncNames,
 | 
			
		||||
                    R.array.rendererVSyncValues
 | 
			
		||||
                    titleId = R.string.renderer_vsync,
 | 
			
		||||
                    choicesId = R.array.rendererVSyncNames,
 | 
			
		||||
                    valuesId = R.array.rendererVSyncValues
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SingleChoiceSetting(
 | 
			
		||||
                    IntSetting.RENDERER_SCALING_FILTER,
 | 
			
		||||
                    R.string.renderer_scaling_filter,
 | 
			
		||||
                    0,
 | 
			
		||||
                    R.array.rendererScalingFilterNames,
 | 
			
		||||
                    R.array.rendererScalingFilterValues
 | 
			
		||||
                    titleId = R.string.renderer_scaling_filter,
 | 
			
		||||
                    choicesId = R.array.rendererScalingFilterNames,
 | 
			
		||||
                    valuesId = R.array.rendererScalingFilterValues
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SliderSetting(
 | 
			
		||||
                    IntSetting.FSR_SHARPENING_SLIDER,
 | 
			
		||||
                    R.string.fsr_sharpness,
 | 
			
		||||
                    R.string.fsr_sharpness_description,
 | 
			
		||||
                    0,
 | 
			
		||||
                    100,
 | 
			
		||||
                    "%"
 | 
			
		||||
                    titleId = R.string.fsr_sharpness,
 | 
			
		||||
                    descriptionId = R.string.fsr_sharpness_description,
 | 
			
		||||
                    units = "%"
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SingleChoiceSetting(
 | 
			
		||||
                    IntSetting.RENDERER_ANTI_ALIASING,
 | 
			
		||||
                    R.string.renderer_anti_aliasing,
 | 
			
		||||
                    0,
 | 
			
		||||
                    R.array.rendererAntiAliasingNames,
 | 
			
		||||
                    R.array.rendererAntiAliasingValues
 | 
			
		||||
                    titleId = R.string.renderer_anti_aliasing,
 | 
			
		||||
                    choicesId = R.array.rendererAntiAliasingNames,
 | 
			
		||||
                    valuesId = R.array.rendererAntiAliasingValues
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SingleChoiceSetting(
 | 
			
		||||
                    IntSetting.RENDERER_SCREEN_LAYOUT,
 | 
			
		||||
                    R.string.renderer_screen_layout,
 | 
			
		||||
                    0,
 | 
			
		||||
                    R.array.rendererScreenLayoutNames,
 | 
			
		||||
                    R.array.rendererScreenLayoutValues
 | 
			
		||||
                    titleId = R.string.renderer_screen_layout,
 | 
			
		||||
                    choicesId = R.array.rendererScreenLayoutNames,
 | 
			
		||||
                    valuesId = R.array.rendererScreenLayoutValues
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SingleChoiceSetting(
 | 
			
		||||
                    IntSetting.RENDERER_ASPECT_RATIO,
 | 
			
		||||
                    R.string.renderer_aspect_ratio,
 | 
			
		||||
                    0,
 | 
			
		||||
                    R.array.rendererAspectRatioNames,
 | 
			
		||||
                    R.array.rendererAspectRatioValues
 | 
			
		||||
                    titleId = R.string.renderer_aspect_ratio,
 | 
			
		||||
                    choicesId = R.array.rendererAspectRatioNames,
 | 
			
		||||
                    valuesId = R.array.rendererAspectRatioValues
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SingleChoiceSetting(
 | 
			
		||||
                    IntSetting.VERTICAL_ALIGNMENT,
 | 
			
		||||
                    R.string.vertical_alignment,
 | 
			
		||||
                    0,
 | 
			
		||||
                    R.array.verticalAlignmentEntries,
 | 
			
		||||
                    R.array.verticalAlignmentValues
 | 
			
		||||
                    titleId = R.string.vertical_alignment,
 | 
			
		||||
                    descriptionId = 0,
 | 
			
		||||
                    choicesId = R.array.verticalAlignmentEntries,
 | 
			
		||||
                    valuesId = R.array.verticalAlignmentValues
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SwitchSetting(
 | 
			
		||||
                    BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE,
 | 
			
		||||
                    R.string.use_disk_shader_cache,
 | 
			
		||||
                    R.string.use_disk_shader_cache_description
 | 
			
		||||
                    titleId = R.string.use_disk_shader_cache,
 | 
			
		||||
                    descriptionId = R.string.use_disk_shader_cache_description
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SwitchSetting(
 | 
			
		||||
                    BooleanSetting.RENDERER_FORCE_MAX_CLOCK,
 | 
			
		||||
                    R.string.renderer_force_max_clock,
 | 
			
		||||
                    R.string.renderer_force_max_clock_description
 | 
			
		||||
                    titleId = R.string.renderer_force_max_clock,
 | 
			
		||||
                    descriptionId = R.string.renderer_force_max_clock_description
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SwitchSetting(
 | 
			
		||||
                    BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS,
 | 
			
		||||
                    R.string.renderer_asynchronous_shaders,
 | 
			
		||||
                    R.string.renderer_asynchronous_shaders_description
 | 
			
		||||
                    titleId = R.string.renderer_asynchronous_shaders,
 | 
			
		||||
                    descriptionId = R.string.renderer_asynchronous_shaders_description
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SwitchSetting(
 | 
			
		||||
                    BooleanSetting.RENDERER_REACTIVE_FLUSHING,
 | 
			
		||||
                    R.string.renderer_reactive_flushing,
 | 
			
		||||
                    R.string.renderer_reactive_flushing_description
 | 
			
		||||
                    titleId = R.string.renderer_reactive_flushing,
 | 
			
		||||
                    descriptionId = R.string.renderer_reactive_flushing_description
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SingleChoiceSetting(
 | 
			
		||||
                    IntSetting.MAX_ANISOTROPY,
 | 
			
		||||
                    R.string.anisotropic_filtering,
 | 
			
		||||
                    R.string.anisotropic_filtering_description,
 | 
			
		||||
                    R.array.anisoEntries,
 | 
			
		||||
                    R.array.anisoValues
 | 
			
		||||
                    titleId = R.string.anisotropic_filtering,
 | 
			
		||||
                    descriptionId = R.string.anisotropic_filtering_description,
 | 
			
		||||
                    choicesId = R.array.anisoEntries,
 | 
			
		||||
                    valuesId = R.array.anisoValues
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SingleChoiceSetting(
 | 
			
		||||
                    IntSetting.AUDIO_OUTPUT_ENGINE,
 | 
			
		||||
                    R.string.audio_output_engine,
 | 
			
		||||
                    0,
 | 
			
		||||
                    R.array.outputEngineEntries,
 | 
			
		||||
                    R.array.outputEngineValues
 | 
			
		||||
                    titleId = R.string.audio_output_engine,
 | 
			
		||||
                    choicesId = R.array.outputEngineEntries,
 | 
			
		||||
                    valuesId = R.array.outputEngineValues
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SliderSetting(
 | 
			
		||||
                    ByteSetting.AUDIO_VOLUME,
 | 
			
		||||
                    R.string.audio_volume,
 | 
			
		||||
                    R.string.audio_volume_description,
 | 
			
		||||
                    0,
 | 
			
		||||
                    100,
 | 
			
		||||
                    "%"
 | 
			
		||||
                    titleId = R.string.audio_volume,
 | 
			
		||||
                    descriptionId = R.string.audio_volume_description,
 | 
			
		||||
                    units = "%"
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SingleChoiceSetting(
 | 
			
		||||
                    IntSetting.RENDERER_BACKEND,
 | 
			
		||||
                    R.string.renderer_api,
 | 
			
		||||
                    0,
 | 
			
		||||
                    R.array.rendererApiNames,
 | 
			
		||||
                    R.array.rendererApiValues
 | 
			
		||||
                    titleId = R.string.renderer_api,
 | 
			
		||||
                    choicesId = R.array.rendererApiNames,
 | 
			
		||||
                    valuesId = R.array.rendererApiValues
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SwitchSetting(
 | 
			
		||||
                    BooleanSetting.RENDERER_DEBUG,
 | 
			
		||||
                    R.string.renderer_debug,
 | 
			
		||||
                    R.string.renderer_debug_description
 | 
			
		||||
                    titleId = R.string.renderer_debug,
 | 
			
		||||
                    descriptionId = R.string.renderer_debug_description
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            put(
 | 
			
		||||
                SwitchSetting(
 | 
			
		||||
                    BooleanSetting.CPU_DEBUG_MODE,
 | 
			
		||||
                    R.string.cpu_debug_mode,
 | 
			
		||||
                    R.string.cpu_debug_mode_description
 | 
			
		||||
                    titleId = R.string.cpu_debug_mode,
 | 
			
		||||
                    descriptionId = R.string.cpu_debug_mode_description
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
@@ -346,7 +379,7 @@ abstract class SettingsItem(
 | 
			
		||||
 | 
			
		||||
                override fun reset() = setBoolean(defaultValue)
 | 
			
		||||
            }
 | 
			
		||||
            put(SwitchSetting(fastmem, R.string.fastmem, 0))
 | 
			
		||||
            put(SwitchSetting(fastmem, R.string.fastmem))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,16 +3,20 @@
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.model.view
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.ArrayRes
 | 
			
		||||
import androidx.annotation.StringRes
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
 | 
			
		||||
 | 
			
		||||
class SingleChoiceSetting(
 | 
			
		||||
    setting: AbstractSetting,
 | 
			
		||||
    titleId: Int,
 | 
			
		||||
    descriptionId: Int,
 | 
			
		||||
    val choicesId: Int,
 | 
			
		||||
    val valuesId: Int
 | 
			
		||||
) : SettingsItem(setting, titleId, descriptionId) {
 | 
			
		||||
    @StringRes titleId: Int = 0,
 | 
			
		||||
    titleString: String = "",
 | 
			
		||||
    @StringRes descriptionId: Int = 0,
 | 
			
		||||
    descriptionString: String = "",
 | 
			
		||||
    @ArrayRes val choicesId: Int,
 | 
			
		||||
    @ArrayRes val valuesId: Int
 | 
			
		||||
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
 | 
			
		||||
    override val type = TYPE_SINGLE_CHOICE
 | 
			
		||||
 | 
			
		||||
    fun getSelectedValue(needsGlobal: Boolean = false) =
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.model.view
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.StringRes
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractByteSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
 | 
			
		||||
@@ -12,12 +13,14 @@ import kotlin.math.roundToInt
 | 
			
		||||
 | 
			
		||||
class SliderSetting(
 | 
			
		||||
    setting: AbstractSetting,
 | 
			
		||||
    titleId: Int,
 | 
			
		||||
    descriptionId: Int,
 | 
			
		||||
    val min: Int,
 | 
			
		||||
    val max: Int,
 | 
			
		||||
    val units: String
 | 
			
		||||
) : SettingsItem(setting, titleId, descriptionId) {
 | 
			
		||||
    @StringRes titleId: Int = 0,
 | 
			
		||||
    titleString: String = "",
 | 
			
		||||
    @StringRes descriptionId: Int = 0,
 | 
			
		||||
    descriptionString: String = "",
 | 
			
		||||
    val min: Int = 0,
 | 
			
		||||
    val max: Int = 100,
 | 
			
		||||
    val units: String = ""
 | 
			
		||||
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
 | 
			
		||||
    override val type = TYPE_SLIDER
 | 
			
		||||
 | 
			
		||||
    fun getSelectedValue(needsGlobal: Boolean = false) =
 | 
			
		||||
 
 | 
			
		||||
@@ -3,15 +3,18 @@
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.model.view
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.StringRes
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
 | 
			
		||||
 | 
			
		||||
class StringSingleChoiceSetting(
 | 
			
		||||
    private val stringSetting: AbstractStringSetting,
 | 
			
		||||
    titleId: Int,
 | 
			
		||||
    descriptionId: Int,
 | 
			
		||||
    @StringRes titleId: Int = 0,
 | 
			
		||||
    titleString: String = "",
 | 
			
		||||
    @StringRes descriptionId: Int = 0,
 | 
			
		||||
    descriptionString: String = "",
 | 
			
		||||
    val choices: Array<String>,
 | 
			
		||||
    val values: Array<String>
 | 
			
		||||
) : SettingsItem(stringSetting, titleId, descriptionId) {
 | 
			
		||||
) : SettingsItem(stringSetting, titleId, titleString, descriptionId, descriptionString) {
 | 
			
		||||
    override val type = TYPE_STRING_SINGLE_CHOICE
 | 
			
		||||
 | 
			
		||||
    fun getValueAt(index: Int): String =
 | 
			
		||||
@@ -20,7 +23,7 @@ class StringSingleChoiceSetting(
 | 
			
		||||
    fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal)
 | 
			
		||||
    fun setSelectedValue(value: String) = stringSetting.setString(value)
 | 
			
		||||
 | 
			
		||||
    val selectValueIndex: Int
 | 
			
		||||
    val selectedValueIndex: Int
 | 
			
		||||
        get() {
 | 
			
		||||
            for (i in values.indices) {
 | 
			
		||||
                if (values[i] == getSelectedValue()) {
 | 
			
		||||
 
 | 
			
		||||
@@ -8,10 +8,12 @@ import androidx.annotation.StringRes
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
 | 
			
		||||
 | 
			
		||||
class SubmenuSetting(
 | 
			
		||||
    @StringRes titleId: Int,
 | 
			
		||||
    @StringRes descriptionId: Int,
 | 
			
		||||
    @DrawableRes val iconId: Int,
 | 
			
		||||
    @StringRes titleId: Int = 0,
 | 
			
		||||
    titleString: String = "",
 | 
			
		||||
    @StringRes descriptionId: Int = 0,
 | 
			
		||||
    descriptionString: String = "",
 | 
			
		||||
    @DrawableRes val iconId: Int = 0,
 | 
			
		||||
    val menuKey: Settings.MenuTag
 | 
			
		||||
) : SettingsItem(emptySetting, titleId, descriptionId) {
 | 
			
		||||
) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) {
 | 
			
		||||
    override val type = TYPE_SUBMENU
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,15 +3,18 @@
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.model.view
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.StringRes
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
 | 
			
		||||
 | 
			
		||||
class SwitchSetting(
 | 
			
		||||
    setting: AbstractSetting,
 | 
			
		||||
    titleId: Int,
 | 
			
		||||
    descriptionId: Int
 | 
			
		||||
) : SettingsItem(setting, titleId, descriptionId) {
 | 
			
		||||
    @StringRes titleId: Int = 0,
 | 
			
		||||
    titleString: String = "",
 | 
			
		||||
    @StringRes descriptionId: Int = 0,
 | 
			
		||||
    descriptionString: String = ""
 | 
			
		||||
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
 | 
			
		||||
    override val type = TYPE_SWITCH
 | 
			
		||||
 | 
			
		||||
    fun getIsChecked(needsGlobal: Boolean = false): Boolean {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,300 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.ui
 | 
			
		||||
 | 
			
		||||
import android.app.Dialog
 | 
			
		||||
import android.graphics.drawable.Animatable2
 | 
			
		||||
import android.graphics.drawable.AnimatedVectorDrawable
 | 
			
		||||
import android.graphics.drawable.Drawable
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.view.InputDevice
 | 
			
		||||
import android.view.KeyEvent
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.MotionEvent
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import androidx.fragment.app.DialogFragment
 | 
			
		||||
import androidx.fragment.app.activityViewModels
 | 
			
		||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.DialogMappingBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.NativeButton
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.InputSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.ModifierInputSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.InputHandler
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.ParamPackage
 | 
			
		||||
 | 
			
		||||
class InputDialogFragment : DialogFragment() {
 | 
			
		||||
    private var inputAccepted = false
 | 
			
		||||
 | 
			
		||||
    private var position: Int = 0
 | 
			
		||||
 | 
			
		||||
    private lateinit var inputSetting: InputSetting
 | 
			
		||||
 | 
			
		||||
    private lateinit var binding: DialogMappingBinding
 | 
			
		||||
 | 
			
		||||
    private val settingsViewModel: SettingsViewModel by activityViewModels()
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
        if (settingsViewModel.clickedItem == null) dismiss()
 | 
			
		||||
 | 
			
		||||
        position = requireArguments().getInt(POSITION)
 | 
			
		||||
 | 
			
		||||
        InputHandler.updateControllerData()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
 | 
			
		||||
        inputSetting = settingsViewModel.clickedItem as InputSetting
 | 
			
		||||
        binding = DialogMappingBinding.inflate(layoutInflater)
 | 
			
		||||
 | 
			
		||||
        val builder = MaterialAlertDialogBuilder(requireContext())
 | 
			
		||||
            .setPositiveButton(android.R.string.cancel) { _, _ ->
 | 
			
		||||
                NativeInput.stopMapping()
 | 
			
		||||
                dismiss()
 | 
			
		||||
            }
 | 
			
		||||
            .setView(binding.root)
 | 
			
		||||
 | 
			
		||||
        val playButtonMapAnimation = { twoDirections: Boolean ->
 | 
			
		||||
            val stickAnimation: AnimatedVectorDrawable
 | 
			
		||||
            val buttonAnimation: AnimatedVectorDrawable
 | 
			
		||||
            binding.imageStickAnimation.apply {
 | 
			
		||||
                val anim = if (twoDirections) {
 | 
			
		||||
                    R.drawable.stick_two_direction_anim
 | 
			
		||||
                } else {
 | 
			
		||||
                    R.drawable.stick_one_direction_anim
 | 
			
		||||
                }
 | 
			
		||||
                setBackgroundResource(anim)
 | 
			
		||||
                stickAnimation = background as AnimatedVectorDrawable
 | 
			
		||||
            }
 | 
			
		||||
            binding.imageButtonAnimation.apply {
 | 
			
		||||
                setBackgroundResource(R.drawable.button_anim)
 | 
			
		||||
                buttonAnimation = background as AnimatedVectorDrawable
 | 
			
		||||
            }
 | 
			
		||||
            stickAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() {
 | 
			
		||||
                override fun onAnimationEnd(drawable: Drawable?) {
 | 
			
		||||
                    buttonAnimation.start()
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            buttonAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() {
 | 
			
		||||
                override fun onAnimationEnd(drawable: Drawable?) {
 | 
			
		||||
                    stickAnimation.start()
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            stickAnimation.start()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        when (val setting = inputSetting) {
 | 
			
		||||
            is AnalogInputSetting -> {
 | 
			
		||||
                when (setting.nativeAnalog) {
 | 
			
		||||
                    NativeAnalog.LStick -> builder.setTitle(
 | 
			
		||||
                        getString(R.string.map_control, getString(R.string.left_stick))
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                    NativeAnalog.RStick -> builder.setTitle(
 | 
			
		||||
                        getString(R.string.map_control, getString(R.string.right_stick))
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                builder.setMessage(R.string.stick_map_description)
 | 
			
		||||
 | 
			
		||||
                playButtonMapAnimation.invoke(true)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            is ModifierInputSetting -> {
 | 
			
		||||
                builder.setTitle(getString(R.string.map_control, setting.title))
 | 
			
		||||
                    .setMessage(R.string.button_map_description)
 | 
			
		||||
                playButtonMapAnimation.invoke(false)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            is ButtonInputSetting -> {
 | 
			
		||||
                if (setting.nativeButton == NativeButton.DUp ||
 | 
			
		||||
                    setting.nativeButton == NativeButton.DDown ||
 | 
			
		||||
                    setting.nativeButton == NativeButton.DLeft ||
 | 
			
		||||
                    setting.nativeButton == NativeButton.DRight
 | 
			
		||||
                ) {
 | 
			
		||||
                    builder.setTitle(getString(R.string.map_dpad_direction, setting.title))
 | 
			
		||||
                } else {
 | 
			
		||||
                    builder.setTitle(getString(R.string.map_control, setting.title))
 | 
			
		||||
                }
 | 
			
		||||
                builder.setMessage(R.string.button_map_description)
 | 
			
		||||
                playButtonMapAnimation.invoke(false)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return builder.create()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateView(
 | 
			
		||||
        inflater: LayoutInflater,
 | 
			
		||||
        container: ViewGroup?,
 | 
			
		||||
        savedInstanceState: Bundle?
 | 
			
		||||
    ): View {
 | 
			
		||||
        return binding.root
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onViewCreated(view, savedInstanceState)
 | 
			
		||||
        view.requestFocus()
 | 
			
		||||
        view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
 | 
			
		||||
        dialog?.setOnKeyListener { _, _, keyEvent -> onKeyEvent(keyEvent) }
 | 
			
		||||
        binding.root.setOnGenericMotionListener { _, motionEvent -> onMotionEvent(motionEvent) }
 | 
			
		||||
        NativeInput.beginMapping(inputSetting.inputType.int)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun onKeyEvent(event: KeyEvent): Boolean {
 | 
			
		||||
        if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
 | 
			
		||||
            event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
 | 
			
		||||
        ) {
 | 
			
		||||
            return false
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val action = when (event.action) {
 | 
			
		||||
            KeyEvent.ACTION_DOWN -> NativeInput.ButtonState.PRESSED
 | 
			
		||||
            KeyEvent.ACTION_UP -> NativeInput.ButtonState.RELEASED
 | 
			
		||||
            else -> return false
 | 
			
		||||
        }
 | 
			
		||||
        val controllerData =
 | 
			
		||||
            InputHandler.androidControllers[event.device.controllerNumber] ?: return false
 | 
			
		||||
        NativeInput.onGamePadButtonEvent(
 | 
			
		||||
            controllerData.getGUID(),
 | 
			
		||||
            controllerData.getPort(),
 | 
			
		||||
            event.keyCode,
 | 
			
		||||
            action
 | 
			
		||||
        )
 | 
			
		||||
        onInputReceived(event.device)
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun onMotionEvent(event: MotionEvent): Boolean {
 | 
			
		||||
        if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
 | 
			
		||||
            event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
 | 
			
		||||
        ) {
 | 
			
		||||
            return false
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Temp workaround for DPads that give both axis and button input. The input system can't
 | 
			
		||||
        // take in a specific axis direction for a binding so you lose half of the directions for a DPad.
 | 
			
		||||
 | 
			
		||||
        val controllerData =
 | 
			
		||||
            InputHandler.androidControllers[event.device.controllerNumber] ?: return false
 | 
			
		||||
        event.device.motionRanges.forEach {
 | 
			
		||||
            NativeInput.onGamePadAxisEvent(
 | 
			
		||||
                controllerData.getGUID(),
 | 
			
		||||
                controllerData.getPort(),
 | 
			
		||||
                it.axis,
 | 
			
		||||
                event.getAxisValue(it.axis)
 | 
			
		||||
            )
 | 
			
		||||
            onInputReceived(event.device)
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun onInputReceived(device: InputDevice) {
 | 
			
		||||
        val params = ParamPackage(NativeInput.getNextInput())
 | 
			
		||||
        if (params.has("engine") && isInputAcceptable(params) && !inputAccepted) {
 | 
			
		||||
            inputAccepted = true
 | 
			
		||||
            setResult(params, device)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setResult(params: ParamPackage, device: InputDevice) {
 | 
			
		||||
        NativeInput.stopMapping()
 | 
			
		||||
        params.set("display", "${device.name} ${params.get("port", 0)}")
 | 
			
		||||
        when (val item = settingsViewModel.clickedItem as InputSetting) {
 | 
			
		||||
            is ModifierInputSetting,
 | 
			
		||||
            is ButtonInputSetting -> {
 | 
			
		||||
                // Invert DPad up and left bindings by default
 | 
			
		||||
                val tempSetting = inputSetting as? ButtonInputSetting
 | 
			
		||||
                if (tempSetting != null) {
 | 
			
		||||
                    if (tempSetting.nativeButton == NativeButton.DUp ||
 | 
			
		||||
                        tempSetting.nativeButton == NativeButton.DLeft &&
 | 
			
		||||
                        params.has("axis")
 | 
			
		||||
                    ) {
 | 
			
		||||
                        params.set("invert", "-")
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                item.setSelectedValue(params)
 | 
			
		||||
                settingsViewModel.setAdapterItemChanged(position)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            is AnalogInputSetting -> {
 | 
			
		||||
                var analogParam = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
 | 
			
		||||
                analogParam = adjustAnalogParam(params, analogParam, item.analogDirection.param)
 | 
			
		||||
 | 
			
		||||
                // Invert Y-Axis by default
 | 
			
		||||
                analogParam.set("invert_y", "-")
 | 
			
		||||
 | 
			
		||||
                item.setSelectedValue(analogParam)
 | 
			
		||||
                settingsViewModel.setReloadListAndNotifyDataset(true)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        dismiss()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun adjustAnalogParam(
 | 
			
		||||
        inputParam: ParamPackage,
 | 
			
		||||
        analogParam: ParamPackage,
 | 
			
		||||
        buttonName: String
 | 
			
		||||
    ): ParamPackage {
 | 
			
		||||
        // The poller returned a complete axis, so set all the buttons
 | 
			
		||||
        if (inputParam.has("axis_x") && inputParam.has("axis_y")) {
 | 
			
		||||
            return inputParam
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if the current configuration has either no engine or an axis binding.
 | 
			
		||||
        // Clears out the old binding and adds one with analog_from_button.
 | 
			
		||||
        if (!analogParam.has("engine") || analogParam.has("axis_x") || analogParam.has("axis_y")) {
 | 
			
		||||
            analogParam.clear()
 | 
			
		||||
            analogParam.set("engine", "analog_from_button")
 | 
			
		||||
        }
 | 
			
		||||
        analogParam.set(buttonName, inputParam.serialize())
 | 
			
		||||
        return analogParam
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun isInputAcceptable(params: ParamPackage): Boolean {
 | 
			
		||||
        if (InputHandler.registeredControllers.size == 1) {
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (params.has("motion")) {
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val currentDevice = settingsViewModel.getCurrentDeviceParams(params)
 | 
			
		||||
        if (currentDevice.get("engine", "any") == "any") {
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val guidMatch = params.get("guid", "") == currentDevice.get("guid", "") ||
 | 
			
		||||
            params.get("guid", "") == currentDevice.get("guid2", "")
 | 
			
		||||
        return params.get("engine", "") == currentDevice.get("engine", "") &&
 | 
			
		||||
            guidMatch &&
 | 
			
		||||
            params.get("port", 0) == currentDevice.get("port", 0)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val TAG = "InputDialogFragment"
 | 
			
		||||
 | 
			
		||||
        const val POSITION = "Position"
 | 
			
		||||
 | 
			
		||||
        fun newInstance(
 | 
			
		||||
            inputMappingViewModel: SettingsViewModel,
 | 
			
		||||
            setting: InputSetting,
 | 
			
		||||
            position: Int
 | 
			
		||||
        ): InputDialogFragment {
 | 
			
		||||
            inputMappingViewModel.clickedItem = setting
 | 
			
		||||
            val args = Bundle()
 | 
			
		||||
            args.putInt(POSITION, position)
 | 
			
		||||
            val fragment = InputDialogFragment()
 | 
			
		||||
            fragment.arguments = args
 | 
			
		||||
            return fragment
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,68 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.ui
 | 
			
		||||
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import org.yuzu.yuzu_emu.YuzuApplication
 | 
			
		||||
import org.yuzu.yuzu_emu.adapters.AbstractListAdapter
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.ListItemInputProfileBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
 | 
			
		||||
class InputProfileAdapter(options: List<ProfileItem>) :
 | 
			
		||||
    AbstractListAdapter<ProfileItem, AbstractViewHolder<ProfileItem>>(options) {
 | 
			
		||||
    override fun onCreateViewHolder(
 | 
			
		||||
        parent: ViewGroup,
 | 
			
		||||
        viewType: Int
 | 
			
		||||
    ): AbstractViewHolder<ProfileItem> {
 | 
			
		||||
        ListItemInputProfileBinding.inflate(LayoutInflater.from(parent.context), parent, false)
 | 
			
		||||
            .also { return InputProfileViewHolder(it) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inner class InputProfileViewHolder(val binding: ListItemInputProfileBinding) :
 | 
			
		||||
        AbstractViewHolder<ProfileItem>(binding) {
 | 
			
		||||
        override fun bind(model: ProfileItem) {
 | 
			
		||||
            when (model) {
 | 
			
		||||
                is ExistingProfileItem -> {
 | 
			
		||||
                    binding.title.text = model.name
 | 
			
		||||
                    binding.buttonNew.visibility = View.GONE
 | 
			
		||||
                    binding.buttonDelete.visibility = View.VISIBLE
 | 
			
		||||
                    binding.buttonDelete.setOnClickListener { model.deleteProfile.invoke() }
 | 
			
		||||
                    binding.buttonSave.visibility = View.VISIBLE
 | 
			
		||||
                    binding.buttonSave.setOnClickListener { model.saveProfile.invoke() }
 | 
			
		||||
                    binding.buttonLoad.visibility = View.VISIBLE
 | 
			
		||||
                    binding.buttonLoad.setOnClickListener { model.loadProfile.invoke() }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                is NewProfileItem -> {
 | 
			
		||||
                    binding.title.text = model.name
 | 
			
		||||
                    binding.buttonNew.visibility = View.VISIBLE
 | 
			
		||||
                    binding.buttonNew.setOnClickListener { model.createNewProfile.invoke() }
 | 
			
		||||
                    binding.buttonSave.visibility = View.GONE
 | 
			
		||||
                    binding.buttonDelete.visibility = View.GONE
 | 
			
		||||
                    binding.buttonLoad.visibility = View.GONE
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
sealed interface ProfileItem {
 | 
			
		||||
    val name: String
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
data class NewProfileItem(
 | 
			
		||||
    val createNewProfile: () -> Unit
 | 
			
		||||
) : ProfileItem {
 | 
			
		||||
    override val name: String = YuzuApplication.appContext.getString(R.string.create_new_profile)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
data class ExistingProfileItem(
 | 
			
		||||
    override val name: String,
 | 
			
		||||
    val deleteProfile: () -> Unit,
 | 
			
		||||
    val saveProfile: () -> Unit,
 | 
			
		||||
    val loadProfile: () -> Unit
 | 
			
		||||
) : ProfileItem
 | 
			
		||||
@@ -0,0 +1,155 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.ui
 | 
			
		||||
 | 
			
		||||
import android.app.Dialog
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import androidx.fragment.app.DialogFragment
 | 
			
		||||
import androidx.fragment.app.activityViewModels
 | 
			
		||||
import androidx.lifecycle.Lifecycle
 | 
			
		||||
import androidx.lifecycle.lifecycleScope
 | 
			
		||||
import androidx.lifecycle.repeatOnLifecycle
 | 
			
		||||
import androidx.recyclerview.widget.LinearLayoutManager
 | 
			
		||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.DialogInputProfilesBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
 | 
			
		||||
 | 
			
		||||
class InputProfileDialogFragment : DialogFragment() {
 | 
			
		||||
    private var position = 0
 | 
			
		||||
 | 
			
		||||
    private val settingsViewModel: SettingsViewModel by activityViewModels()
 | 
			
		||||
 | 
			
		||||
    private lateinit var binding: DialogInputProfilesBinding
 | 
			
		||||
 | 
			
		||||
    private lateinit var setting: InputProfileSetting
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
        position = requireArguments().getInt(POSITION)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
 | 
			
		||||
        binding = DialogInputProfilesBinding.inflate(layoutInflater)
 | 
			
		||||
 | 
			
		||||
        setting = settingsViewModel.clickedItem as InputProfileSetting
 | 
			
		||||
        val options = mutableListOf<ProfileItem>().apply {
 | 
			
		||||
            add(
 | 
			
		||||
                NewProfileItem(
 | 
			
		||||
                    createNewProfile = {
 | 
			
		||||
                        NewInputProfileDialogFragment.newInstance(
 | 
			
		||||
                            settingsViewModel,
 | 
			
		||||
                            setting,
 | 
			
		||||
                            position
 | 
			
		||||
                        ).show(parentFragmentManager, NewInputProfileDialogFragment.TAG)
 | 
			
		||||
                        dismiss()
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            val onActionDismiss = {
 | 
			
		||||
                settingsViewModel.setReloadListAndNotifyDataset(true)
 | 
			
		||||
                dismiss()
 | 
			
		||||
            }
 | 
			
		||||
            setting.getProfileNames().forEach {
 | 
			
		||||
                add(
 | 
			
		||||
                    ExistingProfileItem(
 | 
			
		||||
                        it,
 | 
			
		||||
                        deleteProfile = {
 | 
			
		||||
                            settingsViewModel.setShouldShowDeleteProfileDialog(it)
 | 
			
		||||
                        },
 | 
			
		||||
                        saveProfile = {
 | 
			
		||||
                            if (!setting.saveProfile(it)) {
 | 
			
		||||
                                Toast.makeText(
 | 
			
		||||
                                    requireContext(),
 | 
			
		||||
                                    R.string.failed_to_save_profile,
 | 
			
		||||
                                    Toast.LENGTH_SHORT
 | 
			
		||||
                                ).show()
 | 
			
		||||
                            }
 | 
			
		||||
                            onActionDismiss.invoke()
 | 
			
		||||
                        },
 | 
			
		||||
                        loadProfile = {
 | 
			
		||||
                            if (!setting.loadProfile(it)) {
 | 
			
		||||
                                Toast.makeText(
 | 
			
		||||
                                    requireContext(),
 | 
			
		||||
                                    R.string.failed_to_load_profile,
 | 
			
		||||
                                    Toast.LENGTH_SHORT
 | 
			
		||||
                                ).show()
 | 
			
		||||
                            }
 | 
			
		||||
                            onActionDismiss.invoke()
 | 
			
		||||
                        }
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        binding.listProfiles.apply {
 | 
			
		||||
            layoutManager = LinearLayoutManager(requireContext())
 | 
			
		||||
            adapter = InputProfileAdapter(options)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return MaterialAlertDialogBuilder(requireContext())
 | 
			
		||||
            .setView(binding.root)
 | 
			
		||||
            .create()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateView(
 | 
			
		||||
        inflater: LayoutInflater,
 | 
			
		||||
        container: ViewGroup?,
 | 
			
		||||
        savedInstanceState: Bundle?
 | 
			
		||||
    ): View {
 | 
			
		||||
        return binding.root
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onViewCreated(view, savedInstanceState)
 | 
			
		||||
 | 
			
		||||
        viewLifecycleOwner.lifecycleScope.launch {
 | 
			
		||||
            repeatOnLifecycle(Lifecycle.State.CREATED) {
 | 
			
		||||
                settingsViewModel.shouldShowDeleteProfileDialog.collect {
 | 
			
		||||
                    if (it.isNotEmpty()) {
 | 
			
		||||
                        MessageDialogFragment.newInstance(
 | 
			
		||||
                            activity = requireActivity(),
 | 
			
		||||
                            titleId = R.string.delete_input_profile,
 | 
			
		||||
                            descriptionId = R.string.delete_input_profile_description,
 | 
			
		||||
                            positiveAction = {
 | 
			
		||||
                                setting.deleteProfile(it)
 | 
			
		||||
                                settingsViewModel.setReloadListAndNotifyDataset(true)
 | 
			
		||||
                            },
 | 
			
		||||
                            negativeAction = {},
 | 
			
		||||
                            negativeButtonTitleId = android.R.string.cancel
 | 
			
		||||
                        ).show(parentFragmentManager, MessageDialogFragment.TAG)
 | 
			
		||||
                        settingsViewModel.setShouldShowDeleteProfileDialog("")
 | 
			
		||||
                        dismiss()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val TAG = "InputProfileDialogFragment"
 | 
			
		||||
 | 
			
		||||
        const val POSITION = "Position"
 | 
			
		||||
 | 
			
		||||
        fun newInstance(
 | 
			
		||||
            settingsViewModel: SettingsViewModel,
 | 
			
		||||
            profileSetting: InputProfileSetting,
 | 
			
		||||
            position: Int
 | 
			
		||||
        ): InputProfileDialogFragment {
 | 
			
		||||
            settingsViewModel.clickedItem = profileSetting
 | 
			
		||||
 | 
			
		||||
            val args = Bundle()
 | 
			
		||||
            args.putInt(POSITION, position)
 | 
			
		||||
            val fragment = InputProfileDialogFragment()
 | 
			
		||||
            fragment.arguments = args
 | 
			
		||||
            return fragment
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,79 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.ui
 | 
			
		||||
 | 
			
		||||
import android.app.Dialog
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import androidx.fragment.app.DialogFragment
 | 
			
		||||
import androidx.fragment.app.activityViewModels
 | 
			
		||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.DialogEditTextBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
 | 
			
		||||
class NewInputProfileDialogFragment : DialogFragment() {
 | 
			
		||||
    private var position = 0
 | 
			
		||||
 | 
			
		||||
    private val settingsViewModel: SettingsViewModel by activityViewModels()
 | 
			
		||||
 | 
			
		||||
    private lateinit var binding: DialogEditTextBinding
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
        position = requireArguments().getInt(POSITION)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
 | 
			
		||||
        binding = DialogEditTextBinding.inflate(layoutInflater)
 | 
			
		||||
 | 
			
		||||
        val setting = settingsViewModel.clickedItem as InputProfileSetting
 | 
			
		||||
        return MaterialAlertDialogBuilder(requireContext())
 | 
			
		||||
            .setTitle(R.string.enter_profile_name)
 | 
			
		||||
            .setPositiveButton(android.R.string.ok) { _, _ ->
 | 
			
		||||
                val profileName = binding.editText.text.toString()
 | 
			
		||||
                if (!setting.isProfileNameValid(profileName)) {
 | 
			
		||||
                    Toast.makeText(
 | 
			
		||||
                        requireContext(),
 | 
			
		||||
                        R.string.invalid_profile_name,
 | 
			
		||||
                        Toast.LENGTH_SHORT
 | 
			
		||||
                    ).show()
 | 
			
		||||
                    return@setPositiveButton
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!setting.createProfile(profileName)) {
 | 
			
		||||
                    Toast.makeText(
 | 
			
		||||
                        requireContext(),
 | 
			
		||||
                        R.string.profile_name_already_exists,
 | 
			
		||||
                        Toast.LENGTH_SHORT
 | 
			
		||||
                    ).show()
 | 
			
		||||
                } else {
 | 
			
		||||
                    settingsViewModel.setAdapterItemChanged(position)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .setNegativeButton(android.R.string.cancel, null)
 | 
			
		||||
            .setView(binding.root)
 | 
			
		||||
            .show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val TAG = "NewInputProfileDialogFragment"
 | 
			
		||||
 | 
			
		||||
        const val POSITION = "Position"
 | 
			
		||||
 | 
			
		||||
        fun newInstance(
 | 
			
		||||
            settingsViewModel: SettingsViewModel,
 | 
			
		||||
            profileSetting: InputProfileSetting,
 | 
			
		||||
            position: Int
 | 
			
		||||
        ): NewInputProfileDialogFragment {
 | 
			
		||||
            settingsViewModel.clickedItem = profileSetting
 | 
			
		||||
 | 
			
		||||
            val args = Bundle()
 | 
			
		||||
            args.putInt(POSITION, position)
 | 
			
		||||
            val fragment = NewInputProfileDialogFragment()
 | 
			
		||||
            fragment.arguments = args
 | 
			
		||||
            return fragment
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -25,9 +25,9 @@ import org.yuzu.yuzu_emu.NativeLibrary
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
 | 
			
		||||
import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment
 | 
			
		||||
import org.yuzu.yuzu_emu.model.SettingsViewModel
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.*
 | 
			
		||||
 | 
			
		||||
class SettingsActivity : AppCompatActivity() {
 | 
			
		||||
@@ -137,6 +137,7 @@ class SettingsActivity : AppCompatActivity() {
 | 
			
		||||
        super.onStop()
 | 
			
		||||
        Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
 | 
			
		||||
        if (isFinishing) {
 | 
			
		||||
            NativeInput.reloadInputDevices()
 | 
			
		||||
            NativeLibrary.applySettings()
 | 
			
		||||
            if (args.game == null) {
 | 
			
		||||
                NativeConfig.saveGlobalConfig()
 | 
			
		||||
 
 | 
			
		||||
@@ -8,12 +8,11 @@ import android.icu.util.Calendar
 | 
			
		||||
import android.icu.util.TimeZone
 | 
			
		||||
import android.text.format.DateFormat
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import android.widget.PopupMenu
 | 
			
		||||
import androidx.fragment.app.Fragment
 | 
			
		||||
import androidx.lifecycle.Lifecycle
 | 
			
		||||
import androidx.lifecycle.ViewModelProvider
 | 
			
		||||
import androidx.lifecycle.lifecycleScope
 | 
			
		||||
import androidx.lifecycle.repeatOnLifecycle
 | 
			
		||||
import androidx.navigation.findNavController
 | 
			
		||||
import androidx.recyclerview.widget.AsyncDifferConfig
 | 
			
		||||
import androidx.recyclerview.widget.DiffUtil
 | 
			
		||||
@@ -21,16 +20,18 @@ import androidx.recyclerview.widget.ListAdapter
 | 
			
		||||
import com.google.android.material.datepicker.MaterialDatePicker
 | 
			
		||||
import com.google.android.material.timepicker.MaterialTimePicker
 | 
			
		||||
import com.google.android.material.timepicker.TimeFormat
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
import org.yuzu.yuzu_emu.SettingsNavigationDirections
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingInputBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.*
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.ui.viewholder.*
 | 
			
		||||
import org.yuzu.yuzu_emu.fragments.SettingsDialogFragment
 | 
			
		||||
import org.yuzu.yuzu_emu.model.SettingsViewModel
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.ParamPackage
 | 
			
		||||
 | 
			
		||||
class SettingsAdapter(
 | 
			
		||||
    private val fragment: Fragment,
 | 
			
		||||
@@ -41,19 +42,6 @@ class SettingsAdapter(
 | 
			
		||||
    private val settingsViewModel: SettingsViewModel
 | 
			
		||||
        get() = ViewModelProvider(fragment.requireActivity())[SettingsViewModel::class.java]
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        fragment.viewLifecycleOwner.lifecycleScope.launch {
 | 
			
		||||
            fragment.repeatOnLifecycle(Lifecycle.State.STARTED) {
 | 
			
		||||
                settingsViewModel.adapterItemChanged.collect {
 | 
			
		||||
                    if (it != -1) {
 | 
			
		||||
                        notifyItemChanged(it)
 | 
			
		||||
                        settingsViewModel.setAdapterItemChanged(-1)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder {
 | 
			
		||||
        val inflater = LayoutInflater.from(parent.context)
 | 
			
		||||
        return when (viewType) {
 | 
			
		||||
@@ -85,8 +73,19 @@ class SettingsAdapter(
 | 
			
		||||
                RunnableViewHolder(ListItemSettingBinding.inflate(inflater), this)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            SettingsItem.TYPE_INPUT -> {
 | 
			
		||||
                InputViewHolder(ListItemSettingInputBinding.inflate(inflater), this)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            SettingsItem.TYPE_INT_SINGLE_CHOICE -> {
 | 
			
		||||
                SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            SettingsItem.TYPE_INPUT_PROFILE -> {
 | 
			
		||||
                InputProfileViewHolder(ListItemSettingBinding.inflate(inflater), this)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            else -> {
 | 
			
		||||
                // TODO: Create an error view since we can't return null now
 | 
			
		||||
                HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -126,6 +125,15 @@ class SettingsAdapter(
 | 
			
		||||
        ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onIntSingleChoiceClick(item: IntSingleChoiceSetting, position: Int) {
 | 
			
		||||
        SettingsDialogFragment.newInstance(
 | 
			
		||||
            settingsViewModel,
 | 
			
		||||
            item,
 | 
			
		||||
            SettingsItem.TYPE_INT_SINGLE_CHOICE,
 | 
			
		||||
            position
 | 
			
		||||
        ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onDateTimeClick(item: DateTimeSetting, position: Int) {
 | 
			
		||||
        val storedTime = item.getValue() * 1000
 | 
			
		||||
 | 
			
		||||
@@ -185,6 +193,205 @@ class SettingsAdapter(
 | 
			
		||||
        fragment.view?.findNavController()?.navigate(action)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onInputProfileClick(item: InputProfileSetting, position: Int) {
 | 
			
		||||
        InputProfileDialogFragment.newInstance(
 | 
			
		||||
            settingsViewModel,
 | 
			
		||||
            item,
 | 
			
		||||
            position
 | 
			
		||||
        ).show(fragment.childFragmentManager, InputProfileDialogFragment.TAG)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onInputClick(item: InputSetting, position: Int) {
 | 
			
		||||
        InputDialogFragment.newInstance(
 | 
			
		||||
            settingsViewModel,
 | 
			
		||||
            item,
 | 
			
		||||
            position
 | 
			
		||||
        ).show(fragment.childFragmentManager, InputDialogFragment.TAG)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onInputOptionsClick(anchor: View, item: InputSetting, position: Int) {
 | 
			
		||||
        val popup = PopupMenu(context, anchor)
 | 
			
		||||
        popup.menuInflater.inflate(R.menu.menu_input_options, popup.menu)
 | 
			
		||||
 | 
			
		||||
        popup.menu.apply {
 | 
			
		||||
            val invertAxis = findItem(R.id.invert_axis)
 | 
			
		||||
            val invertButton = findItem(R.id.invert_button)
 | 
			
		||||
            val toggleButton = findItem(R.id.toggle_button)
 | 
			
		||||
            val turboButton = findItem(R.id.turbo_button)
 | 
			
		||||
            val setThreshold = findItem(R.id.set_threshold)
 | 
			
		||||
            val toggleAxis = findItem(R.id.toggle_axis)
 | 
			
		||||
            when (item) {
 | 
			
		||||
                is AnalogInputSetting -> {
 | 
			
		||||
                    val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
 | 
			
		||||
 | 
			
		||||
                    invertAxis.isVisible = true
 | 
			
		||||
                    invertAxis.isCheckable = true
 | 
			
		||||
                    invertAxis.isChecked = when (item.analogDirection) {
 | 
			
		||||
                        AnalogDirection.Left, AnalogDirection.Right -> {
 | 
			
		||||
                            params.get("invert_x", "+") == "-"
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        AnalogDirection.Up, AnalogDirection.Down -> {
 | 
			
		||||
                            params.get("invert_y", "+") == "-"
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    invertAxis.setOnMenuItemClickListener {
 | 
			
		||||
                        if (item.analogDirection == AnalogDirection.Left ||
 | 
			
		||||
                            item.analogDirection == AnalogDirection.Right
 | 
			
		||||
                        ) {
 | 
			
		||||
                            val invertValue = params.get("invert_x", "+") == "-"
 | 
			
		||||
                            val invertString = if (invertValue) "+" else "-"
 | 
			
		||||
                            params.set("invert_x", invertString)
 | 
			
		||||
                        } else if (
 | 
			
		||||
                            item.analogDirection == AnalogDirection.Up ||
 | 
			
		||||
                            item.analogDirection == AnalogDirection.Down
 | 
			
		||||
                        ) {
 | 
			
		||||
                            val invertValue = params.get("invert_y", "+") == "-"
 | 
			
		||||
                            val invertString = if (invertValue) "+" else "-"
 | 
			
		||||
                            params.set("invert_y", invertString)
 | 
			
		||||
                        }
 | 
			
		||||
                        true
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    popup.setOnDismissListener {
 | 
			
		||||
                        NativeInput.setStickParam(item.playerIndex, item.nativeAnalog, params)
 | 
			
		||||
                        settingsViewModel.setDatasetChanged(true)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                is ButtonInputSetting -> {
 | 
			
		||||
                    val params = NativeInput.getButtonParam(item.playerIndex, item.nativeButton)
 | 
			
		||||
                    if (params.has("code") || params.has("button") || params.has("hat")) {
 | 
			
		||||
                        val buttonInvert = params.get("inverted", false)
 | 
			
		||||
                        invertButton.isVisible = true
 | 
			
		||||
                        invertButton.isCheckable = true
 | 
			
		||||
                        invertButton.isChecked = buttonInvert
 | 
			
		||||
                        invertButton.setOnMenuItemClickListener {
 | 
			
		||||
                            params.set("inverted", !buttonInvert)
 | 
			
		||||
                            true
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        val toggle = params.get("toggle", false)
 | 
			
		||||
                        toggleButton.isVisible = true
 | 
			
		||||
                        toggleButton.isCheckable = true
 | 
			
		||||
                        toggleButton.isChecked = toggle
 | 
			
		||||
                        toggleButton.setOnMenuItemClickListener {
 | 
			
		||||
                            params.set("toggle", !toggle)
 | 
			
		||||
                            true
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        val turbo = params.get("turbo", false)
 | 
			
		||||
                        turboButton.isVisible = true
 | 
			
		||||
                        turboButton.isCheckable = true
 | 
			
		||||
                        turboButton.isChecked = turbo
 | 
			
		||||
                        turboButton.setOnMenuItemClickListener {
 | 
			
		||||
                            params.set("turbo", !turbo)
 | 
			
		||||
                            true
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if (params.has("axis")) {
 | 
			
		||||
                        val axisInvert = params.get("invert", "+") == "-"
 | 
			
		||||
                        invertAxis.isVisible = true
 | 
			
		||||
                        invertAxis.isCheckable = true
 | 
			
		||||
                        invertAxis.isChecked = axisInvert
 | 
			
		||||
                        invertAxis.setOnMenuItemClickListener {
 | 
			
		||||
                            params.set("invert", if (!axisInvert) "-" else "+")
 | 
			
		||||
                            true
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        val buttonInvert = params.get("inverted", false)
 | 
			
		||||
                        invertButton.isVisible = true
 | 
			
		||||
                        invertButton.isCheckable = true
 | 
			
		||||
                        invertButton.isChecked = buttonInvert
 | 
			
		||||
                        invertButton.setOnMenuItemClickListener {
 | 
			
		||||
                            params.set("inverted", !buttonInvert)
 | 
			
		||||
                            true
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        setThreshold.isVisible = true
 | 
			
		||||
                        val thresholdSetting = object : AbstractIntSetting {
 | 
			
		||||
                            override val key = ""
 | 
			
		||||
 | 
			
		||||
                            override fun getInt(needsGlobal: Boolean): Int =
 | 
			
		||||
                                (params.get("threshold", 0.5f) * 100).toInt()
 | 
			
		||||
 | 
			
		||||
                            override fun setInt(value: Int) {
 | 
			
		||||
                                params.set("threshold", value.toFloat() / 100)
 | 
			
		||||
                                NativeInput.setButtonParam(
 | 
			
		||||
                                    item.playerIndex,
 | 
			
		||||
                                    item.nativeButton,
 | 
			
		||||
                                    params
 | 
			
		||||
                                )
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            override val defaultValue = 50
 | 
			
		||||
 | 
			
		||||
                            override fun getValueAsString(needsGlobal: Boolean): String =
 | 
			
		||||
                                getInt(needsGlobal).toString()
 | 
			
		||||
 | 
			
		||||
                            override fun reset() = setInt(defaultValue)
 | 
			
		||||
                        }
 | 
			
		||||
                        setThreshold.setOnMenuItemClickListener {
 | 
			
		||||
                            onSliderClick(
 | 
			
		||||
                                SliderSetting(thresholdSetting, R.string.set_threshold),
 | 
			
		||||
                                position
 | 
			
		||||
                            )
 | 
			
		||||
                            true
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        val axisToggle = params.get("toggle", false)
 | 
			
		||||
                        toggleAxis.isVisible = true
 | 
			
		||||
                        toggleAxis.isCheckable = true
 | 
			
		||||
                        toggleAxis.isChecked = axisToggle
 | 
			
		||||
                        toggleAxis.setOnMenuItemClickListener {
 | 
			
		||||
                            params.set("toggle", !axisToggle)
 | 
			
		||||
                            true
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    popup.setOnDismissListener {
 | 
			
		||||
                        NativeInput.setButtonParam(item.playerIndex, item.nativeButton, params)
 | 
			
		||||
                        settingsViewModel.setAdapterItemChanged(position)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                is ModifierInputSetting -> {
 | 
			
		||||
                    val stickParams = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
 | 
			
		||||
                    val modifierParams = ParamPackage(stickParams.get("modifier", ""))
 | 
			
		||||
 | 
			
		||||
                    val invert = modifierParams.get("inverted", false)
 | 
			
		||||
                    invertButton.isVisible = true
 | 
			
		||||
                    invertButton.isCheckable = true
 | 
			
		||||
                    invertButton.isChecked = invert
 | 
			
		||||
                    invertButton.setOnMenuItemClickListener {
 | 
			
		||||
                        modifierParams.set("inverted", !invert)
 | 
			
		||||
                        stickParams.set("modifier", modifierParams.serialize())
 | 
			
		||||
                        true
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    val toggle = modifierParams.get("toggle", false)
 | 
			
		||||
                    toggleButton.isVisible = true
 | 
			
		||||
                    toggleButton.isCheckable = true
 | 
			
		||||
                    toggleButton.isChecked = toggle
 | 
			
		||||
                    toggleButton.setOnMenuItemClickListener {
 | 
			
		||||
                        modifierParams.set("toggle", !toggle)
 | 
			
		||||
                        stickParams.set("modifier", modifierParams.serialize())
 | 
			
		||||
                        true
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    popup.setOnDismissListener {
 | 
			
		||||
                        NativeInput.setStickParam(
 | 
			
		||||
                            item.playerIndex,
 | 
			
		||||
                            item.nativeAnalog,
 | 
			
		||||
                            stickParams
 | 
			
		||||
                        )
 | 
			
		||||
                        settingsViewModel.setAdapterItemChanged(position)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        popup.show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onLongClick(item: SettingsItem, position: Int): Boolean {
 | 
			
		||||
        SettingsDialogFragment.newInstance(
 | 
			
		||||
            settingsViewModel,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.fragments
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.ui
 | 
			
		||||
 | 
			
		||||
import android.app.Dialog
 | 
			
		||||
import android.content.DialogInterface
 | 
			
		||||
@@ -19,11 +19,16 @@ import com.google.android.material.slider.Slider
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.DialogSliderBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.IntSingleChoiceSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.model.SettingsViewModel
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.ParamPackage
 | 
			
		||||
 | 
			
		||||
class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener {
 | 
			
		||||
    private var type = 0
 | 
			
		||||
@@ -50,8 +55,49 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
 | 
			
		||||
                MaterialAlertDialogBuilder(requireContext())
 | 
			
		||||
                    .setMessage(R.string.reset_setting_confirmation)
 | 
			
		||||
                    .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
 | 
			
		||||
                        settingsViewModel.clickedItem!!.setting.reset()
 | 
			
		||||
                        settingsViewModel.setAdapterItemChanged(position)
 | 
			
		||||
                        when (val item = settingsViewModel.clickedItem) {
 | 
			
		||||
                            is AnalogInputSetting -> {
 | 
			
		||||
                                val stickParam = NativeInput.getStickParam(
 | 
			
		||||
                                    item.playerIndex,
 | 
			
		||||
                                    item.nativeAnalog
 | 
			
		||||
                                )
 | 
			
		||||
                                if (stickParam.get("engine", "") == "analog_from_button") {
 | 
			
		||||
                                    when (item.analogDirection) {
 | 
			
		||||
                                        AnalogDirection.Up -> stickParam.erase("up")
 | 
			
		||||
                                        AnalogDirection.Down -> stickParam.erase("down")
 | 
			
		||||
                                        AnalogDirection.Left -> stickParam.erase("left")
 | 
			
		||||
                                        AnalogDirection.Right -> stickParam.erase("right")
 | 
			
		||||
                                    }
 | 
			
		||||
                                    NativeInput.setStickParam(
 | 
			
		||||
                                        item.playerIndex,
 | 
			
		||||
                                        item.nativeAnalog,
 | 
			
		||||
                                        stickParam
 | 
			
		||||
                                    )
 | 
			
		||||
                                    settingsViewModel.setAdapterItemChanged(position)
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    NativeInput.setStickParam(
 | 
			
		||||
                                        item.playerIndex,
 | 
			
		||||
                                        item.nativeAnalog,
 | 
			
		||||
                                        ParamPackage()
 | 
			
		||||
                                    )
 | 
			
		||||
                                    settingsViewModel.setDatasetChanged(true)
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            is ButtonInputSetting -> {
 | 
			
		||||
                                NativeInput.setButtonParam(
 | 
			
		||||
                                    item.playerIndex,
 | 
			
		||||
                                    item.nativeButton,
 | 
			
		||||
                                    ParamPackage()
 | 
			
		||||
                                )
 | 
			
		||||
                                settingsViewModel.setAdapterItemChanged(position)
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            else -> {
 | 
			
		||||
                                settingsViewModel.clickedItem!!.setting.reset()
 | 
			
		||||
                                settingsViewModel.setAdapterItemChanged(position)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    .setNegativeButton(android.R.string.cancel, null)
 | 
			
		||||
                    .create()
 | 
			
		||||
@@ -61,7 +107,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
 | 
			
		||||
                val item = settingsViewModel.clickedItem as SingleChoiceSetting
 | 
			
		||||
                val value = getSelectionForSingleChoiceValue(item)
 | 
			
		||||
                MaterialAlertDialogBuilder(requireContext())
 | 
			
		||||
                    .setTitle(item.nameId)
 | 
			
		||||
                    .setTitle(item.title)
 | 
			
		||||
                    .setSingleChoiceItems(item.choicesId, value, this)
 | 
			
		||||
                    .create()
 | 
			
		||||
            }
 | 
			
		||||
@@ -81,7 +127,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                MaterialAlertDialogBuilder(requireContext())
 | 
			
		||||
                    .setTitle(item.nameId)
 | 
			
		||||
                    .setTitle(item.title)
 | 
			
		||||
                    .setView(sliderBinding.root)
 | 
			
		||||
                    .setPositiveButton(android.R.string.ok, this)
 | 
			
		||||
                    .setNegativeButton(android.R.string.cancel, defaultCancelListener)
 | 
			
		||||
@@ -91,8 +137,16 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
 | 
			
		||||
            SettingsItem.TYPE_STRING_SINGLE_CHOICE -> {
 | 
			
		||||
                val item = settingsViewModel.clickedItem as StringSingleChoiceSetting
 | 
			
		||||
                MaterialAlertDialogBuilder(requireContext())
 | 
			
		||||
                    .setTitle(item.nameId)
 | 
			
		||||
                    .setSingleChoiceItems(item.choices, item.selectValueIndex, this)
 | 
			
		||||
                    .setTitle(item.title)
 | 
			
		||||
                    .setSingleChoiceItems(item.choices, item.selectedValueIndex, this)
 | 
			
		||||
                    .create()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            SettingsItem.TYPE_INT_SINGLE_CHOICE -> {
 | 
			
		||||
                val item = settingsViewModel.clickedItem as IntSingleChoiceSetting
 | 
			
		||||
                MaterialAlertDialogBuilder(requireContext())
 | 
			
		||||
                    .setTitle(item.title)
 | 
			
		||||
                    .setSingleChoiceItems(item.choices, item.selectedValueIndex, this)
 | 
			
		||||
                    .create()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -145,6 +199,12 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
 | 
			
		||||
                scSetting.setSelectedValue(value)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            is IntSingleChoiceSetting -> {
 | 
			
		||||
                val scSetting = settingsViewModel.clickedItem as IntSingleChoiceSetting
 | 
			
		||||
                val value = scSetting.getValueAt(which)
 | 
			
		||||
                scSetting.setSelectedValue(value)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            is SliderSetting -> {
 | 
			
		||||
                val sliderSetting = settingsViewModel.clickedItem as SliderSetting
 | 
			
		||||
                sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value)
 | 
			
		||||
@@ -24,8 +24,9 @@ import kotlinx.coroutines.flow.collectLatest
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
 | 
			
		||||
import org.yuzu.yuzu_emu.model.SettingsViewModel
 | 
			
		||||
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
 | 
			
		||||
 | 
			
		||||
class SettingsFragment : Fragment() {
 | 
			
		||||
@@ -45,6 +46,12 @@ class SettingsFragment : Fragment() {
 | 
			
		||||
        returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
 | 
			
		||||
        reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
 | 
			
		||||
        exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
 | 
			
		||||
 | 
			
		||||
        val playerIndex = getPlayerIndex()
 | 
			
		||||
        if (playerIndex != -1) {
 | 
			
		||||
            NativeInput.loadInputProfiles()
 | 
			
		||||
            NativeInput.reloadInputDevices()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateView(
 | 
			
		||||
@@ -57,8 +64,9 @@ class SettingsFragment : Fragment() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // This is using the correct scope, lint is just acting up
 | 
			
		||||
    @SuppressLint("UnsafeRepeatOnLifecycleDetector")
 | 
			
		||||
    @SuppressLint("UnsafeRepeatOnLifecycleDetector", "NotifyDataSetChanged")
 | 
			
		||||
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onViewCreated(view, savedInstanceState)
 | 
			
		||||
        settingsAdapter = SettingsAdapter(this, requireContext())
 | 
			
		||||
        presenter = SettingsFragmentPresenter(
 | 
			
		||||
            settingsViewModel,
 | 
			
		||||
@@ -71,7 +79,17 @@ class SettingsFragment : Fragment() {
 | 
			
		||||
        ) {
 | 
			
		||||
            args.game!!.title
 | 
			
		||||
        } else {
 | 
			
		||||
            getString(args.menuTag.titleId)
 | 
			
		||||
            when (args.menuTag) {
 | 
			
		||||
                Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> Settings.getPlayerString(1)
 | 
			
		||||
                Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> Settings.getPlayerString(2)
 | 
			
		||||
                Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> Settings.getPlayerString(3)
 | 
			
		||||
                Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> Settings.getPlayerString(4)
 | 
			
		||||
                Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> Settings.getPlayerString(5)
 | 
			
		||||
                Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> Settings.getPlayerString(6)
 | 
			
		||||
                Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> Settings.getPlayerString(7)
 | 
			
		||||
                Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> Settings.getPlayerString(8)
 | 
			
		||||
                else -> getString(args.menuTag.titleId)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        binding.listSettings.apply {
 | 
			
		||||
            adapter = settingsAdapter
 | 
			
		||||
@@ -93,6 +111,55 @@ class SettingsFragment : Fragment() {
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            launch {
 | 
			
		||||
                repeatOnLifecycle(Lifecycle.State.STARTED) {
 | 
			
		||||
                    settingsViewModel.adapterItemChanged.collect {
 | 
			
		||||
                        if (it != -1) {
 | 
			
		||||
                            settingsAdapter?.notifyItemChanged(it)
 | 
			
		||||
                            settingsViewModel.setAdapterItemChanged(-1)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            launch {
 | 
			
		||||
                repeatOnLifecycle(Lifecycle.State.STARTED) {
 | 
			
		||||
                    settingsViewModel.datasetChanged.collect {
 | 
			
		||||
                        if (it) {
 | 
			
		||||
                            settingsAdapter?.notifyDataSetChanged()
 | 
			
		||||
                            settingsViewModel.setDatasetChanged(false)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            launch {
 | 
			
		||||
                repeatOnLifecycle(Lifecycle.State.CREATED) {
 | 
			
		||||
                    settingsViewModel.reloadListAndNotifyDataset.collectLatest {
 | 
			
		||||
                        if (it) {
 | 
			
		||||
                            settingsViewModel.setReloadListAndNotifyDataset(false)
 | 
			
		||||
                            presenter.loadSettingsList(true)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            launch {
 | 
			
		||||
                repeatOnLifecycle(Lifecycle.State.CREATED) {
 | 
			
		||||
                    settingsViewModel.shouldShowResetInputDialog.collectLatest {
 | 
			
		||||
                        if (it) {
 | 
			
		||||
                            MessageDialogFragment.newInstance(
 | 
			
		||||
                                activity = requireActivity(),
 | 
			
		||||
                                titleId = R.string.reset_mapping,
 | 
			
		||||
                                descriptionId = R.string.reset_mapping_description,
 | 
			
		||||
                                positiveAction = {
 | 
			
		||||
                                    NativeInput.resetControllerMappings(getPlayerIndex())
 | 
			
		||||
                                    settingsViewModel.setReloadListAndNotifyDataset(true)
 | 
			
		||||
                                },
 | 
			
		||||
                                negativeAction = {}
 | 
			
		||||
                            ).show(parentFragmentManager, MessageDialogFragment.TAG)
 | 
			
		||||
                            settingsViewModel.setShouldShowResetInputDialog(false)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (args.menuTag == Settings.MenuTag.SECTION_ROOT) {
 | 
			
		||||
@@ -115,6 +182,19 @@ class SettingsFragment : Fragment() {
 | 
			
		||||
        setInsets()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getPlayerIndex(): Int =
 | 
			
		||||
        when (args.menuTag) {
 | 
			
		||||
            Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> 0
 | 
			
		||||
            Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> 1
 | 
			
		||||
            Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> 2
 | 
			
		||||
            Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> 3
 | 
			
		||||
            Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> 4
 | 
			
		||||
            Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> 5
 | 
			
		||||
            Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> 6
 | 
			
		||||
            Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> 7
 | 
			
		||||
            else -> -1
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    private fun setInsets() {
 | 
			
		||||
        ViewCompat.setOnApplyWindowInsetsListener(
 | 
			
		||||
            binding.root
 | 
			
		||||
 
 | 
			
		||||
@@ -3,11 +3,17 @@
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.ui
 | 
			
		||||
 | 
			
		||||
import android.annotation.SuppressLint
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import org.yuzu.yuzu_emu.NativeLibrary
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
import org.yuzu.yuzu_emu.YuzuApplication
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.NativeButton
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
 | 
			
		||||
@@ -15,18 +21,21 @@ import org.yuzu.yuzu_emu.features.settings.model.ByteSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.LongSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.Settings.MenuTag
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.*
 | 
			
		||||
import org.yuzu.yuzu_emu.model.SettingsViewModel
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.InputHandler
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.NativeConfig
 | 
			
		||||
 | 
			
		||||
class SettingsFragmentPresenter(
 | 
			
		||||
    private val settingsViewModel: SettingsViewModel,
 | 
			
		||||
    private val adapter: SettingsAdapter,
 | 
			
		||||
    private var menuTag: Settings.MenuTag
 | 
			
		||||
    private var menuTag: MenuTag
 | 
			
		||||
) {
 | 
			
		||||
    private var settingsList = ArrayList<SettingsItem>()
 | 
			
		||||
 | 
			
		||||
    private val context get() = YuzuApplication.appContext
 | 
			
		||||
 | 
			
		||||
    // Extension for altering settings list based on each setting's properties
 | 
			
		||||
    fun ArrayList<SettingsItem>.add(key: String) {
 | 
			
		||||
        val item = SettingsItem.settingsItems[key]!!
 | 
			
		||||
@@ -53,73 +62,90 @@ class SettingsFragmentPresenter(
 | 
			
		||||
        add(item)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Allows you to show/hide abstract settings based on the paired setting key
 | 
			
		||||
    fun ArrayList<SettingsItem>.addAbstract(item: SettingsItem) {
 | 
			
		||||
        val pairedSettingKey = item.setting.pairedSettingKey
 | 
			
		||||
        if (pairedSettingKey.isNotEmpty()) {
 | 
			
		||||
            val pairedSettingsItem =
 | 
			
		||||
                this.firstOrNull { it.setting.key == pairedSettingKey } ?: return
 | 
			
		||||
            val pairedSetting = pairedSettingsItem.setting as AbstractBooleanSetting
 | 
			
		||||
            if (!pairedSetting.getBoolean(!NativeConfig.isPerGameConfigLoaded())) return
 | 
			
		||||
        }
 | 
			
		||||
        add(item)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onViewCreated() {
 | 
			
		||||
        loadSettingsList()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun loadSettingsList() {
 | 
			
		||||
    @SuppressLint("NotifyDataSetChanged")
 | 
			
		||||
    fun loadSettingsList(notifyDataSetChanged: Boolean = false) {
 | 
			
		||||
        val sl = ArrayList<SettingsItem>()
 | 
			
		||||
        when (menuTag) {
 | 
			
		||||
            Settings.MenuTag.SECTION_ROOT -> addConfigSettings(sl)
 | 
			
		||||
            Settings.MenuTag.SECTION_SYSTEM -> addSystemSettings(sl)
 | 
			
		||||
            Settings.MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl)
 | 
			
		||||
            Settings.MenuTag.SECTION_AUDIO -> addAudioSettings(sl)
 | 
			
		||||
            Settings.MenuTag.SECTION_THEME -> addThemeSettings(sl)
 | 
			
		||||
            Settings.MenuTag.SECTION_DEBUG -> addDebugSettings(sl)
 | 
			
		||||
            else -> {
 | 
			
		||||
                val context = YuzuApplication.appContext
 | 
			
		||||
                Toast.makeText(
 | 
			
		||||
                    context,
 | 
			
		||||
                    context.getString(R.string.unimplemented_menu),
 | 
			
		||||
                    Toast.LENGTH_SHORT
 | 
			
		||||
                ).show()
 | 
			
		||||
                return
 | 
			
		||||
            }
 | 
			
		||||
            MenuTag.SECTION_ROOT -> addConfigSettings(sl)
 | 
			
		||||
            MenuTag.SECTION_SYSTEM -> addSystemSettings(sl)
 | 
			
		||||
            MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl)
 | 
			
		||||
            MenuTag.SECTION_AUDIO -> addAudioSettings(sl)
 | 
			
		||||
            MenuTag.SECTION_INPUT -> addInputSettings(sl)
 | 
			
		||||
            MenuTag.SECTION_INPUT_PLAYER_ONE -> addInputPlayer(sl, 0)
 | 
			
		||||
            MenuTag.SECTION_INPUT_PLAYER_TWO -> addInputPlayer(sl, 1)
 | 
			
		||||
            MenuTag.SECTION_INPUT_PLAYER_THREE -> addInputPlayer(sl, 2)
 | 
			
		||||
            MenuTag.SECTION_INPUT_PLAYER_FOUR -> addInputPlayer(sl, 3)
 | 
			
		||||
            MenuTag.SECTION_INPUT_PLAYER_FIVE -> addInputPlayer(sl, 4)
 | 
			
		||||
            MenuTag.SECTION_INPUT_PLAYER_SIX -> addInputPlayer(sl, 5)
 | 
			
		||||
            MenuTag.SECTION_INPUT_PLAYER_SEVEN -> addInputPlayer(sl, 6)
 | 
			
		||||
            MenuTag.SECTION_INPUT_PLAYER_EIGHT -> addInputPlayer(sl, 7)
 | 
			
		||||
            MenuTag.SECTION_THEME -> addThemeSettings(sl)
 | 
			
		||||
            MenuTag.SECTION_DEBUG -> addDebugSettings(sl)
 | 
			
		||||
        }
 | 
			
		||||
        settingsList = sl
 | 
			
		||||
        adapter.submitList(settingsList)
 | 
			
		||||
        adapter.submitList(settingsList) {
 | 
			
		||||
            if (notifyDataSetChanged) {
 | 
			
		||||
                adapter.notifyDataSetChanged()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun addConfigSettings(sl: ArrayList<SettingsItem>) {
 | 
			
		||||
        sl.apply {
 | 
			
		||||
            add(
 | 
			
		||||
                SubmenuSetting(
 | 
			
		||||
                    R.string.preferences_system,
 | 
			
		||||
                    R.string.preferences_system_description,
 | 
			
		||||
                    R.drawable.ic_system_settings,
 | 
			
		||||
                    Settings.MenuTag.SECTION_SYSTEM
 | 
			
		||||
                    titleId = R.string.preferences_system,
 | 
			
		||||
                    descriptionId = R.string.preferences_system_description,
 | 
			
		||||
                    iconId = R.drawable.ic_system_settings,
 | 
			
		||||
                    menuKey = MenuTag.SECTION_SYSTEM
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            add(
 | 
			
		||||
                SubmenuSetting(
 | 
			
		||||
                    R.string.preferences_graphics,
 | 
			
		||||
                    R.string.preferences_graphics_description,
 | 
			
		||||
                    R.drawable.ic_graphics,
 | 
			
		||||
                    Settings.MenuTag.SECTION_RENDERER
 | 
			
		||||
                    titleId = R.string.preferences_graphics,
 | 
			
		||||
                    descriptionId = R.string.preferences_graphics_description,
 | 
			
		||||
                    iconId = R.drawable.ic_graphics,
 | 
			
		||||
                    menuKey = MenuTag.SECTION_RENDERER
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            add(
 | 
			
		||||
                SubmenuSetting(
 | 
			
		||||
                    R.string.preferences_audio,
 | 
			
		||||
                    R.string.preferences_audio_description,
 | 
			
		||||
                    R.drawable.ic_audio,
 | 
			
		||||
                    Settings.MenuTag.SECTION_AUDIO
 | 
			
		||||
                    titleId = R.string.preferences_audio,
 | 
			
		||||
                    descriptionId = R.string.preferences_audio_description,
 | 
			
		||||
                    iconId = R.drawable.ic_audio,
 | 
			
		||||
                    menuKey = MenuTag.SECTION_AUDIO
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            add(
 | 
			
		||||
                SubmenuSetting(
 | 
			
		||||
                    R.string.preferences_debug,
 | 
			
		||||
                    R.string.preferences_debug_description,
 | 
			
		||||
                    R.drawable.ic_code,
 | 
			
		||||
                    Settings.MenuTag.SECTION_DEBUG
 | 
			
		||||
                    titleId = R.string.preferences_debug,
 | 
			
		||||
                    descriptionId = R.string.preferences_debug_description,
 | 
			
		||||
                    iconId = R.drawable.ic_code,
 | 
			
		||||
                    menuKey = MenuTag.SECTION_DEBUG
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            add(
 | 
			
		||||
                RunnableSetting(
 | 
			
		||||
                    R.string.reset_to_default,
 | 
			
		||||
                    R.string.reset_to_default_description,
 | 
			
		||||
                    false,
 | 
			
		||||
                    R.drawable.ic_restore
 | 
			
		||||
                    titleId = R.string.reset_to_default,
 | 
			
		||||
                    descriptionId = R.string.reset_to_default_description,
 | 
			
		||||
                    isRunnable = !NativeLibrary.isRunning(),
 | 
			
		||||
                    iconId = R.drawable.ic_restore
 | 
			
		||||
                ) { settingsViewModel.setShouldShowResetSettingsDialog(true) }
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
@@ -164,6 +190,671 @@ class SettingsFragmentPresenter(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun addInputSettings(sl: ArrayList<SettingsItem>) {
 | 
			
		||||
        settingsViewModel.currentDevice = 0
 | 
			
		||||
 | 
			
		||||
        if (NativeConfig.isPerGameConfigLoaded()) {
 | 
			
		||||
            NativeInput.loadInputProfiles()
 | 
			
		||||
            val profiles = NativeInput.getInputProfileNames().toMutableList()
 | 
			
		||||
            profiles.add(0, "")
 | 
			
		||||
            val prettyProfiles = profiles.toTypedArray()
 | 
			
		||||
            prettyProfiles[0] =
 | 
			
		||||
                context.getString(R.string.use_global_input_configuration)
 | 
			
		||||
            sl.apply {
 | 
			
		||||
                for (i in 0 until 8) {
 | 
			
		||||
                    add(
 | 
			
		||||
                        IntSingleChoiceSetting(
 | 
			
		||||
                            getPerGameProfileSetting(profiles, i),
 | 
			
		||||
                            titleString = getPlayerProfileString(i + 1),
 | 
			
		||||
                            choices = prettyProfiles,
 | 
			
		||||
                            values = IntArray(profiles.size) { it }.toTypedArray()
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val getConnectedIcon: (Int) -> Int = { playerIndex: Int ->
 | 
			
		||||
            if (NativeInput.getIsConnected(playerIndex)) {
 | 
			
		||||
                R.drawable.ic_controller
 | 
			
		||||
            } else {
 | 
			
		||||
                R.drawable.ic_controller_disconnected
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val inputSettings = NativeConfig.getInputSettings(true)
 | 
			
		||||
        sl.apply {
 | 
			
		||||
            add(
 | 
			
		||||
                SubmenuSetting(
 | 
			
		||||
                    titleString = Settings.getPlayerString(1),
 | 
			
		||||
                    descriptionString = inputSettings[0].profileName,
 | 
			
		||||
                    menuKey = MenuTag.SECTION_INPUT_PLAYER_ONE,
 | 
			
		||||
                    iconId = getConnectedIcon(0)
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            add(
 | 
			
		||||
                SubmenuSetting(
 | 
			
		||||
                    titleString = Settings.getPlayerString(2),
 | 
			
		||||
                    descriptionString = inputSettings[1].profileName,
 | 
			
		||||
                    menuKey = MenuTag.SECTION_INPUT_PLAYER_TWO,
 | 
			
		||||
                    iconId = getConnectedIcon(1)
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            add(
 | 
			
		||||
                SubmenuSetting(
 | 
			
		||||
                    titleString = Settings.getPlayerString(3),
 | 
			
		||||
                    descriptionString = inputSettings[2].profileName,
 | 
			
		||||
                    menuKey = MenuTag.SECTION_INPUT_PLAYER_THREE,
 | 
			
		||||
                    iconId = getConnectedIcon(2)
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            add(
 | 
			
		||||
                SubmenuSetting(
 | 
			
		||||
                    titleString = Settings.getPlayerString(4),
 | 
			
		||||
                    descriptionString = inputSettings[3].profileName,
 | 
			
		||||
                    menuKey = MenuTag.SECTION_INPUT_PLAYER_FOUR,
 | 
			
		||||
                    iconId = getConnectedIcon(3)
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            add(
 | 
			
		||||
                SubmenuSetting(
 | 
			
		||||
                    titleString = Settings.getPlayerString(5),
 | 
			
		||||
                    descriptionString = inputSettings[4].profileName,
 | 
			
		||||
                    menuKey = MenuTag.SECTION_INPUT_PLAYER_FIVE,
 | 
			
		||||
                    iconId = getConnectedIcon(4)
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            add(
 | 
			
		||||
                SubmenuSetting(
 | 
			
		||||
                    titleString = Settings.getPlayerString(6),
 | 
			
		||||
                    descriptionString = inputSettings[5].profileName,
 | 
			
		||||
                    menuKey = MenuTag.SECTION_INPUT_PLAYER_SIX,
 | 
			
		||||
                    iconId = getConnectedIcon(5)
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            add(
 | 
			
		||||
                SubmenuSetting(
 | 
			
		||||
                    titleString = Settings.getPlayerString(7),
 | 
			
		||||
                    descriptionString = inputSettings[6].profileName,
 | 
			
		||||
                    menuKey = MenuTag.SECTION_INPUT_PLAYER_SEVEN,
 | 
			
		||||
                    iconId = getConnectedIcon(6)
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            add(
 | 
			
		||||
                SubmenuSetting(
 | 
			
		||||
                    titleString = Settings.getPlayerString(8),
 | 
			
		||||
                    descriptionString = inputSettings[7].profileName,
 | 
			
		||||
                    menuKey = MenuTag.SECTION_INPUT_PLAYER_EIGHT,
 | 
			
		||||
                    iconId = getConnectedIcon(7)
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getPlayerProfileString(player: Int): String =
 | 
			
		||||
        context.getString(R.string.player_num_profile, player)
 | 
			
		||||
 | 
			
		||||
    private fun getPerGameProfileSetting(
 | 
			
		||||
        profiles: List<String>,
 | 
			
		||||
        playerIndex: Int
 | 
			
		||||
    ): AbstractIntSetting {
 | 
			
		||||
        return object : AbstractIntSetting {
 | 
			
		||||
            private val players
 | 
			
		||||
                get() = NativeConfig.getInputSettings(false)
 | 
			
		||||
 | 
			
		||||
            override val key = ""
 | 
			
		||||
 | 
			
		||||
            override fun getInt(needsGlobal: Boolean): Int {
 | 
			
		||||
                val currentProfile = players[playerIndex].profileName
 | 
			
		||||
                profiles.forEachIndexed { i, profile ->
 | 
			
		||||
                    if (profile == currentProfile) {
 | 
			
		||||
                        return i
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                return 0
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun setInt(value: Int) {
 | 
			
		||||
                NativeInput.loadPerGameConfiguration(playerIndex, value, profiles[value])
 | 
			
		||||
                NativeInput.connectControllers(playerIndex)
 | 
			
		||||
                NativeConfig.saveControlPlayerValues()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override val defaultValue = 0
 | 
			
		||||
 | 
			
		||||
            override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
 | 
			
		||||
 | 
			
		||||
            override fun reset() = setInt(defaultValue)
 | 
			
		||||
 | 
			
		||||
            override var global = true
 | 
			
		||||
 | 
			
		||||
            override val isRuntimeModifiable = true
 | 
			
		||||
 | 
			
		||||
            override val isSaveable = true
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun addInputPlayer(sl: ArrayList<SettingsItem>, playerIndex: Int) {
 | 
			
		||||
        sl.apply {
 | 
			
		||||
            val connectedSetting = object : AbstractBooleanSetting {
 | 
			
		||||
                override val key = "connected"
 | 
			
		||||
 | 
			
		||||
                override fun getBoolean(needsGlobal: Boolean): Boolean =
 | 
			
		||||
                    NativeInput.getIsConnected(playerIndex)
 | 
			
		||||
 | 
			
		||||
                override fun setBoolean(value: Boolean) =
 | 
			
		||||
                    NativeInput.connectControllers(playerIndex, value)
 | 
			
		||||
 | 
			
		||||
                override val defaultValue = playerIndex == 0
 | 
			
		||||
 | 
			
		||||
                override fun getValueAsString(needsGlobal: Boolean): String =
 | 
			
		||||
                    getBoolean(needsGlobal).toString()
 | 
			
		||||
 | 
			
		||||
                override fun reset() = setBoolean(defaultValue)
 | 
			
		||||
            }
 | 
			
		||||
            add(SwitchSetting(connectedSetting, R.string.connected))
 | 
			
		||||
 | 
			
		||||
            val styleTags = NativeInput.getSupportedStyleTags(playerIndex)
 | 
			
		||||
            val npadType = object : AbstractIntSetting {
 | 
			
		||||
                override val key = "npad_type"
 | 
			
		||||
                override fun getInt(needsGlobal: Boolean): Int {
 | 
			
		||||
                    val styleIndex = NativeInput.getStyleIndex(playerIndex)
 | 
			
		||||
                    return styleTags.indexOfFirst { it == styleIndex }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun setInt(value: Int) {
 | 
			
		||||
                    NativeInput.setStyleIndex(playerIndex, styleTags[value])
 | 
			
		||||
                    settingsViewModel.setReloadListAndNotifyDataset(true)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override val defaultValue = NpadStyleIndex.Fullkey.int
 | 
			
		||||
                override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
 | 
			
		||||
                override fun reset() = setInt(defaultValue)
 | 
			
		||||
                override val pairedSettingKey: String = "connected"
 | 
			
		||||
            }
 | 
			
		||||
            addAbstract(
 | 
			
		||||
                IntSingleChoiceSetting(
 | 
			
		||||
                    npadType,
 | 
			
		||||
                    titleId = R.string.controller_type,
 | 
			
		||||
                    choices = styleTags.map { context.getString(it.nameId) }
 | 
			
		||||
                        .toTypedArray(),
 | 
			
		||||
                    values = IntArray(styleTags.size) { it }.toTypedArray()
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            InputHandler.updateControllerData()
 | 
			
		||||
 | 
			
		||||
            val autoMappingSetting = object : AbstractIntSetting {
 | 
			
		||||
                override val key = "auto_mapping_device"
 | 
			
		||||
 | 
			
		||||
                override fun getInt(needsGlobal: Boolean): Int = -1
 | 
			
		||||
 | 
			
		||||
                override fun setInt(value: Int) {
 | 
			
		||||
                    val registeredController = InputHandler.registeredControllers[value + 1]
 | 
			
		||||
                    val displayName = registeredController.get(
 | 
			
		||||
                        "display",
 | 
			
		||||
                        context.getString(R.string.unknown)
 | 
			
		||||
                    )
 | 
			
		||||
                    NativeInput.updateMappingsWithDefault(
 | 
			
		||||
                        playerIndex,
 | 
			
		||||
                        registeredController,
 | 
			
		||||
                        displayName
 | 
			
		||||
                    )
 | 
			
		||||
                    Toast.makeText(
 | 
			
		||||
                        context,
 | 
			
		||||
                        context.getString(R.string.attempted_auto_map, displayName),
 | 
			
		||||
                        Toast.LENGTH_SHORT
 | 
			
		||||
                    ).show()
 | 
			
		||||
                    settingsViewModel.setReloadListAndNotifyDataset(true)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override val defaultValue = -1
 | 
			
		||||
 | 
			
		||||
                override fun getValueAsString(needsGlobal: Boolean) = getInt().toString()
 | 
			
		||||
 | 
			
		||||
                override fun reset() = setInt(defaultValue)
 | 
			
		||||
 | 
			
		||||
                override val isRuntimeModifiable: Boolean = true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val unknownString = context.getString(R.string.unknown)
 | 
			
		||||
            val prettyAutoMappingControllerList = InputHandler.registeredControllers.mapNotNull {
 | 
			
		||||
                val port = it.get("port", -1)
 | 
			
		||||
                return@mapNotNull if (port == 100 || port == -1) {
 | 
			
		||||
                    null
 | 
			
		||||
                } else {
 | 
			
		||||
                    it.get("display", unknownString)
 | 
			
		||||
                }
 | 
			
		||||
            }.toTypedArray()
 | 
			
		||||
            add(
 | 
			
		||||
                IntSingleChoiceSetting(
 | 
			
		||||
                    autoMappingSetting,
 | 
			
		||||
                    titleId = R.string.auto_map,
 | 
			
		||||
                    descriptionId = R.string.auto_map_description,
 | 
			
		||||
                    choices = prettyAutoMappingControllerList,
 | 
			
		||||
                    values = IntArray(prettyAutoMappingControllerList.size) { it }.toTypedArray()
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            val mappingFilterSetting = object : AbstractIntSetting {
 | 
			
		||||
                override val key = "mapping_filter"
 | 
			
		||||
 | 
			
		||||
                override fun getInt(needsGlobal: Boolean): Int = settingsViewModel.currentDevice
 | 
			
		||||
 | 
			
		||||
                override fun setInt(value: Int) {
 | 
			
		||||
                    settingsViewModel.currentDevice = value
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override val defaultValue = 0
 | 
			
		||||
 | 
			
		||||
                override fun getValueAsString(needsGlobal: Boolean) = getInt().toString()
 | 
			
		||||
 | 
			
		||||
                override fun reset() = setInt(defaultValue)
 | 
			
		||||
 | 
			
		||||
                override val isRuntimeModifiable: Boolean = true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val prettyControllerList = InputHandler.registeredControllers.mapNotNull {
 | 
			
		||||
                return@mapNotNull if (it.get("port", 0) == 100) {
 | 
			
		||||
                    null
 | 
			
		||||
                } else {
 | 
			
		||||
                    it.get("display", unknownString)
 | 
			
		||||
                }
 | 
			
		||||
            }.toTypedArray()
 | 
			
		||||
            add(
 | 
			
		||||
                IntSingleChoiceSetting(
 | 
			
		||||
                    mappingFilterSetting,
 | 
			
		||||
                    titleId = R.string.input_mapping_filter,
 | 
			
		||||
                    descriptionId = R.string.input_mapping_filter_description,
 | 
			
		||||
                    choices = prettyControllerList,
 | 
			
		||||
                    values = IntArray(prettyControllerList.size) { it }.toTypedArray()
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            add(InputProfileSetting(playerIndex))
 | 
			
		||||
            add(
 | 
			
		||||
                RunnableSetting(titleId = R.string.reset_to_default, isRunnable = true) {
 | 
			
		||||
                    settingsViewModel.setShouldShowResetInputDialog(true)
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            val styleIndex = NativeInput.getStyleIndex(playerIndex)
 | 
			
		||||
 | 
			
		||||
            // Buttons
 | 
			
		||||
            when (styleIndex) {
 | 
			
		||||
                NpadStyleIndex.Fullkey,
 | 
			
		||||
                NpadStyleIndex.Handheld,
 | 
			
		||||
                NpadStyleIndex.JoyconDual -> {
 | 
			
		||||
                    add(HeaderSetting(R.string.buttons))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.button_plus))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.Minus, R.string.button_minus))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.Home, R.string.button_home))
 | 
			
		||||
                    add(
 | 
			
		||||
                        ButtonInputSetting(
 | 
			
		||||
                            playerIndex,
 | 
			
		||||
                            NativeButton.Capture,
 | 
			
		||||
                            R.string.button_capture
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                NpadStyleIndex.JoyconLeft -> {
 | 
			
		||||
                    add(HeaderSetting(R.string.buttons))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.Minus, R.string.button_minus))
 | 
			
		||||
                    add(
 | 
			
		||||
                        ButtonInputSetting(
 | 
			
		||||
                            playerIndex,
 | 
			
		||||
                            NativeButton.Capture,
 | 
			
		||||
                            R.string.button_capture
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                NpadStyleIndex.JoyconRight -> {
 | 
			
		||||
                    add(HeaderSetting(R.string.buttons))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.button_plus))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.Home, R.string.button_home))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                NpadStyleIndex.GameCube -> {
 | 
			
		||||
                    add(HeaderSetting(R.string.buttons))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.start_pause))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                else -> {
 | 
			
		||||
                    // No-op
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            when (styleIndex) {
 | 
			
		||||
                NpadStyleIndex.Fullkey,
 | 
			
		||||
                NpadStyleIndex.Handheld,
 | 
			
		||||
                NpadStyleIndex.JoyconDual,
 | 
			
		||||
                NpadStyleIndex.JoyconLeft -> {
 | 
			
		||||
                    add(HeaderSetting(R.string.dpad))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.DUp, R.string.up))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.DDown, R.string.down))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.DLeft, R.string.left))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.DRight, R.string.right))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                else -> {
 | 
			
		||||
                    // No-op
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Left stick
 | 
			
		||||
            when (styleIndex) {
 | 
			
		||||
                NpadStyleIndex.Fullkey,
 | 
			
		||||
                NpadStyleIndex.Handheld,
 | 
			
		||||
                NpadStyleIndex.JoyconDual,
 | 
			
		||||
                NpadStyleIndex.JoyconLeft -> {
 | 
			
		||||
                    add(HeaderSetting(R.string.left_stick))
 | 
			
		||||
                    addAll(getStickDirections(playerIndex, NativeAnalog.LStick))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.LStick, R.string.pressed))
 | 
			
		||||
                    addAll(getExtraStickSettings(playerIndex, NativeAnalog.LStick))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                NpadStyleIndex.GameCube -> {
 | 
			
		||||
                    add(HeaderSetting(R.string.control_stick))
 | 
			
		||||
                    addAll(getStickDirections(playerIndex, NativeAnalog.LStick))
 | 
			
		||||
                    addAll(getExtraStickSettings(playerIndex, NativeAnalog.LStick))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                else -> {
 | 
			
		||||
                    // No-op
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Right stick
 | 
			
		||||
            when (styleIndex) {
 | 
			
		||||
                NpadStyleIndex.Fullkey,
 | 
			
		||||
                NpadStyleIndex.Handheld,
 | 
			
		||||
                NpadStyleIndex.JoyconDual,
 | 
			
		||||
                NpadStyleIndex.JoyconRight -> {
 | 
			
		||||
                    add(HeaderSetting(R.string.right_stick))
 | 
			
		||||
                    addAll(getStickDirections(playerIndex, NativeAnalog.RStick))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.RStick, R.string.pressed))
 | 
			
		||||
                    addAll(getExtraStickSettings(playerIndex, NativeAnalog.RStick))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                NpadStyleIndex.GameCube -> {
 | 
			
		||||
                    add(HeaderSetting(R.string.c_stick))
 | 
			
		||||
                    addAll(getStickDirections(playerIndex, NativeAnalog.RStick))
 | 
			
		||||
                    addAll(getExtraStickSettings(playerIndex, NativeAnalog.RStick))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                else -> {
 | 
			
		||||
                    // No-op
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // L/R, ZL/ZR, and SL/SR
 | 
			
		||||
            when (styleIndex) {
 | 
			
		||||
                NpadStyleIndex.Fullkey,
 | 
			
		||||
                NpadStyleIndex.Handheld -> {
 | 
			
		||||
                    add(HeaderSetting(R.string.triggers))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                NpadStyleIndex.JoyconDual -> {
 | 
			
		||||
                    add(HeaderSetting(R.string.triggers))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr))
 | 
			
		||||
                    add(
 | 
			
		||||
                        ButtonInputSetting(
 | 
			
		||||
                            playerIndex,
 | 
			
		||||
                            NativeButton.SLLeft,
 | 
			
		||||
                            R.string.button_sl_left
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                    add(
 | 
			
		||||
                        ButtonInputSetting(
 | 
			
		||||
                            playerIndex,
 | 
			
		||||
                            NativeButton.SRLeft,
 | 
			
		||||
                            R.string.button_sr_left
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                    add(
 | 
			
		||||
                        ButtonInputSetting(
 | 
			
		||||
                            playerIndex,
 | 
			
		||||
                            NativeButton.SLRight,
 | 
			
		||||
                            R.string.button_sl_right
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                    add(
 | 
			
		||||
                        ButtonInputSetting(
 | 
			
		||||
                            playerIndex,
 | 
			
		||||
                            NativeButton.SRRight,
 | 
			
		||||
                            R.string.button_sr_right
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                NpadStyleIndex.JoyconLeft -> {
 | 
			
		||||
                    add(HeaderSetting(R.string.triggers))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl))
 | 
			
		||||
                    add(
 | 
			
		||||
                        ButtonInputSetting(
 | 
			
		||||
                            playerIndex,
 | 
			
		||||
                            NativeButton.SLLeft,
 | 
			
		||||
                            R.string.button_sl_left
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                    add(
 | 
			
		||||
                        ButtonInputSetting(
 | 
			
		||||
                            playerIndex,
 | 
			
		||||
                            NativeButton.SRLeft,
 | 
			
		||||
                            R.string.button_sr_left
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                NpadStyleIndex.JoyconRight -> {
 | 
			
		||||
                    add(HeaderSetting(R.string.triggers))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr))
 | 
			
		||||
                    add(
 | 
			
		||||
                        ButtonInputSetting(
 | 
			
		||||
                            playerIndex,
 | 
			
		||||
                            NativeButton.SLRight,
 | 
			
		||||
                            R.string.button_sl_right
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                    add(
 | 
			
		||||
                        ButtonInputSetting(
 | 
			
		||||
                            playerIndex,
 | 
			
		||||
                            NativeButton.SRRight,
 | 
			
		||||
                            R.string.button_sr_right
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                NpadStyleIndex.GameCube -> {
 | 
			
		||||
                    add(HeaderSetting(R.string.triggers))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_z))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_l))
 | 
			
		||||
                    add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_r))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                else -> {
 | 
			
		||||
                    // No-op
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            add(HeaderSetting(R.string.vibration))
 | 
			
		||||
            val vibrationEnabledSetting = object : AbstractBooleanSetting {
 | 
			
		||||
                override val key = "vibration"
 | 
			
		||||
 | 
			
		||||
                override fun getBoolean(needsGlobal: Boolean): Boolean =
 | 
			
		||||
                    NativeConfig.getInputSettings(true)[playerIndex].vibrationEnabled
 | 
			
		||||
 | 
			
		||||
                override fun setBoolean(value: Boolean) {
 | 
			
		||||
                    val settings = NativeConfig.getInputSettings(true)
 | 
			
		||||
                    settings[playerIndex].vibrationEnabled = value
 | 
			
		||||
                    NativeConfig.setInputSettings(settings, true)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override val defaultValue = true
 | 
			
		||||
 | 
			
		||||
                override fun getValueAsString(needsGlobal: Boolean): String =
 | 
			
		||||
                    getBoolean(needsGlobal).toString()
 | 
			
		||||
 | 
			
		||||
                override fun reset() = setBoolean(defaultValue)
 | 
			
		||||
            }
 | 
			
		||||
            add(SwitchSetting(vibrationEnabledSetting, R.string.vibration))
 | 
			
		||||
 | 
			
		||||
            val useSystemVibratorSetting = object : AbstractBooleanSetting {
 | 
			
		||||
                override val key = ""
 | 
			
		||||
 | 
			
		||||
                override fun getBoolean(needsGlobal: Boolean): Boolean =
 | 
			
		||||
                    NativeConfig.getInputSettings(true)[playerIndex].useSystemVibrator
 | 
			
		||||
 | 
			
		||||
                override fun setBoolean(value: Boolean) {
 | 
			
		||||
                    val settings = NativeConfig.getInputSettings(true)
 | 
			
		||||
                    settings[playerIndex].useSystemVibrator = value
 | 
			
		||||
                    NativeConfig.setInputSettings(settings, true)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override val defaultValue = playerIndex == 0
 | 
			
		||||
 | 
			
		||||
                override fun getValueAsString(needsGlobal: Boolean): String =
 | 
			
		||||
                    getBoolean(needsGlobal).toString()
 | 
			
		||||
 | 
			
		||||
                override fun reset() = setBoolean(defaultValue)
 | 
			
		||||
 | 
			
		||||
                override val pairedSettingKey: String = "vibration"
 | 
			
		||||
            }
 | 
			
		||||
            addAbstract(SwitchSetting(useSystemVibratorSetting, R.string.use_system_vibrator))
 | 
			
		||||
 | 
			
		||||
            val vibrationStrengthSetting = object : AbstractIntSetting {
 | 
			
		||||
                override val key = ""
 | 
			
		||||
 | 
			
		||||
                override fun getInt(needsGlobal: Boolean): Int =
 | 
			
		||||
                    NativeConfig.getInputSettings(true)[playerIndex].vibrationStrength
 | 
			
		||||
 | 
			
		||||
                override fun setInt(value: Int) {
 | 
			
		||||
                    val settings = NativeConfig.getInputSettings(true)
 | 
			
		||||
                    settings[playerIndex].vibrationStrength = value
 | 
			
		||||
                    NativeConfig.setInputSettings(settings, true)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override val defaultValue = 100
 | 
			
		||||
 | 
			
		||||
                override fun getValueAsString(needsGlobal: Boolean): String =
 | 
			
		||||
                    getInt(needsGlobal).toString()
 | 
			
		||||
 | 
			
		||||
                override fun reset() = setInt(defaultValue)
 | 
			
		||||
 | 
			
		||||
                override val pairedSettingKey: String = "vibration"
 | 
			
		||||
            }
 | 
			
		||||
            addAbstract(
 | 
			
		||||
                SliderSetting(vibrationStrengthSetting, R.string.vibration_strength, units = "%")
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Convenience function for creating AbstractIntSettings for modifier range/stick range/stick deadzones
 | 
			
		||||
    private fun getStickIntSettingFromParam(
 | 
			
		||||
        playerIndex: Int,
 | 
			
		||||
        paramName: String,
 | 
			
		||||
        stick: NativeAnalog,
 | 
			
		||||
        defaultValue: Int
 | 
			
		||||
    ): AbstractIntSetting =
 | 
			
		||||
        object : AbstractIntSetting {
 | 
			
		||||
            val params get() = NativeInput.getStickParam(playerIndex, stick)
 | 
			
		||||
 | 
			
		||||
            override val key = ""
 | 
			
		||||
 | 
			
		||||
            override fun getInt(needsGlobal: Boolean): Int =
 | 
			
		||||
                (params.get(paramName, 0.15f) * 100).toInt()
 | 
			
		||||
 | 
			
		||||
            override fun setInt(value: Int) {
 | 
			
		||||
                val tempParams = params
 | 
			
		||||
                tempParams.set(paramName, value.toFloat() / 100)
 | 
			
		||||
                NativeInput.setStickParam(playerIndex, stick, tempParams)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override val defaultValue = defaultValue
 | 
			
		||||
 | 
			
		||||
            override fun getValueAsString(needsGlobal: Boolean): String =
 | 
			
		||||
                getInt(needsGlobal).toString()
 | 
			
		||||
 | 
			
		||||
            override fun reset() = setInt(defaultValue)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    private fun getExtraStickSettings(
 | 
			
		||||
        playerIndex: Int,
 | 
			
		||||
        nativeAnalog: NativeAnalog
 | 
			
		||||
    ): List<SettingsItem> {
 | 
			
		||||
        val stickIsController =
 | 
			
		||||
            NativeInput.isController(NativeInput.getStickParam(playerIndex, nativeAnalog))
 | 
			
		||||
        val modifierRangeSetting =
 | 
			
		||||
            getStickIntSettingFromParam(playerIndex, "modifier_scale", nativeAnalog, 50)
 | 
			
		||||
        val stickRangeSetting =
 | 
			
		||||
            getStickIntSettingFromParam(playerIndex, "range", nativeAnalog, 95)
 | 
			
		||||
        val stickDeadzoneSetting =
 | 
			
		||||
            getStickIntSettingFromParam(playerIndex, "deadzone", nativeAnalog, 15)
 | 
			
		||||
 | 
			
		||||
        val out = mutableListOf<SettingsItem>().apply {
 | 
			
		||||
            if (stickIsController) {
 | 
			
		||||
                add(SliderSetting(stickRangeSetting, titleId = R.string.range, min = 25, max = 150))
 | 
			
		||||
                add(SliderSetting(stickDeadzoneSetting, R.string.deadzone))
 | 
			
		||||
            } else {
 | 
			
		||||
                add(ModifierInputSetting(playerIndex, NativeAnalog.LStick, R.string.modifier))
 | 
			
		||||
                add(SliderSetting(modifierRangeSetting, R.string.modifier_range))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return out
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getStickDirections(player: Int, stick: NativeAnalog): List<AnalogInputSetting> =
 | 
			
		||||
        listOf(
 | 
			
		||||
            AnalogInputSetting(
 | 
			
		||||
                player,
 | 
			
		||||
                stick,
 | 
			
		||||
                AnalogDirection.Up,
 | 
			
		||||
                R.string.up
 | 
			
		||||
            ),
 | 
			
		||||
            AnalogInputSetting(
 | 
			
		||||
                player,
 | 
			
		||||
                stick,
 | 
			
		||||
                AnalogDirection.Down,
 | 
			
		||||
                R.string.down
 | 
			
		||||
            ),
 | 
			
		||||
            AnalogInputSetting(
 | 
			
		||||
                player,
 | 
			
		||||
                stick,
 | 
			
		||||
                AnalogDirection.Left,
 | 
			
		||||
                R.string.left
 | 
			
		||||
            ),
 | 
			
		||||
            AnalogInputSetting(
 | 
			
		||||
                player,
 | 
			
		||||
                stick,
 | 
			
		||||
                AnalogDirection.Right,
 | 
			
		||||
                R.string.right
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
 | 
			
		||||
        sl.apply {
 | 
			
		||||
            val theme: AbstractIntSetting = object : AbstractIntSetting {
 | 
			
		||||
@@ -186,20 +877,18 @@ class SettingsFragmentPresenter(
 | 
			
		||||
                add(
 | 
			
		||||
                    SingleChoiceSetting(
 | 
			
		||||
                        theme,
 | 
			
		||||
                        R.string.change_app_theme,
 | 
			
		||||
                        0,
 | 
			
		||||
                        R.array.themeEntriesA12,
 | 
			
		||||
                        R.array.themeValuesA12
 | 
			
		||||
                        titleId = R.string.change_app_theme,
 | 
			
		||||
                        choicesId = R.array.themeEntriesA12,
 | 
			
		||||
                        valuesId = R.array.themeValuesA12
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
            } else {
 | 
			
		||||
                add(
 | 
			
		||||
                    SingleChoiceSetting(
 | 
			
		||||
                        theme,
 | 
			
		||||
                        R.string.change_app_theme,
 | 
			
		||||
                        0,
 | 
			
		||||
                        R.array.themeEntries,
 | 
			
		||||
                        R.array.themeValues
 | 
			
		||||
                        titleId = R.string.change_app_theme,
 | 
			
		||||
                        choicesId = R.array.themeEntries,
 | 
			
		||||
                        valuesId = R.array.themeValues
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
@@ -228,10 +917,9 @@ class SettingsFragmentPresenter(
 | 
			
		||||
            add(
 | 
			
		||||
                SingleChoiceSetting(
 | 
			
		||||
                    themeMode,
 | 
			
		||||
                    R.string.change_theme_mode,
 | 
			
		||||
                    0,
 | 
			
		||||
                    R.array.themeModeEntries,
 | 
			
		||||
                    R.array.themeModeValues
 | 
			
		||||
                    titleId = R.string.change_theme_mode,
 | 
			
		||||
                    choicesId = R.array.themeModeEntries,
 | 
			
		||||
                    valuesId = R.array.themeModeValues
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
@@ -262,8 +950,8 @@ class SettingsFragmentPresenter(
 | 
			
		||||
            add(
 | 
			
		||||
                SwitchSetting(
 | 
			
		||||
                    blackBackgrounds,
 | 
			
		||||
                    R.string.use_black_backgrounds,
 | 
			
		||||
                    R.string.use_black_backgrounds_description
 | 
			
		||||
                    titleId = R.string.use_black_backgrounds,
 | 
			
		||||
                    descriptionId = R.string.use_black_backgrounds_description
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.fragments
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.ui
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
@@ -26,8 +26,6 @@ import kotlinx.coroutines.launch
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.FragmentSettingsSearchBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
 | 
			
		||||
import org.yuzu.yuzu_emu.model.SettingsViewModel
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.NativeConfig
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
 | 
			
		||||
 | 
			
		||||
@@ -119,7 +117,7 @@ class SettingsSearchFragment : Fragment() {
 | 
			
		||||
        val baseList = SettingsItem.settingsItems
 | 
			
		||||
        val similarityAlgorithm = if (searchTerm.length > 2) Cosine() else Cosine(1)
 | 
			
		||||
        val sortedList: List<SettingsItem> = baseList.mapNotNull { item ->
 | 
			
		||||
            val title = getString(item.value.nameId).lowercase()
 | 
			
		||||
            val title = item.value.title.lowercase()
 | 
			
		||||
            val similarity = similarityAlgorithm.similarity(searchTerm, title)
 | 
			
		||||
            if (similarity > 0.08) {
 | 
			
		||||
                Pair(similarity, item)
 | 
			
		||||
@@ -1,20 +1,26 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.model
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.ui
 | 
			
		||||
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
import kotlinx.coroutines.flow.MutableStateFlow
 | 
			
		||||
import kotlinx.coroutines.flow.StateFlow
 | 
			
		||||
import kotlinx.coroutines.flow.asStateFlow
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
import org.yuzu.yuzu_emu.YuzuApplication
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
 | 
			
		||||
import org.yuzu.yuzu_emu.model.Game
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.InputHandler
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.ParamPackage
 | 
			
		||||
 | 
			
		||||
class SettingsViewModel : ViewModel() {
 | 
			
		||||
    var game: Game? = null
 | 
			
		||||
 | 
			
		||||
    var clickedItem: SettingsItem? = null
 | 
			
		||||
 | 
			
		||||
    var currentDevice = 0
 | 
			
		||||
 | 
			
		||||
    val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate
 | 
			
		||||
    private val _shouldRecreate = MutableStateFlow(false)
 | 
			
		||||
 | 
			
		||||
@@ -36,6 +42,18 @@ class SettingsViewModel : ViewModel() {
 | 
			
		||||
    val adapterItemChanged: StateFlow<Int> get() = _adapterItemChanged
 | 
			
		||||
    private val _adapterItemChanged = MutableStateFlow(-1)
 | 
			
		||||
 | 
			
		||||
    private val _datasetChanged = MutableStateFlow(false)
 | 
			
		||||
    val datasetChanged = _datasetChanged.asStateFlow()
 | 
			
		||||
 | 
			
		||||
    private val _reloadListAndNotifyDataset = MutableStateFlow(false)
 | 
			
		||||
    val reloadListAndNotifyDataset = _reloadListAndNotifyDataset.asStateFlow()
 | 
			
		||||
 | 
			
		||||
    private val _shouldShowDeleteProfileDialog = MutableStateFlow("")
 | 
			
		||||
    val shouldShowDeleteProfileDialog = _shouldShowDeleteProfileDialog.asStateFlow()
 | 
			
		||||
 | 
			
		||||
    private val _shouldShowResetInputDialog = MutableStateFlow(false)
 | 
			
		||||
    val shouldShowResetInputDialog = _shouldShowResetInputDialog.asStateFlow()
 | 
			
		||||
 | 
			
		||||
    fun setShouldRecreate(value: Boolean) {
 | 
			
		||||
        _shouldRecreate.value = value
 | 
			
		||||
    }
 | 
			
		||||
@@ -68,4 +86,27 @@ class SettingsViewModel : ViewModel() {
 | 
			
		||||
    fun setAdapterItemChanged(value: Int) {
 | 
			
		||||
        _adapterItemChanged.value = value
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setDatasetChanged(value: Boolean) {
 | 
			
		||||
        _datasetChanged.value = value
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setReloadListAndNotifyDataset(value: Boolean) {
 | 
			
		||||
        _reloadListAndNotifyDataset.value = value
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setShouldShowDeleteProfileDialog(profile: String) {
 | 
			
		||||
        _shouldShowDeleteProfileDialog.value = profile
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setShouldShowResetInputDialog(value: Boolean) {
 | 
			
		||||
        _shouldShowResetInputDialog.value = value
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getCurrentDeviceParams(defaultParams: ParamPackage): ParamPackage =
 | 
			
		||||
        try {
 | 
			
		||||
            InputHandler.registeredControllers[currentDevice]
 | 
			
		||||
        } catch (e: IndexOutOfBoundsException) {
 | 
			
		||||
            defaultParams
 | 
			
		||||
        }
 | 
			
		||||
}
 | 
			
		||||
@@ -21,9 +21,9 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
 | 
			
		||||
 | 
			
		||||
    override fun bind(item: SettingsItem) {
 | 
			
		||||
        setting = item as DateTimeSetting
 | 
			
		||||
        binding.textSettingName.setText(item.nameId)
 | 
			
		||||
        if (item.descriptionId != 0) {
 | 
			
		||||
            binding.textSettingDescription.setText(item.descriptionId)
 | 
			
		||||
        binding.textSettingName.text = item.title
 | 
			
		||||
        if (setting.description.isNotEmpty()) {
 | 
			
		||||
            binding.textSettingDescription.text = item.description
 | 
			
		||||
            binding.textSettingDescription.visibility = View.VISIBLE
 | 
			
		||||
        } else {
 | 
			
		||||
            binding.textSettingDescription.visibility = View.GONE
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ class HeaderViewHolder(val binding: ListItemSettingsHeaderBinding, adapter: Sett
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun bind(item: SettingsItem) {
 | 
			
		||||
        binding.textHeaderName.setText(item.nameId)
 | 
			
		||||
        binding.textHeaderName.text = item.title
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onClick(clicked: View) {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,33 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.ui.viewholder
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
 | 
			
		||||
class InputProfileViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
 | 
			
		||||
    SettingViewHolder(binding.root, adapter) {
 | 
			
		||||
    private lateinit var setting: InputProfileSetting
 | 
			
		||||
 | 
			
		||||
    override fun bind(item: SettingsItem) {
 | 
			
		||||
        setting = item as InputProfileSetting
 | 
			
		||||
        binding.textSettingName.text = setting.title
 | 
			
		||||
        binding.textSettingValue.text =
 | 
			
		||||
            setting.getCurrentProfile().ifEmpty { binding.root.context.getString(R.string.not_set) }
 | 
			
		||||
 | 
			
		||||
        binding.textSettingDescription.visibility = View.GONE
 | 
			
		||||
        binding.buttonClear.visibility = View.GONE
 | 
			
		||||
        binding.icon.visibility = View.GONE
 | 
			
		||||
        binding.buttonClear.visibility = View.GONE
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onClick(clicked: View) =
 | 
			
		||||
        adapter.onInputProfileClick(setting, bindingAdapterPosition)
 | 
			
		||||
 | 
			
		||||
    override fun onLongClick(clicked: View): Boolean = false
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,71 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.features.settings.ui.viewholder
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingInputBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.InputSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.ModifierInputSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
 | 
			
		||||
 | 
			
		||||
class InputViewHolder(val binding: ListItemSettingInputBinding, adapter: SettingsAdapter) :
 | 
			
		||||
    SettingViewHolder(binding.root, adapter) {
 | 
			
		||||
    private lateinit var setting: InputSetting
 | 
			
		||||
 | 
			
		||||
    override fun bind(item: SettingsItem) {
 | 
			
		||||
        setting = item as InputSetting
 | 
			
		||||
        binding.textSettingName.text = setting.title
 | 
			
		||||
        binding.textSettingValue.text = setting.getSelectedValue()
 | 
			
		||||
 | 
			
		||||
        binding.buttonOptions.visibility = when (item) {
 | 
			
		||||
            is AnalogInputSetting -> {
 | 
			
		||||
                val param = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
 | 
			
		||||
                if (
 | 
			
		||||
                    param.get("engine", "") == "analog_from_button" ||
 | 
			
		||||
                    param.has("axis_x") || param.has("axis_y")
 | 
			
		||||
                ) {
 | 
			
		||||
                    View.VISIBLE
 | 
			
		||||
                } else {
 | 
			
		||||
                    View.GONE
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            is ButtonInputSetting -> {
 | 
			
		||||
                val param = NativeInput.getButtonParam(item.playerIndex, item.nativeButton)
 | 
			
		||||
                if (
 | 
			
		||||
                    param.has("code") || param.has("button") || param.has("hat") ||
 | 
			
		||||
                    param.has("axis")
 | 
			
		||||
                ) {
 | 
			
		||||
                    View.VISIBLE
 | 
			
		||||
                } else {
 | 
			
		||||
                    View.GONE
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            is ModifierInputSetting -> {
 | 
			
		||||
                val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
 | 
			
		||||
                if (params.has("modifier")) {
 | 
			
		||||
                    View.VISIBLE
 | 
			
		||||
                } else {
 | 
			
		||||
                    View.GONE
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.buttonOptions.setOnClickListener(null)
 | 
			
		||||
        binding.buttonOptions.setOnClickListener {
 | 
			
		||||
            adapter.onInputOptionsClick(binding.buttonOptions, setting, bindingAdapterPosition)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onClick(clicked: View) =
 | 
			
		||||
        adapter.onInputClick(setting, bindingAdapterPosition)
 | 
			
		||||
 | 
			
		||||
    override fun onLongClick(clicked: View): Boolean =
 | 
			
		||||
        adapter.onLongClick(setting, bindingAdapterPosition)
 | 
			
		||||
}
 | 
			
		||||
@@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.features.settings.ui.viewholder
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import androidx.core.content.res.ResourcesCompat
 | 
			
		||||
import org.yuzu.yuzu_emu.NativeLibrary
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.RunnableSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
 | 
			
		||||
@@ -17,12 +16,12 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
 | 
			
		||||
 | 
			
		||||
    override fun bind(item: SettingsItem) {
 | 
			
		||||
        setting = item as RunnableSetting
 | 
			
		||||
        if (item.iconId != 0) {
 | 
			
		||||
        if (setting.iconId != 0) {
 | 
			
		||||
            binding.icon.visibility = View.VISIBLE
 | 
			
		||||
            binding.icon.setImageDrawable(
 | 
			
		||||
                ResourcesCompat.getDrawable(
 | 
			
		||||
                    binding.icon.resources,
 | 
			
		||||
                    item.iconId,
 | 
			
		||||
                    setting.iconId,
 | 
			
		||||
                    binding.icon.context.theme
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
@@ -30,8 +29,8 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
 | 
			
		||||
            binding.icon.visibility = View.GONE
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.textSettingName.setText(item.nameId)
 | 
			
		||||
        if (item.descriptionId != 0) {
 | 
			
		||||
        binding.textSettingName.text = setting.title
 | 
			
		||||
        if (setting.description.isNotEmpty()) {
 | 
			
		||||
            binding.textSettingDescription.setText(item.descriptionId)
 | 
			
		||||
            binding.textSettingDescription.visibility = View.VISIBLE
 | 
			
		||||
        } else {
 | 
			
		||||
@@ -44,7 +43,7 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onClick(clicked: View) {
 | 
			
		||||
        if (!setting.isRuntimeRunnable && !NativeLibrary.isRunning()) {
 | 
			
		||||
        if (setting.isRunnable) {
 | 
			
		||||
            setting.runnable.invoke()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ package org.yuzu.yuzu_emu.features.settings.ui.viewholder
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.IntSingleChoiceSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
 | 
			
		||||
@@ -17,31 +18,37 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
 | 
			
		||||
 | 
			
		||||
    override fun bind(item: SettingsItem) {
 | 
			
		||||
        setting = item
 | 
			
		||||
        binding.textSettingName.setText(item.nameId)
 | 
			
		||||
        if (item.descriptionId != 0) {
 | 
			
		||||
            binding.textSettingDescription.setText(item.descriptionId)
 | 
			
		||||
        binding.textSettingName.text = setting.title
 | 
			
		||||
        if (item.description.isNotEmpty()) {
 | 
			
		||||
            binding.textSettingDescription.text = item.description
 | 
			
		||||
            binding.textSettingDescription.visibility = View.VISIBLE
 | 
			
		||||
        } else {
 | 
			
		||||
            binding.textSettingDescription.visibility = View.GONE
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.textSettingValue.visibility = View.VISIBLE
 | 
			
		||||
        if (item is SingleChoiceSetting) {
 | 
			
		||||
            val resMgr = binding.textSettingValue.context.resources
 | 
			
		||||
            val values = resMgr.getIntArray(item.valuesId)
 | 
			
		||||
            for (i in values.indices) {
 | 
			
		||||
                if (values[i] == item.getSelectedValue()) {
 | 
			
		||||
                    binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i]
 | 
			
		||||
                    break
 | 
			
		||||
        when (item) {
 | 
			
		||||
            is SingleChoiceSetting -> {
 | 
			
		||||
                val resMgr = binding.textSettingValue.context.resources
 | 
			
		||||
                val values = resMgr.getIntArray(item.valuesId)
 | 
			
		||||
                for (i in values.indices) {
 | 
			
		||||
                    if (values[i] == item.getSelectedValue()) {
 | 
			
		||||
                        binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i]
 | 
			
		||||
                        break
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else if (item is StringSingleChoiceSetting) {
 | 
			
		||||
            for (i in item.values.indices) {
 | 
			
		||||
                if (item.values[i] == item.getSelectedValue()) {
 | 
			
		||||
                    binding.textSettingValue.text = item.choices[i]
 | 
			
		||||
                    break
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            is StringSingleChoiceSetting -> {
 | 
			
		||||
                binding.textSettingValue.text = item.getSelectedValue()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            is IntSingleChoiceSetting -> {
 | 
			
		||||
                binding.textSettingValue.text = item.getChoiceAt(item.getSelectedValue())
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (binding.textSettingValue.text.isEmpty()) {
 | 
			
		||||
            binding.textSettingValue.visibility = View.GONE
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.buttonClear.visibility = if (setting.setting.global ||
 | 
			
		||||
@@ -63,16 +70,25 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (setting is SingleChoiceSetting) {
 | 
			
		||||
            adapter.onSingleChoiceClick(
 | 
			
		||||
                (setting as SingleChoiceSetting),
 | 
			
		||||
                bindingAdapterPosition
 | 
			
		||||
            )
 | 
			
		||||
        } else if (setting is StringSingleChoiceSetting) {
 | 
			
		||||
            adapter.onStringSingleChoiceClick(
 | 
			
		||||
                (setting as StringSingleChoiceSetting),
 | 
			
		||||
        when (setting) {
 | 
			
		||||
            is SingleChoiceSetting -> adapter.onSingleChoiceClick(
 | 
			
		||||
                setting as SingleChoiceSetting,
 | 
			
		||||
                bindingAdapterPosition
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            is StringSingleChoiceSetting -> {
 | 
			
		||||
                adapter.onStringSingleChoiceClick(
 | 
			
		||||
                    setting as StringSingleChoiceSetting,
 | 
			
		||||
                    bindingAdapterPosition
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            is IntSingleChoiceSetting -> {
 | 
			
		||||
                adapter.onIntSingleChoiceClick(
 | 
			
		||||
                    setting as IntSingleChoiceSetting,
 | 
			
		||||
                    bindingAdapterPosition
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,9 +17,9 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
 | 
			
		||||
 | 
			
		||||
    override fun bind(item: SettingsItem) {
 | 
			
		||||
        setting = item as SliderSetting
 | 
			
		||||
        binding.textSettingName.setText(item.nameId)
 | 
			
		||||
        if (item.descriptionId != 0) {
 | 
			
		||||
            binding.textSettingDescription.setText(item.descriptionId)
 | 
			
		||||
        binding.textSettingName.text = setting.title
 | 
			
		||||
        if (item.description.isNotEmpty()) {
 | 
			
		||||
            binding.textSettingDescription.text = setting.description
 | 
			
		||||
            binding.textSettingDescription.visibility = View.VISIBLE
 | 
			
		||||
        } else {
 | 
			
		||||
            binding.textSettingDescription.visibility = View.GONE
 | 
			
		||||
 
 | 
			
		||||
@@ -12,16 +12,16 @@ import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
 | 
			
		||||
 | 
			
		||||
class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
 | 
			
		||||
    SettingViewHolder(binding.root, adapter) {
 | 
			
		||||
    private lateinit var item: SubmenuSetting
 | 
			
		||||
    private lateinit var setting: SubmenuSetting
 | 
			
		||||
 | 
			
		||||
    override fun bind(item: SettingsItem) {
 | 
			
		||||
        this.item = item as SubmenuSetting
 | 
			
		||||
        if (item.iconId != 0) {
 | 
			
		||||
        setting = item as SubmenuSetting
 | 
			
		||||
        if (setting.iconId != 0) {
 | 
			
		||||
            binding.icon.visibility = View.VISIBLE
 | 
			
		||||
            binding.icon.setImageDrawable(
 | 
			
		||||
                ResourcesCompat.getDrawable(
 | 
			
		||||
                    binding.icon.resources,
 | 
			
		||||
                    item.iconId,
 | 
			
		||||
                    setting.iconId,
 | 
			
		||||
                    binding.icon.context.theme
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
@@ -29,9 +29,9 @@ class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAd
 | 
			
		||||
            binding.icon.visibility = View.GONE
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.textSettingName.setText(item.nameId)
 | 
			
		||||
        if (item.descriptionId != 0) {
 | 
			
		||||
            binding.textSettingDescription.setText(item.descriptionId)
 | 
			
		||||
        binding.textSettingName.text = setting.title
 | 
			
		||||
        if (setting.description.isNotEmpty()) {
 | 
			
		||||
            binding.textSettingDescription.text = setting.description
 | 
			
		||||
            binding.textSettingDescription.visibility = View.VISIBLE
 | 
			
		||||
        } else {
 | 
			
		||||
            binding.textSettingDescription.visibility = View.GONE
 | 
			
		||||
@@ -41,7 +41,7 @@ class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAd
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onClick(clicked: View) {
 | 
			
		||||
        adapter.onSubmenuClick(item)
 | 
			
		||||
        adapter.onSubmenuClick(setting)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onLongClick(clicked: View): Boolean {
 | 
			
		||||
 
 | 
			
		||||
@@ -18,19 +18,18 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
 | 
			
		||||
 | 
			
		||||
    override fun bind(item: SettingsItem) {
 | 
			
		||||
        setting = item as SwitchSetting
 | 
			
		||||
        binding.textSettingName.setText(item.nameId)
 | 
			
		||||
        if (item.descriptionId != 0) {
 | 
			
		||||
            binding.textSettingDescription.setText(item.descriptionId)
 | 
			
		||||
        binding.textSettingName.text = setting.title
 | 
			
		||||
        if (setting.description.isNotEmpty()) {
 | 
			
		||||
            binding.textSettingDescription.text = setting.description
 | 
			
		||||
            binding.textSettingDescription.visibility = View.VISIBLE
 | 
			
		||||
        } else {
 | 
			
		||||
            binding.textSettingDescription.text = ""
 | 
			
		||||
            binding.textSettingDescription.visibility = View.GONE
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.switchWidget.setOnCheckedChangeListener(null)
 | 
			
		||||
        binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal)
 | 
			
		||||
        binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
 | 
			
		||||
            adapter.onBooleanClick(item, binding.switchWidget.isChecked, bindingAdapterPosition)
 | 
			
		||||
            adapter.onBooleanClick(setting, binding.switchWidget.isChecked, bindingAdapterPosition)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.buttonClear.visibility = if (setting.setting.global ||
 | 
			
		||||
 
 | 
			
		||||
@@ -277,6 +277,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
 | 
			
		||||
                    true
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                R.id.menu_controls -> {
 | 
			
		||||
                    val action = HomeNavigationDirections.actionGlobalSettingsActivity(
 | 
			
		||||
                        null,
 | 
			
		||||
                        Settings.MenuTag.SECTION_INPUT
 | 
			
		||||
                    )
 | 
			
		||||
                    binding.root.findNavController().navigate(action)
 | 
			
		||||
                    true
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                R.id.menu_overlay_controls -> {
 | 
			
		||||
                    showOverlayOptions()
 | 
			
		||||
                    true
 | 
			
		||||
 
 | 
			
		||||
@@ -89,6 +89,20 @@ class HomeSettingsFragment : Fragment() {
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            add(
 | 
			
		||||
                HomeSetting(
 | 
			
		||||
                    R.string.preferences_controls,
 | 
			
		||||
                    R.string.preferences_controls_description,
 | 
			
		||||
                    R.drawable.ic_controller,
 | 
			
		||||
                    {
 | 
			
		||||
                        val action = HomeNavigationDirections.actionGlobalSettingsActivity(
 | 
			
		||||
                            null,
 | 
			
		||||
                            Settings.MenuTag.SECTION_INPUT
 | 
			
		||||
                        )
 | 
			
		||||
                        binding.root.findNavController().navigate(action)
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            add(
 | 
			
		||||
                HomeSetting(
 | 
			
		||||
                    R.string.gpu_driver_manager,
 | 
			
		||||
 
 | 
			
		||||
@@ -24,10 +24,10 @@ import androidx.core.content.ContextCompat
 | 
			
		||||
import androidx.window.layout.WindowMetricsCalculator
 | 
			
		||||
import kotlin.math.max
 | 
			
		||||
import kotlin.math.min
 | 
			
		||||
import org.yuzu.yuzu_emu.NativeLibrary
 | 
			
		||||
import org.yuzu.yuzu_emu.NativeLibrary.ButtonType
 | 
			
		||||
import org.yuzu.yuzu_emu.NativeLibrary.StickType
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.NativeButton
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.overlay.model.OverlayControl
 | 
			
		||||
@@ -100,19 +100,19 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
 | 
			
		||||
        var shouldUpdateView = false
 | 
			
		||||
        val playerIndex =
 | 
			
		||||
            if (NativeLibrary.isHandheldOnly()) {
 | 
			
		||||
                NativeLibrary.ConsoleDevice
 | 
			
		||||
            if (NativeInput.isHandheldOnly()) {
 | 
			
		||||
                NativeInput.ConsoleDevice
 | 
			
		||||
            } else {
 | 
			
		||||
                NativeLibrary.Player1Device
 | 
			
		||||
                NativeInput.Player1Device
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        for (button in overlayButtons) {
 | 
			
		||||
            if (!button.updateStatus(event)) {
 | 
			
		||||
                continue
 | 
			
		||||
            }
 | 
			
		||||
            NativeLibrary.onGamePadButtonEvent(
 | 
			
		||||
            NativeInput.onOverlayButtonEvent(
 | 
			
		||||
                playerIndex,
 | 
			
		||||
                button.buttonId,
 | 
			
		||||
                button.button,
 | 
			
		||||
                button.status
 | 
			
		||||
            )
 | 
			
		||||
            playHaptics(event)
 | 
			
		||||
@@ -123,24 +123,24 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
            if (!dpad.updateStatus(event, BooleanSetting.DPAD_SLIDE.getBoolean())) {
 | 
			
		||||
                continue
 | 
			
		||||
            }
 | 
			
		||||
            NativeLibrary.onGamePadButtonEvent(
 | 
			
		||||
            NativeInput.onOverlayButtonEvent(
 | 
			
		||||
                playerIndex,
 | 
			
		||||
                dpad.upId,
 | 
			
		||||
                dpad.up,
 | 
			
		||||
                dpad.upStatus
 | 
			
		||||
            )
 | 
			
		||||
            NativeLibrary.onGamePadButtonEvent(
 | 
			
		||||
            NativeInput.onOverlayButtonEvent(
 | 
			
		||||
                playerIndex,
 | 
			
		||||
                dpad.downId,
 | 
			
		||||
                dpad.down,
 | 
			
		||||
                dpad.downStatus
 | 
			
		||||
            )
 | 
			
		||||
            NativeLibrary.onGamePadButtonEvent(
 | 
			
		||||
            NativeInput.onOverlayButtonEvent(
 | 
			
		||||
                playerIndex,
 | 
			
		||||
                dpad.leftId,
 | 
			
		||||
                dpad.left,
 | 
			
		||||
                dpad.leftStatus
 | 
			
		||||
            )
 | 
			
		||||
            NativeLibrary.onGamePadButtonEvent(
 | 
			
		||||
            NativeInput.onOverlayButtonEvent(
 | 
			
		||||
                playerIndex,
 | 
			
		||||
                dpad.rightId,
 | 
			
		||||
                dpad.right,
 | 
			
		||||
                dpad.rightStatus
 | 
			
		||||
            )
 | 
			
		||||
            playHaptics(event)
 | 
			
		||||
@@ -151,16 +151,15 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
            if (!joystick.updateStatus(event)) {
 | 
			
		||||
                continue
 | 
			
		||||
            }
 | 
			
		||||
            val axisID = joystick.joystickId
 | 
			
		||||
            NativeLibrary.onGamePadJoystickEvent(
 | 
			
		||||
            NativeInput.onOverlayJoystickEvent(
 | 
			
		||||
                playerIndex,
 | 
			
		||||
                axisID,
 | 
			
		||||
                joystick.joystick,
 | 
			
		||||
                joystick.xAxis,
 | 
			
		||||
                joystick.realYAxis
 | 
			
		||||
            )
 | 
			
		||||
            NativeLibrary.onGamePadButtonEvent(
 | 
			
		||||
            NativeInput.onOverlayButtonEvent(
 | 
			
		||||
                playerIndex,
 | 
			
		||||
                joystick.buttonId,
 | 
			
		||||
                joystick.button,
 | 
			
		||||
                joystick.buttonStatus
 | 
			
		||||
            )
 | 
			
		||||
            playHaptics(event)
 | 
			
		||||
@@ -187,7 +186,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
            motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
 | 
			
		||||
 | 
			
		||||
        if (isActionDown && !isTouchInputConsumed(pointerId)) {
 | 
			
		||||
            NativeLibrary.onTouchPressed(pointerId, xPosition.toFloat(), yPosition.toFloat())
 | 
			
		||||
            NativeInput.onTouchPressed(pointerId, xPosition.toFloat(), yPosition.toFloat())
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isActionMove) {
 | 
			
		||||
@@ -196,12 +195,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                if (isTouchInputConsumed(fingerId)) {
 | 
			
		||||
                    continue
 | 
			
		||||
                }
 | 
			
		||||
                NativeLibrary.onTouchMoved(fingerId, event.getX(i), event.getY(i))
 | 
			
		||||
                NativeInput.onTouchMoved(fingerId, event.getX(i), event.getY(i))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isActionUp && !isTouchInputConsumed(pointerId)) {
 | 
			
		||||
            NativeLibrary.onTouchReleased(pointerId)
 | 
			
		||||
            NativeInput.onTouchReleased(pointerId)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true
 | 
			
		||||
@@ -359,7 +358,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                            windowSize,
 | 
			
		||||
                            R.drawable.facebutton_a,
 | 
			
		||||
                            R.drawable.facebutton_a_depressed,
 | 
			
		||||
                            ButtonType.BUTTON_A,
 | 
			
		||||
                            NativeButton.A,
 | 
			
		||||
                            data,
 | 
			
		||||
                            position
 | 
			
		||||
                        )
 | 
			
		||||
@@ -373,7 +372,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                            windowSize,
 | 
			
		||||
                            R.drawable.facebutton_b,
 | 
			
		||||
                            R.drawable.facebutton_b_depressed,
 | 
			
		||||
                            ButtonType.BUTTON_B,
 | 
			
		||||
                            NativeButton.B,
 | 
			
		||||
                            data,
 | 
			
		||||
                            position
 | 
			
		||||
                        )
 | 
			
		||||
@@ -387,7 +386,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                            windowSize,
 | 
			
		||||
                            R.drawable.facebutton_x,
 | 
			
		||||
                            R.drawable.facebutton_x_depressed,
 | 
			
		||||
                            ButtonType.BUTTON_X,
 | 
			
		||||
                            NativeButton.X,
 | 
			
		||||
                            data,
 | 
			
		||||
                            position
 | 
			
		||||
                        )
 | 
			
		||||
@@ -401,7 +400,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                            windowSize,
 | 
			
		||||
                            R.drawable.facebutton_y,
 | 
			
		||||
                            R.drawable.facebutton_y_depressed,
 | 
			
		||||
                            ButtonType.BUTTON_Y,
 | 
			
		||||
                            NativeButton.Y,
 | 
			
		||||
                            data,
 | 
			
		||||
                            position
 | 
			
		||||
                        )
 | 
			
		||||
@@ -415,7 +414,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                            windowSize,
 | 
			
		||||
                            R.drawable.facebutton_plus,
 | 
			
		||||
                            R.drawable.facebutton_plus_depressed,
 | 
			
		||||
                            ButtonType.BUTTON_PLUS,
 | 
			
		||||
                            NativeButton.Plus,
 | 
			
		||||
                            data,
 | 
			
		||||
                            position
 | 
			
		||||
                        )
 | 
			
		||||
@@ -429,7 +428,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                            windowSize,
 | 
			
		||||
                            R.drawable.facebutton_minus,
 | 
			
		||||
                            R.drawable.facebutton_minus_depressed,
 | 
			
		||||
                            ButtonType.BUTTON_MINUS,
 | 
			
		||||
                            NativeButton.Minus,
 | 
			
		||||
                            data,
 | 
			
		||||
                            position
 | 
			
		||||
                        )
 | 
			
		||||
@@ -443,7 +442,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                            windowSize,
 | 
			
		||||
                            R.drawable.facebutton_home,
 | 
			
		||||
                            R.drawable.facebutton_home_depressed,
 | 
			
		||||
                            ButtonType.BUTTON_HOME,
 | 
			
		||||
                            NativeButton.Home,
 | 
			
		||||
                            data,
 | 
			
		||||
                            position
 | 
			
		||||
                        )
 | 
			
		||||
@@ -457,7 +456,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                            windowSize,
 | 
			
		||||
                            R.drawable.facebutton_screenshot,
 | 
			
		||||
                            R.drawable.facebutton_screenshot_depressed,
 | 
			
		||||
                            ButtonType.BUTTON_CAPTURE,
 | 
			
		||||
                            NativeButton.Capture,
 | 
			
		||||
                            data,
 | 
			
		||||
                            position
 | 
			
		||||
                        )
 | 
			
		||||
@@ -471,7 +470,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                            windowSize,
 | 
			
		||||
                            R.drawable.l_shoulder,
 | 
			
		||||
                            R.drawable.l_shoulder_depressed,
 | 
			
		||||
                            ButtonType.TRIGGER_L,
 | 
			
		||||
                            NativeButton.L,
 | 
			
		||||
                            data,
 | 
			
		||||
                            position
 | 
			
		||||
                        )
 | 
			
		||||
@@ -485,7 +484,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                            windowSize,
 | 
			
		||||
                            R.drawable.r_shoulder,
 | 
			
		||||
                            R.drawable.r_shoulder_depressed,
 | 
			
		||||
                            ButtonType.TRIGGER_R,
 | 
			
		||||
                            NativeButton.R,
 | 
			
		||||
                            data,
 | 
			
		||||
                            position
 | 
			
		||||
                        )
 | 
			
		||||
@@ -499,7 +498,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                            windowSize,
 | 
			
		||||
                            R.drawable.zl_trigger,
 | 
			
		||||
                            R.drawable.zl_trigger_depressed,
 | 
			
		||||
                            ButtonType.TRIGGER_ZL,
 | 
			
		||||
                            NativeButton.ZL,
 | 
			
		||||
                            data,
 | 
			
		||||
                            position
 | 
			
		||||
                        )
 | 
			
		||||
@@ -513,7 +512,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                            windowSize,
 | 
			
		||||
                            R.drawable.zr_trigger,
 | 
			
		||||
                            R.drawable.zr_trigger_depressed,
 | 
			
		||||
                            ButtonType.TRIGGER_ZR,
 | 
			
		||||
                            NativeButton.ZR,
 | 
			
		||||
                            data,
 | 
			
		||||
                            position
 | 
			
		||||
                        )
 | 
			
		||||
@@ -527,7 +526,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                            windowSize,
 | 
			
		||||
                            R.drawable.button_l3,
 | 
			
		||||
                            R.drawable.button_l3_depressed,
 | 
			
		||||
                            ButtonType.STICK_L,
 | 
			
		||||
                            NativeButton.LStick,
 | 
			
		||||
                            data,
 | 
			
		||||
                            position
 | 
			
		||||
                        )
 | 
			
		||||
@@ -541,7 +540,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                            windowSize,
 | 
			
		||||
                            R.drawable.button_r3,
 | 
			
		||||
                            R.drawable.button_r3_depressed,
 | 
			
		||||
                            ButtonType.STICK_R,
 | 
			
		||||
                            NativeButton.RStick,
 | 
			
		||||
                            data,
 | 
			
		||||
                            position
 | 
			
		||||
                        )
 | 
			
		||||
@@ -556,8 +555,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                            R.drawable.joystick_range,
 | 
			
		||||
                            R.drawable.joystick,
 | 
			
		||||
                            R.drawable.joystick_depressed,
 | 
			
		||||
                            StickType.STICK_L,
 | 
			
		||||
                            ButtonType.STICK_L,
 | 
			
		||||
                            NativeAnalog.LStick,
 | 
			
		||||
                            NativeButton.LStick,
 | 
			
		||||
                            data,
 | 
			
		||||
                            position
 | 
			
		||||
                        )
 | 
			
		||||
@@ -572,8 +571,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                            R.drawable.joystick_range,
 | 
			
		||||
                            R.drawable.joystick,
 | 
			
		||||
                            R.drawable.joystick_depressed,
 | 
			
		||||
                            StickType.STICK_R,
 | 
			
		||||
                            ButtonType.STICK_R,
 | 
			
		||||
                            NativeAnalog.RStick,
 | 
			
		||||
                            NativeButton.RStick,
 | 
			
		||||
                            data,
 | 
			
		||||
                            position
 | 
			
		||||
                        )
 | 
			
		||||
@@ -835,7 +834,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
            windowSize: Pair<Point, Point>,
 | 
			
		||||
            defaultResId: Int,
 | 
			
		||||
            pressedResId: Int,
 | 
			
		||||
            buttonId: Int,
 | 
			
		||||
            button: NativeButton,
 | 
			
		||||
            overlayControlData: OverlayControlData,
 | 
			
		||||
            position: Pair<Double, Double>
 | 
			
		||||
        ): InputOverlayDrawableButton {
 | 
			
		||||
@@ -869,7 +868,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                res,
 | 
			
		||||
                defaultStateBitmap,
 | 
			
		||||
                pressedStateBitmap,
 | 
			
		||||
                buttonId,
 | 
			
		||||
                button,
 | 
			
		||||
                overlayControlData
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
@@ -940,11 +939,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                res,
 | 
			
		||||
                defaultStateBitmap,
 | 
			
		||||
                pressedOneDirectionStateBitmap,
 | 
			
		||||
                pressedTwoDirectionsStateBitmap,
 | 
			
		||||
                ButtonType.DPAD_UP,
 | 
			
		||||
                ButtonType.DPAD_DOWN,
 | 
			
		||||
                ButtonType.DPAD_LEFT,
 | 
			
		||||
                ButtonType.DPAD_RIGHT
 | 
			
		||||
                pressedTwoDirectionsStateBitmap
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            // 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?) :
 | 
			
		||||
            resOuter: Int,
 | 
			
		||||
            defaultResInner: Int,
 | 
			
		||||
            pressedResInner: Int,
 | 
			
		||||
            joystick: Int,
 | 
			
		||||
            buttonId: Int,
 | 
			
		||||
            joystick: NativeAnalog,
 | 
			
		||||
            button: NativeButton,
 | 
			
		||||
            overlayControlData: OverlayControlData,
 | 
			
		||||
            position: Pair<Double, Double>
 | 
			
		||||
        ): InputOverlayDrawableJoystick {
 | 
			
		||||
@@ -1042,7 +1037,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
 | 
			
		||||
                outerRect,
 | 
			
		||||
                innerRect,
 | 
			
		||||
                joystick,
 | 
			
		||||
                buttonId,
 | 
			
		||||
                button,
 | 
			
		||||
                overlayControlData.id
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,8 @@ import android.graphics.Canvas
 | 
			
		||||
import android.graphics.Rect
 | 
			
		||||
import android.graphics.drawable.BitmapDrawable
 | 
			
		||||
import android.view.MotionEvent
 | 
			
		||||
import org.yuzu.yuzu_emu.NativeLibrary.ButtonState
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.NativeButton
 | 
			
		||||
import org.yuzu.yuzu_emu.overlay.model.OverlayControlData
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -19,13 +20,13 @@ import org.yuzu.yuzu_emu.overlay.model.OverlayControlData
 | 
			
		||||
 * @param res                [Resources] instance.
 | 
			
		||||
 * @param defaultStateBitmap [Bitmap] to use with the default state Drawable.
 | 
			
		||||
 * @param pressedStateBitmap [Bitmap] to use with the pressed state Drawable.
 | 
			
		||||
 * @param buttonId           Identifier for this type of button.
 | 
			
		||||
 * @param button             [NativeButton] for this type of button.
 | 
			
		||||
 */
 | 
			
		||||
class InputOverlayDrawableButton(
 | 
			
		||||
    res: Resources,
 | 
			
		||||
    defaultStateBitmap: Bitmap,
 | 
			
		||||
    pressedStateBitmap: Bitmap,
 | 
			
		||||
    val buttonId: Int,
 | 
			
		||||
    val button: NativeButton,
 | 
			
		||||
    val overlayControlData: OverlayControlData
 | 
			
		||||
) {
 | 
			
		||||
    // The ID value what motion event is tracking
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,8 @@ import android.graphics.Canvas
 | 
			
		||||
import android.graphics.Rect
 | 
			
		||||
import android.graphics.drawable.BitmapDrawable
 | 
			
		||||
import android.view.MotionEvent
 | 
			
		||||
import org.yuzu.yuzu_emu.NativeLibrary.ButtonState
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.NativeButton
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Custom [BitmapDrawable] that is capable
 | 
			
		||||
@@ -19,20 +20,12 @@ import org.yuzu.yuzu_emu.NativeLibrary.ButtonState
 | 
			
		||||
 * @param defaultStateBitmap              [Bitmap] of the default state.
 | 
			
		||||
 * @param pressedOneDirectionStateBitmap  [Bitmap] of the pressed state in one direction.
 | 
			
		||||
 * @param pressedTwoDirectionsStateBitmap [Bitmap] of the pressed state in two direction.
 | 
			
		||||
 * @param buttonUp                        Identifier for the up button.
 | 
			
		||||
 * @param buttonDown                      Identifier for the down button.
 | 
			
		||||
 * @param buttonLeft                      Identifier for the left button.
 | 
			
		||||
 * @param buttonRight                     Identifier for the right button.
 | 
			
		||||
 */
 | 
			
		||||
class InputOverlayDrawableDpad(
 | 
			
		||||
    res: Resources,
 | 
			
		||||
    defaultStateBitmap: Bitmap,
 | 
			
		||||
    pressedOneDirectionStateBitmap: Bitmap,
 | 
			
		||||
    pressedTwoDirectionsStateBitmap: Bitmap,
 | 
			
		||||
    buttonUp: Int,
 | 
			
		||||
    buttonDown: Int,
 | 
			
		||||
    buttonLeft: Int,
 | 
			
		||||
    buttonRight: Int
 | 
			
		||||
    pressedTwoDirectionsStateBitmap: Bitmap
 | 
			
		||||
) {
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets one of the InputOverlayDrawableDpad's button IDs.
 | 
			
		||||
@@ -40,10 +33,10 @@ class InputOverlayDrawableDpad(
 | 
			
		||||
     * @return the requested InputOverlayDrawableDpad's button ID.
 | 
			
		||||
     */
 | 
			
		||||
    // The ID identifying what type of button this Drawable represents.
 | 
			
		||||
    val upId: Int
 | 
			
		||||
    val downId: Int
 | 
			
		||||
    val leftId: Int
 | 
			
		||||
    val rightId: Int
 | 
			
		||||
    val up = NativeButton.DUp
 | 
			
		||||
    val down = NativeButton.DDown
 | 
			
		||||
    val left = NativeButton.DLeft
 | 
			
		||||
    val right = NativeButton.DRight
 | 
			
		||||
    var trackId: Int
 | 
			
		||||
 | 
			
		||||
    val width: Int
 | 
			
		||||
@@ -69,10 +62,6 @@ class InputOverlayDrawableDpad(
 | 
			
		||||
        this.pressedTwoDirectionsStateBitmap = BitmapDrawable(res, pressedTwoDirectionsStateBitmap)
 | 
			
		||||
        width = this.defaultStateBitmap.intrinsicWidth
 | 
			
		||||
        height = this.defaultStateBitmap.intrinsicHeight
 | 
			
		||||
        upId = buttonUp
 | 
			
		||||
        downId = buttonDown
 | 
			
		||||
        leftId = buttonLeft
 | 
			
		||||
        rightId = buttonRight
 | 
			
		||||
        trackId = -1
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,9 @@ import kotlin.math.atan2
 | 
			
		||||
import kotlin.math.cos
 | 
			
		||||
import kotlin.math.sin
 | 
			
		||||
import kotlin.math.sqrt
 | 
			
		||||
import org.yuzu.yuzu_emu.NativeLibrary
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.NativeButton
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -26,8 +28,8 @@ import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
 | 
			
		||||
 * @param bitmapInnerPressed [Bitmap] which represents the pressed inner movable part of the joystick.
 | 
			
		||||
 * @param rectOuter          [Rect] which represents the outer joystick bounds.
 | 
			
		||||
 * @param rectInner          [Rect] which represents the inner joystick bounds.
 | 
			
		||||
 * @param joystickId         The ID value what type of joystick this Drawable represents.
 | 
			
		||||
 * @param buttonId           The ID value what type of button this Drawable represents.
 | 
			
		||||
 * @param joystick           The [NativeAnalog] this Drawable represents.
 | 
			
		||||
 * @param button             The [NativeButton] this Drawable represents.
 | 
			
		||||
 */
 | 
			
		||||
class InputOverlayDrawableJoystick(
 | 
			
		||||
    res: Resources,
 | 
			
		||||
@@ -36,8 +38,8 @@ class InputOverlayDrawableJoystick(
 | 
			
		||||
    bitmapInnerPressed: Bitmap,
 | 
			
		||||
    rectOuter: Rect,
 | 
			
		||||
    rectInner: Rect,
 | 
			
		||||
    val joystickId: Int,
 | 
			
		||||
    val buttonId: Int,
 | 
			
		||||
    val joystick: NativeAnalog,
 | 
			
		||||
    val button: NativeButton,
 | 
			
		||||
    val prefId: String
 | 
			
		||||
) {
 | 
			
		||||
    // The ID value what motion event is tracking
 | 
			
		||||
@@ -69,8 +71,7 @@ class InputOverlayDrawableJoystick(
 | 
			
		||||
 | 
			
		||||
    // TODO: Add button support
 | 
			
		||||
    val buttonStatus: Int
 | 
			
		||||
        get() =
 | 
			
		||||
            NativeLibrary.ButtonState.RELEASED
 | 
			
		||||
        get() = ButtonState.RELEASED
 | 
			
		||||
    var bounds: Rect
 | 
			
		||||
        get() = outerBitmap.bounds
 | 
			
		||||
        set(bounds) {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,439 +6,89 @@ package org.yuzu.yuzu_emu.utils
 | 
			
		||||
import android.view.InputDevice
 | 
			
		||||
import android.view.KeyEvent
 | 
			
		||||
import android.view.MotionEvent
 | 
			
		||||
import kotlin.math.sqrt
 | 
			
		||||
import org.yuzu.yuzu_emu.NativeLibrary
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.YuzuInputOverlayDevice
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.YuzuPhysicalDevice
 | 
			
		||||
 | 
			
		||||
object InputHandler {
 | 
			
		||||
    private var controllerIds = getGameControllerIds()
 | 
			
		||||
 | 
			
		||||
    fun initialize() {
 | 
			
		||||
        // Connect first controller
 | 
			
		||||
        NativeLibrary.onGamePadConnectEvent(getPlayerNumber(NativeLibrary.Player1Device))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updateControllerIds() {
 | 
			
		||||
        controllerIds = getGameControllerIds()
 | 
			
		||||
    }
 | 
			
		||||
    var androidControllers = mapOf<Int, YuzuPhysicalDevice>()
 | 
			
		||||
    var registeredControllers = mutableListOf<ParamPackage>()
 | 
			
		||||
 | 
			
		||||
    fun dispatchKeyEvent(event: KeyEvent): Boolean {
 | 
			
		||||
        val button: Int = when (event.device.vendorId) {
 | 
			
		||||
            0x045E -> getInputXboxButtonKey(event.keyCode)
 | 
			
		||||
            0x054C -> getInputDS5ButtonKey(event.keyCode)
 | 
			
		||||
            0x057E -> getInputJoyconButtonKey(event.keyCode)
 | 
			
		||||
            0x1532 -> getInputRazerButtonKey(event.keyCode)
 | 
			
		||||
            0x3537 -> getInputRedmagicButtonKey(event.keyCode)
 | 
			
		||||
            0x358A -> getInputBackboneLabsButtonKey(event.keyCode)
 | 
			
		||||
            else -> getInputGenericButtonKey(event.keyCode)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val action = when (event.action) {
 | 
			
		||||
            KeyEvent.ACTION_DOWN -> NativeLibrary.ButtonState.PRESSED
 | 
			
		||||
            KeyEvent.ACTION_UP -> NativeLibrary.ButtonState.RELEASED
 | 
			
		||||
            KeyEvent.ACTION_DOWN -> NativeInput.ButtonState.PRESSED
 | 
			
		||||
            KeyEvent.ACTION_UP -> NativeInput.ButtonState.RELEASED
 | 
			
		||||
            else -> return false
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Ignore invalid buttons
 | 
			
		||||
        if (button < 0) {
 | 
			
		||||
            return false
 | 
			
		||||
        var controllerData = androidControllers[event.device.controllerNumber]
 | 
			
		||||
        if (controllerData == null) {
 | 
			
		||||
            updateControllerData()
 | 
			
		||||
            controllerData = androidControllers[event.device.controllerNumber] ?: return false
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return NativeLibrary.onGamePadButtonEvent(
 | 
			
		||||
            getPlayerNumber(event.device.controllerNumber, event.deviceId),
 | 
			
		||||
            button,
 | 
			
		||||
        NativeInput.onGamePadButtonEvent(
 | 
			
		||||
            controllerData.getGUID(),
 | 
			
		||||
            controllerData.getPort(),
 | 
			
		||||
            event.keyCode,
 | 
			
		||||
            action
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
 | 
			
		||||
        val device = event.device
 | 
			
		||||
        // Check every axis input available on the controller
 | 
			
		||||
        for (range in device.motionRanges) {
 | 
			
		||||
            val axis = range.axis
 | 
			
		||||
            when (device.vendorId) {
 | 
			
		||||
                0x045E -> setGenericAxisInput(event, axis)
 | 
			
		||||
                0x054C -> setGenericAxisInput(event, axis)
 | 
			
		||||
                0x057E -> setJoyconAxisInput(event, axis)
 | 
			
		||||
                0x1532 -> setRazerAxisInput(event, axis)
 | 
			
		||||
                else -> setGenericAxisInput(event, axis)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getPlayerNumber(index: Int, deviceId: Int = -1): Int {
 | 
			
		||||
        var deviceIndex = index
 | 
			
		||||
        if (deviceId != -1) {
 | 
			
		||||
            deviceIndex = controllerIds[deviceId] ?: 0
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // TODO: Joycons are handled as different controllers. Find a way to merge them.
 | 
			
		||||
        return when (deviceIndex) {
 | 
			
		||||
            2 -> NativeLibrary.Player2Device
 | 
			
		||||
            3 -> NativeLibrary.Player3Device
 | 
			
		||||
            4 -> NativeLibrary.Player4Device
 | 
			
		||||
            5 -> NativeLibrary.Player5Device
 | 
			
		||||
            6 -> NativeLibrary.Player6Device
 | 
			
		||||
            7 -> NativeLibrary.Player7Device
 | 
			
		||||
            8 -> NativeLibrary.Player8Device
 | 
			
		||||
            else -> if (NativeLibrary.isHandheldOnly()) {
 | 
			
		||||
                NativeLibrary.ConsoleDevice
 | 
			
		||||
            } else {
 | 
			
		||||
                NativeLibrary.Player1Device
 | 
			
		||||
            }
 | 
			
		||||
    fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
 | 
			
		||||
        val controllerData =
 | 
			
		||||
            androidControllers[event.device.controllerNumber] ?: return false
 | 
			
		||||
        event.device.motionRanges.forEach {
 | 
			
		||||
            NativeInput.onGamePadAxisEvent(
 | 
			
		||||
                controllerData.getGUID(),
 | 
			
		||||
                controllerData.getPort(),
 | 
			
		||||
                it.axis,
 | 
			
		||||
                event.getAxisValue(it.axis)
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setStickState(playerNumber: Int, index: Int, xAxis: Float, yAxis: Float) {
 | 
			
		||||
        // Calculate vector size
 | 
			
		||||
        val r2 = xAxis * xAxis + yAxis * yAxis
 | 
			
		||||
        var r = sqrt(r2.toDouble()).toFloat()
 | 
			
		||||
 | 
			
		||||
        // Adjust range of joystick
 | 
			
		||||
        val deadzone = 0.15f
 | 
			
		||||
        var x = xAxis
 | 
			
		||||
        var y = yAxis
 | 
			
		||||
 | 
			
		||||
        if (r > deadzone) {
 | 
			
		||||
            val deadzoneFactor = 1.0f / r * (r - deadzone) / (1.0f - deadzone)
 | 
			
		||||
            x *= deadzoneFactor
 | 
			
		||||
            y *= deadzoneFactor
 | 
			
		||||
            r *= deadzoneFactor
 | 
			
		||||
        } else {
 | 
			
		||||
            x = 0.0f
 | 
			
		||||
            y = 0.0f
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Normalize joystick
 | 
			
		||||
        if (r > 1.0f) {
 | 
			
		||||
            x /= r
 | 
			
		||||
            y /= r
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        NativeLibrary.onGamePadJoystickEvent(
 | 
			
		||||
            playerNumber,
 | 
			
		||||
            index,
 | 
			
		||||
            x,
 | 
			
		||||
            -y
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getAxisToButton(axis: Float): Int {
 | 
			
		||||
        return if (axis > 0.5f) {
 | 
			
		||||
            NativeLibrary.ButtonState.PRESSED
 | 
			
		||||
        } else {
 | 
			
		||||
            NativeLibrary.ButtonState.RELEASED
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setAxisDpadState(playerNumber: Int, xAxis: Float, yAxis: Float) {
 | 
			
		||||
        NativeLibrary.onGamePadButtonEvent(
 | 
			
		||||
            playerNumber,
 | 
			
		||||
            NativeLibrary.ButtonType.DPAD_UP,
 | 
			
		||||
            getAxisToButton(-yAxis)
 | 
			
		||||
        )
 | 
			
		||||
        NativeLibrary.onGamePadButtonEvent(
 | 
			
		||||
            playerNumber,
 | 
			
		||||
            NativeLibrary.ButtonType.DPAD_DOWN,
 | 
			
		||||
            getAxisToButton(yAxis)
 | 
			
		||||
        )
 | 
			
		||||
        NativeLibrary.onGamePadButtonEvent(
 | 
			
		||||
            playerNumber,
 | 
			
		||||
            NativeLibrary.ButtonType.DPAD_LEFT,
 | 
			
		||||
            getAxisToButton(-xAxis)
 | 
			
		||||
        )
 | 
			
		||||
        NativeLibrary.onGamePadButtonEvent(
 | 
			
		||||
            playerNumber,
 | 
			
		||||
            NativeLibrary.ButtonType.DPAD_RIGHT,
 | 
			
		||||
            getAxisToButton(xAxis)
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getInputDS5ButtonKey(key: Int): Int {
 | 
			
		||||
        // The missing ds5 buttons are axis
 | 
			
		||||
        return when (key) {
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
 | 
			
		||||
            else -> -1
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getInputJoyconButtonKey(key: Int): Int {
 | 
			
		||||
        // Joycon support is half dead. A lot of buttons can't be mapped
 | 
			
		||||
        return when (key) {
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
 | 
			
		||||
            KeyEvent.KEYCODE_DPAD_UP -> NativeLibrary.ButtonType.DPAD_UP
 | 
			
		||||
            KeyEvent.KEYCODE_DPAD_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN
 | 
			
		||||
            KeyEvent.KEYCODE_DPAD_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT
 | 
			
		||||
            KeyEvent.KEYCODE_DPAD_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
 | 
			
		||||
            else -> -1
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getInputXboxButtonKey(key: Int): Int {
 | 
			
		||||
        // The missing xbox buttons are axis
 | 
			
		||||
        return when (key) {
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
 | 
			
		||||
            else -> -1
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getInputRazerButtonKey(key: Int): Int {
 | 
			
		||||
        // The missing xbox buttons are axis
 | 
			
		||||
        return when (key) {
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
 | 
			
		||||
            else -> -1
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getInputRedmagicButtonKey(key: Int): Int {
 | 
			
		||||
        return when (key) {
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
 | 
			
		||||
            else -> -1
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getInputBackboneLabsButtonKey(key: Int): Int {
 | 
			
		||||
        return when (key) {
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
 | 
			
		||||
            else -> -1
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getInputGenericButtonKey(key: Int): Int {
 | 
			
		||||
        return when (key) {
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
 | 
			
		||||
            KeyEvent.KEYCODE_DPAD_UP -> NativeLibrary.ButtonType.DPAD_UP
 | 
			
		||||
            KeyEvent.KEYCODE_DPAD_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN
 | 
			
		||||
            KeyEvent.KEYCODE_DPAD_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT
 | 
			
		||||
            KeyEvent.KEYCODE_DPAD_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
 | 
			
		||||
            KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
 | 
			
		||||
            else -> -1
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setGenericAxisInput(event: MotionEvent, axis: Int) {
 | 
			
		||||
        val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId)
 | 
			
		||||
 | 
			
		||||
        when (axis) {
 | 
			
		||||
            MotionEvent.AXIS_X, MotionEvent.AXIS_Y ->
 | 
			
		||||
                setStickState(
 | 
			
		||||
                    playerNumber,
 | 
			
		||||
                    NativeLibrary.StickType.STICK_L,
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_X),
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_Y)
 | 
			
		||||
                )
 | 
			
		||||
            MotionEvent.AXIS_RX, MotionEvent.AXIS_RY ->
 | 
			
		||||
                setStickState(
 | 
			
		||||
                    playerNumber,
 | 
			
		||||
                    NativeLibrary.StickType.STICK_R,
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_RX),
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_RY)
 | 
			
		||||
                )
 | 
			
		||||
            MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ ->
 | 
			
		||||
                setStickState(
 | 
			
		||||
                    playerNumber,
 | 
			
		||||
                    NativeLibrary.StickType.STICK_R,
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_Z),
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_RZ)
 | 
			
		||||
                )
 | 
			
		||||
            MotionEvent.AXIS_LTRIGGER ->
 | 
			
		||||
                NativeLibrary.onGamePadButtonEvent(
 | 
			
		||||
                    playerNumber,
 | 
			
		||||
                    NativeLibrary.ButtonType.TRIGGER_ZL,
 | 
			
		||||
                    getAxisToButton(event.getAxisValue(MotionEvent.AXIS_LTRIGGER))
 | 
			
		||||
                )
 | 
			
		||||
            MotionEvent.AXIS_BRAKE ->
 | 
			
		||||
                NativeLibrary.onGamePadButtonEvent(
 | 
			
		||||
                    playerNumber,
 | 
			
		||||
                    NativeLibrary.ButtonType.TRIGGER_ZL,
 | 
			
		||||
                    getAxisToButton(event.getAxisValue(MotionEvent.AXIS_BRAKE))
 | 
			
		||||
                )
 | 
			
		||||
            MotionEvent.AXIS_RTRIGGER ->
 | 
			
		||||
                NativeLibrary.onGamePadButtonEvent(
 | 
			
		||||
                    playerNumber,
 | 
			
		||||
                    NativeLibrary.ButtonType.TRIGGER_ZR,
 | 
			
		||||
                    getAxisToButton(event.getAxisValue(MotionEvent.AXIS_RTRIGGER))
 | 
			
		||||
                )
 | 
			
		||||
            MotionEvent.AXIS_GAS ->
 | 
			
		||||
                NativeLibrary.onGamePadButtonEvent(
 | 
			
		||||
                    playerNumber,
 | 
			
		||||
                    NativeLibrary.ButtonType.TRIGGER_ZR,
 | 
			
		||||
                    getAxisToButton(event.getAxisValue(MotionEvent.AXIS_GAS))
 | 
			
		||||
                )
 | 
			
		||||
            MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y ->
 | 
			
		||||
                setAxisDpadState(
 | 
			
		||||
                    playerNumber,
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_HAT_X),
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_HAT_Y)
 | 
			
		||||
                )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setJoyconAxisInput(event: MotionEvent, axis: Int) {
 | 
			
		||||
        // Joycon support is half dead. Right joystick doesn't work
 | 
			
		||||
        val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId)
 | 
			
		||||
 | 
			
		||||
        when (axis) {
 | 
			
		||||
            MotionEvent.AXIS_X, MotionEvent.AXIS_Y ->
 | 
			
		||||
                setStickState(
 | 
			
		||||
                    playerNumber,
 | 
			
		||||
                    NativeLibrary.StickType.STICK_L,
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_X),
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_Y)
 | 
			
		||||
                )
 | 
			
		||||
            MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ ->
 | 
			
		||||
                setStickState(
 | 
			
		||||
                    playerNumber,
 | 
			
		||||
                    NativeLibrary.StickType.STICK_R,
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_Z),
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_RZ)
 | 
			
		||||
                )
 | 
			
		||||
            MotionEvent.AXIS_RX, MotionEvent.AXIS_RY ->
 | 
			
		||||
                setStickState(
 | 
			
		||||
                    playerNumber,
 | 
			
		||||
                    NativeLibrary.StickType.STICK_R,
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_RX),
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_RY)
 | 
			
		||||
                )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setRazerAxisInput(event: MotionEvent, axis: Int) {
 | 
			
		||||
        val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId)
 | 
			
		||||
 | 
			
		||||
        when (axis) {
 | 
			
		||||
            MotionEvent.AXIS_X, MotionEvent.AXIS_Y ->
 | 
			
		||||
                setStickState(
 | 
			
		||||
                    playerNumber,
 | 
			
		||||
                    NativeLibrary.StickType.STICK_L,
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_X),
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_Y)
 | 
			
		||||
                )
 | 
			
		||||
            MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ ->
 | 
			
		||||
                setStickState(
 | 
			
		||||
                    playerNumber,
 | 
			
		||||
                    NativeLibrary.StickType.STICK_R,
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_Z),
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_RZ)
 | 
			
		||||
                )
 | 
			
		||||
            MotionEvent.AXIS_BRAKE ->
 | 
			
		||||
                NativeLibrary.onGamePadButtonEvent(
 | 
			
		||||
                    playerNumber,
 | 
			
		||||
                    NativeLibrary.ButtonType.TRIGGER_ZL,
 | 
			
		||||
                    getAxisToButton(event.getAxisValue(MotionEvent.AXIS_BRAKE))
 | 
			
		||||
                )
 | 
			
		||||
            MotionEvent.AXIS_GAS ->
 | 
			
		||||
                NativeLibrary.onGamePadButtonEvent(
 | 
			
		||||
                    playerNumber,
 | 
			
		||||
                    NativeLibrary.ButtonType.TRIGGER_ZR,
 | 
			
		||||
                    getAxisToButton(event.getAxisValue(MotionEvent.AXIS_GAS))
 | 
			
		||||
                )
 | 
			
		||||
            MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y ->
 | 
			
		||||
                setAxisDpadState(
 | 
			
		||||
                    playerNumber,
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_HAT_X),
 | 
			
		||||
                    event.getAxisValue(MotionEvent.AXIS_HAT_Y)
 | 
			
		||||
                )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getGameControllerIds(): Map<Int, Int> {
 | 
			
		||||
        val gameControllerDeviceIds = mutableMapOf<Int, Int>()
 | 
			
		||||
    fun getDevices(): Map<Int, YuzuPhysicalDevice> {
 | 
			
		||||
        val gameControllerDeviceIds = mutableMapOf<Int, YuzuPhysicalDevice>()
 | 
			
		||||
        val deviceIds = InputDevice.getDeviceIds()
 | 
			
		||||
        var controllerSlot = 1
 | 
			
		||||
        var port = 0
 | 
			
		||||
        val inputSettings = NativeConfig.getInputSettings(true)
 | 
			
		||||
        deviceIds.forEach { deviceId ->
 | 
			
		||||
            InputDevice.getDevice(deviceId)?.apply {
 | 
			
		||||
                // Don't over-assign controllers
 | 
			
		||||
                if (controllerSlot >= 8) {
 | 
			
		||||
                    return gameControllerDeviceIds
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Verify that the device has gamepad buttons, control sticks, or both.
 | 
			
		||||
                if (sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD ||
 | 
			
		||||
                    sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK
 | 
			
		||||
                ) {
 | 
			
		||||
                    // This device is a game controller. Store its device ID.
 | 
			
		||||
                    if (deviceId and id and vendorId and productId != 0) {
 | 
			
		||||
                        // Additionally filter out devices that have no ID
 | 
			
		||||
                        gameControllerDeviceIds
 | 
			
		||||
                            .takeIf { !it.contains(deviceId) }
 | 
			
		||||
                            ?.put(deviceId, controllerSlot)
 | 
			
		||||
                        controllerSlot++
 | 
			
		||||
                    if (!gameControllerDeviceIds.contains(controllerNumber)) {
 | 
			
		||||
                        gameControllerDeviceIds[controllerNumber] = YuzuPhysicalDevice(
 | 
			
		||||
                            this,
 | 
			
		||||
                            port,
 | 
			
		||||
                            inputSettings[port].useSystemVibrator
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                    port++
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return gameControllerDeviceIds
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updateControllerData() {
 | 
			
		||||
        androidControllers = getDevices()
 | 
			
		||||
        androidControllers.forEach {
 | 
			
		||||
            NativeInput.registerController(it.value)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Register the input overlay on a dedicated port for all player 1 vibrations
 | 
			
		||||
        NativeInput.registerController(YuzuInputOverlayDevice(androidControllers.isEmpty(), 100))
 | 
			
		||||
        registeredControllers.clear()
 | 
			
		||||
        NativeInput.getInputDevices().forEach {
 | 
			
		||||
            registeredControllers.add(ParamPackage(it))
 | 
			
		||||
        }
 | 
			
		||||
        registeredControllers.sortBy { it.get("port", 0) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun InputDevice.getGUID(): String = String.format("%016x%016x", productId, vendorId)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,8 @@ package org.yuzu.yuzu_emu.utils
 | 
			
		||||
import org.yuzu.yuzu_emu.model.GameDir
 | 
			
		||||
import org.yuzu.yuzu_emu.overlay.model.OverlayControlData
 | 
			
		||||
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.model.PlayerInput
 | 
			
		||||
 | 
			
		||||
object NativeConfig {
 | 
			
		||||
    /**
 | 
			
		||||
     * Loads global config.
 | 
			
		||||
@@ -168,4 +170,17 @@ object NativeConfig {
 | 
			
		||||
     */
 | 
			
		||||
    @Synchronized
 | 
			
		||||
    external fun setOverlayControlData(overlayControlData: Array<OverlayControlData>)
 | 
			
		||||
 | 
			
		||||
    @Synchronized
 | 
			
		||||
    external fun getInputSettings(global: Boolean): Array<PlayerInput>
 | 
			
		||||
 | 
			
		||||
    @Synchronized
 | 
			
		||||
    external fun setInputSettings(value: Array<PlayerInput>, global: Boolean)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Saves control values for a specific player
 | 
			
		||||
     * Must be used when per game config is loaded
 | 
			
		||||
     */
 | 
			
		||||
    @Synchronized
 | 
			
		||||
    external fun saveControlPlayerValues()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ import android.os.Build
 | 
			
		||||
import android.os.Handler
 | 
			
		||||
import android.os.Looper
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
import org.yuzu.yuzu_emu.NativeLibrary
 | 
			
		||||
import org.yuzu.yuzu_emu.features.input.NativeInput
 | 
			
		||||
 | 
			
		||||
class NfcReader(private val activity: Activity) {
 | 
			
		||||
    private var nfcAdapter: NfcAdapter? = null
 | 
			
		||||
@@ -76,12 +76,12 @@ class NfcReader(private val activity: Activity) {
 | 
			
		||||
        amiibo.connect()
 | 
			
		||||
 | 
			
		||||
        val tagData = ntag215ReadAll(amiibo) ?: return
 | 
			
		||||
        NativeLibrary.onReadNfcTag(tagData)
 | 
			
		||||
        NativeInput.onReadNfcTag(tagData)
 | 
			
		||||
 | 
			
		||||
        nfcAdapter?.ignore(
 | 
			
		||||
            tag,
 | 
			
		||||
            1000,
 | 
			
		||||
            { NativeLibrary.onRemoveNfcTag() },
 | 
			
		||||
            { NativeInput.onRemoveNfcTag() },
 | 
			
		||||
            Handler(Looper.getMainLooper())
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,141 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.utils
 | 
			
		||||
 | 
			
		||||
// Kotlin version of src/common/param_package.h
 | 
			
		||||
class ParamPackage(serialized: String = "") {
 | 
			
		||||
    private val KEY_VALUE_SEPARATOR = ":"
 | 
			
		||||
    private val PARAM_SEPARATOR = ","
 | 
			
		||||
 | 
			
		||||
    private val ESCAPE_CHARACTER = "$"
 | 
			
		||||
    private val KEY_VALUE_SEPARATOR_ESCAPE = "$0"
 | 
			
		||||
    private val PARAM_SEPARATOR_ESCAPE = "$1"
 | 
			
		||||
    private val ESCAPE_CHARACTER_ESCAPE = "$2"
 | 
			
		||||
 | 
			
		||||
    private val EMPTY_PLACEHOLDER = "[empty]"
 | 
			
		||||
 | 
			
		||||
    val data = mutableMapOf<String, String>()
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        val pairs = serialized.split(PARAM_SEPARATOR)
 | 
			
		||||
        for (pair in pairs) {
 | 
			
		||||
            val keyValue = pair.split(KEY_VALUE_SEPARATOR).toMutableList()
 | 
			
		||||
            if (keyValue.size != 2) {
 | 
			
		||||
                Log.error("[ParamPackage] Invalid key pair $keyValue")
 | 
			
		||||
                continue
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            keyValue.forEachIndexed { i: Int, _: String ->
 | 
			
		||||
                keyValue[i] = keyValue[i].replace(KEY_VALUE_SEPARATOR_ESCAPE, KEY_VALUE_SEPARATOR)
 | 
			
		||||
                keyValue[i] = keyValue[i].replace(PARAM_SEPARATOR_ESCAPE, PARAM_SEPARATOR)
 | 
			
		||||
                keyValue[i] = keyValue[i].replace(ESCAPE_CHARACTER_ESCAPE, ESCAPE_CHARACTER)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            set(keyValue[0], keyValue[1])
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constructor(params: List<Pair<String, String>>) : this() {
 | 
			
		||||
        params.forEach {
 | 
			
		||||
            data[it.first] = it.second
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun serialize(): String {
 | 
			
		||||
        if (data.isEmpty()) {
 | 
			
		||||
            return EMPTY_PLACEHOLDER
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val result = StringBuilder()
 | 
			
		||||
        data.forEach {
 | 
			
		||||
            val keyValue = mutableListOf(it.key, it.value)
 | 
			
		||||
            keyValue.forEachIndexed { i, _ ->
 | 
			
		||||
                keyValue[i] = keyValue[i].replace(ESCAPE_CHARACTER, ESCAPE_CHARACTER_ESCAPE)
 | 
			
		||||
                keyValue[i] = keyValue[i].replace(PARAM_SEPARATOR, PARAM_SEPARATOR_ESCAPE)
 | 
			
		||||
                keyValue[i] = keyValue[i].replace(KEY_VALUE_SEPARATOR, KEY_VALUE_SEPARATOR_ESCAPE)
 | 
			
		||||
            }
 | 
			
		||||
            result.append("${keyValue[0]}$KEY_VALUE_SEPARATOR${keyValue[1]}$PARAM_SEPARATOR")
 | 
			
		||||
        }
 | 
			
		||||
        return result.removeSuffix(PARAM_SEPARATOR).toString()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun get(key: String, defaultValue: String): String =
 | 
			
		||||
        if (has(key)) {
 | 
			
		||||
            data[key]!!
 | 
			
		||||
        } else {
 | 
			
		||||
            Log.debug("[ParamPackage] key $key not found")
 | 
			
		||||
            defaultValue
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    fun get(key: String, defaultValue: Int): Int =
 | 
			
		||||
        if (has(key)) {
 | 
			
		||||
            try {
 | 
			
		||||
                data[key]!!.toInt()
 | 
			
		||||
            } catch (e: NumberFormatException) {
 | 
			
		||||
                Log.debug("[ParamPackage] failed to convert ${data[key]!!} to int")
 | 
			
		||||
                defaultValue
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            Log.debug("[ParamPackage] key $key not found")
 | 
			
		||||
            defaultValue
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    private fun Int.toBoolean(): Boolean =
 | 
			
		||||
        if (this == 1) {
 | 
			
		||||
            true
 | 
			
		||||
        } else if (this == 0) {
 | 
			
		||||
            false
 | 
			
		||||
        } else {
 | 
			
		||||
            throw Exception("Tried to convert a value to a boolean that was not 0 or 1!")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    fun get(key: String, defaultValue: Boolean): Boolean =
 | 
			
		||||
        if (has(key)) {
 | 
			
		||||
            try {
 | 
			
		||||
                get(key, if (defaultValue) 1 else 0).toBoolean()
 | 
			
		||||
            } catch (e: Exception) {
 | 
			
		||||
                Log.debug("[ParamPackage] failed to convert ${data[key]!!} to boolean")
 | 
			
		||||
                defaultValue
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            Log.debug("[ParamPackage] key $key not found")
 | 
			
		||||
            defaultValue
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    fun get(key: String, defaultValue: Float): Float =
 | 
			
		||||
        if (has(key)) {
 | 
			
		||||
            try {
 | 
			
		||||
                data[key]!!.toFloat()
 | 
			
		||||
            } catch (e: NumberFormatException) {
 | 
			
		||||
                Log.debug("[ParamPackage] failed to convert ${data[key]!!} to float")
 | 
			
		||||
                defaultValue
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            Log.debug("[ParamPackage] key $key not found")
 | 
			
		||||
            defaultValue
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    fun set(key: String, value: String) {
 | 
			
		||||
        data[key] = value
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun set(key: String, value: Int) {
 | 
			
		||||
        data[key] = value.toString()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun Boolean.toInt(): Int = if (this) 1 else 0
 | 
			
		||||
    fun set(key: String, value: Boolean) {
 | 
			
		||||
        data[key] = value.toInt().toString()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun set(key: String, value: Float) {
 | 
			
		||||
        data[key] = value.toString()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun has(key: String): Boolean = data.containsKey(key)
 | 
			
		||||
 | 
			
		||||
    fun erase(key: String) = data.remove(key)
 | 
			
		||||
 | 
			
		||||
    fun clear() = data.clear()
 | 
			
		||||
}
 | 
			
		||||
@@ -12,6 +12,7 @@ add_library(yuzu-android SHARED
 | 
			
		||||
    native_log.cpp
 | 
			
		||||
    android_config.cpp
 | 
			
		||||
    android_config.h
 | 
			
		||||
    native_input.cpp
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR})
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
#include <common/logging/log.h>
 | 
			
		||||
#include <input_common/main.h>
 | 
			
		||||
#include "android_config.h"
 | 
			
		||||
#include "android_settings.h"
 | 
			
		||||
#include "common/settings_setting.h"
 | 
			
		||||
@@ -32,6 +34,7 @@ void AndroidConfig::ReadAndroidValues() {
 | 
			
		||||
        ReadOverlayValues();
 | 
			
		||||
    }
 | 
			
		||||
    ReadDriverValues();
 | 
			
		||||
    ReadAndroidControlValues();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AndroidConfig::ReadAndroidUIValues() {
 | 
			
		||||
@@ -107,6 +110,76 @@ void AndroidConfig::ReadOverlayValues() {
 | 
			
		||||
    EndGroup();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AndroidConfig::ReadAndroidPlayerValues(std::size_t player_index) {
 | 
			
		||||
    std::string player_prefix;
 | 
			
		||||
    if (type != ConfigType::InputProfile) {
 | 
			
		||||
        player_prefix.append("player_").append(ToString(player_index)).append("_");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    auto& player = Settings::values.players.GetValue()[player_index];
 | 
			
		||||
    if (IsCustomConfig()) {
 | 
			
		||||
        const auto profile_name =
 | 
			
		||||
            ReadStringSetting(std::string(player_prefix).append("profile_name"));
 | 
			
		||||
        if (profile_name.empty()) {
 | 
			
		||||
            // Use the global input config
 | 
			
		||||
            player = Settings::values.players.GetValue(true)[player_index];
 | 
			
		||||
            player.profile_name = "";
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Android doesn't have default options for controllers. We have the input overlay for that.
 | 
			
		||||
    for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
 | 
			
		||||
        const std::string default_param;
 | 
			
		||||
        auto& player_buttons = player.buttons[i];
 | 
			
		||||
 | 
			
		||||
        player_buttons = ReadStringSetting(
 | 
			
		||||
            std::string(player_prefix).append(Settings::NativeButton::mapping[i]), default_param);
 | 
			
		||||
        if (player_buttons.empty()) {
 | 
			
		||||
            player_buttons = default_param;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
 | 
			
		||||
        const std::string default_param;
 | 
			
		||||
        auto& player_analogs = player.analogs[i];
 | 
			
		||||
 | 
			
		||||
        player_analogs = ReadStringSetting(
 | 
			
		||||
            std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]), default_param);
 | 
			
		||||
        if (player_analogs.empty()) {
 | 
			
		||||
            player_analogs = default_param;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
 | 
			
		||||
        const std::string default_param;
 | 
			
		||||
        auto& player_motions = player.motions[i];
 | 
			
		||||
 | 
			
		||||
        player_motions = ReadStringSetting(
 | 
			
		||||
            std::string(player_prefix).append(Settings::NativeMotion::mapping[i]), default_param);
 | 
			
		||||
        if (player_motions.empty()) {
 | 
			
		||||
            player_motions = default_param;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    player.use_system_vibrator = ReadBooleanSetting(
 | 
			
		||||
        std::string(player_prefix).append("use_system_vibrator"), player_index == 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AndroidConfig::ReadAndroidControlValues() {
 | 
			
		||||
    BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
 | 
			
		||||
 | 
			
		||||
    Settings::values.players.SetGlobal(!IsCustomConfig());
 | 
			
		||||
    for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) {
 | 
			
		||||
        ReadAndroidPlayerValues(p);
 | 
			
		||||
    }
 | 
			
		||||
    if (IsCustomConfig()) {
 | 
			
		||||
        EndGroup();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    // ReadDebugControlValues();
 | 
			
		||||
    // ReadHidbusValues();
 | 
			
		||||
 | 
			
		||||
    EndGroup();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AndroidConfig::SaveAndroidValues() {
 | 
			
		||||
    if (global) {
 | 
			
		||||
        SaveAndroidUIValues();
 | 
			
		||||
@@ -114,6 +187,7 @@ void AndroidConfig::SaveAndroidValues() {
 | 
			
		||||
        SaveOverlayValues();
 | 
			
		||||
    }
 | 
			
		||||
    SaveDriverValues();
 | 
			
		||||
    SaveAndroidControlValues();
 | 
			
		||||
 | 
			
		||||
    WriteToIni();
 | 
			
		||||
}
 | 
			
		||||
@@ -187,6 +261,52 @@ void AndroidConfig::SaveOverlayValues() {
 | 
			
		||||
    EndGroup();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AndroidConfig::SaveAndroidPlayerValues(std::size_t player_index) {
 | 
			
		||||
    std::string player_prefix;
 | 
			
		||||
    if (type != ConfigType::InputProfile) {
 | 
			
		||||
        player_prefix = std::string("player_").append(ToString(player_index)).append("_");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const auto& player = Settings::values.players.GetValue()[player_index];
 | 
			
		||||
    if (IsCustomConfig() && player.profile_name.empty()) {
 | 
			
		||||
        // No custom profile selected
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const std::string default_param;
 | 
			
		||||
    for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
 | 
			
		||||
        WriteStringSetting(std::string(player_prefix).append(Settings::NativeButton::mapping[i]),
 | 
			
		||||
                           player.buttons[i], std::make_optional(default_param));
 | 
			
		||||
    }
 | 
			
		||||
    for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
 | 
			
		||||
        WriteStringSetting(std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]),
 | 
			
		||||
                           player.analogs[i], std::make_optional(default_param));
 | 
			
		||||
    }
 | 
			
		||||
    for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
 | 
			
		||||
        WriteStringSetting(std::string(player_prefix).append(Settings::NativeMotion::mapping[i]),
 | 
			
		||||
                           player.motions[i], std::make_optional(default_param));
 | 
			
		||||
    }
 | 
			
		||||
    WriteBooleanSetting(std::string(player_prefix).append("use_system_vibrator"),
 | 
			
		||||
                        player.use_system_vibrator, std::make_optional(player_index == 0));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AndroidConfig::SaveAndroidControlValues() {
 | 
			
		||||
    BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
 | 
			
		||||
 | 
			
		||||
    Settings::values.players.SetGlobal(!IsCustomConfig());
 | 
			
		||||
    for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) {
 | 
			
		||||
        SaveAndroidPlayerValues(p);
 | 
			
		||||
    }
 | 
			
		||||
    if (IsCustomConfig()) {
 | 
			
		||||
        EndGroup();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    // SaveDebugControlValues();
 | 
			
		||||
    // SaveHidbusValues();
 | 
			
		||||
 | 
			
		||||
    EndGroup();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) {
 | 
			
		||||
    auto& map = Settings::values.linkage.by_category;
 | 
			
		||||
    if (map.contains(category)) {
 | 
			
		||||
@@ -194,3 +314,24 @@ std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::
 | 
			
		||||
    }
 | 
			
		||||
    return AndroidSettings::values.linkage.by_category[category];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AndroidConfig::ReadAndroidControlPlayerValues(std::size_t player_index) {
 | 
			
		||||
    BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
 | 
			
		||||
 | 
			
		||||
    ReadPlayerValues(player_index);
 | 
			
		||||
    ReadAndroidPlayerValues(player_index);
 | 
			
		||||
 | 
			
		||||
    EndGroup();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AndroidConfig::SaveAndroidControlPlayerValues(std::size_t player_index) {
 | 
			
		||||
    BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
 | 
			
		||||
 | 
			
		||||
    LOG_DEBUG(Config, "Saving players control configuration values");
 | 
			
		||||
    SavePlayerValues(player_index);
 | 
			
		||||
    SaveAndroidPlayerValues(player_index);
 | 
			
		||||
 | 
			
		||||
    EndGroup();
 | 
			
		||||
 | 
			
		||||
    WriteToIni();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,12 @@ public:
 | 
			
		||||
    void ReloadAllValues() override;
 | 
			
		||||
    void SaveAllValues() override;
 | 
			
		||||
 | 
			
		||||
    void ReadAndroidControlPlayerValues(std::size_t player_index);
 | 
			
		||||
    void SaveAndroidControlPlayerValues(std::size_t player_index);
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
    void ReadAndroidPlayerValues(std::size_t player_index);
 | 
			
		||||
    void ReadAndroidControlValues();
 | 
			
		||||
    void ReadAndroidValues();
 | 
			
		||||
    void ReadAndroidUIValues();
 | 
			
		||||
    void ReadDriverValues();
 | 
			
		||||
@@ -27,6 +32,8 @@ protected:
 | 
			
		||||
    void ReadUILayoutValues() override {}
 | 
			
		||||
    void ReadMultiplayerValues() override {}
 | 
			
		||||
 | 
			
		||||
    void SaveAndroidPlayerValues(std::size_t player_index);
 | 
			
		||||
    void SaveAndroidControlValues();
 | 
			
		||||
    void SaveAndroidValues();
 | 
			
		||||
    void SaveAndroidUIValues();
 | 
			
		||||
    void SaveDriverValues();
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
#include "common/android/id_cache.h"
 | 
			
		||||
#include "common/logging/log.h"
 | 
			
		||||
#include "input_common/drivers/android.h"
 | 
			
		||||
#include "input_common/drivers/touch_screen.h"
 | 
			
		||||
#include "input_common/drivers/virtual_amiibo.h"
 | 
			
		||||
#include "input_common/drivers/virtual_gamepad.h"
 | 
			
		||||
@@ -22,43 +23,6 @@ void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) {
 | 
			
		||||
    window_info.render_surface = reinterpret_cast<void*>(surface);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EmuWindow_Android::OnTouchPressed(int id, float x, float y) {
 | 
			
		||||
    const auto [touch_x, touch_y] = MapToTouchScreen(x, y);
 | 
			
		||||
    m_input_subsystem->GetTouchScreen()->TouchPressed(touch_x, touch_y, id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EmuWindow_Android::OnTouchMoved(int id, float x, float y) {
 | 
			
		||||
    const auto [touch_x, touch_y] = MapToTouchScreen(x, y);
 | 
			
		||||
    m_input_subsystem->GetTouchScreen()->TouchMoved(touch_x, touch_y, id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EmuWindow_Android::OnTouchReleased(int id) {
 | 
			
		||||
    m_input_subsystem->GetTouchScreen()->TouchReleased(id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EmuWindow_Android::OnGamepadButtonEvent(int player_index, int button_id, bool pressed) {
 | 
			
		||||
    m_input_subsystem->GetVirtualGamepad()->SetButtonState(player_index, button_id, pressed);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EmuWindow_Android::OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y) {
 | 
			
		||||
    m_input_subsystem->GetVirtualGamepad()->SetStickPosition(player_index, stick_id, x, y);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EmuWindow_Android::OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x,
 | 
			
		||||
                                             float gyro_y, float gyro_z, float accel_x,
 | 
			
		||||
                                             float accel_y, float accel_z) {
 | 
			
		||||
    m_input_subsystem->GetVirtualGamepad()->SetMotionState(
 | 
			
		||||
        player_index, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EmuWindow_Android::OnReadNfcTag(std::span<u8> data) {
 | 
			
		||||
    m_input_subsystem->GetVirtualAmiibo()->LoadAmiibo(data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EmuWindow_Android::OnRemoveNfcTag() {
 | 
			
		||||
    m_input_subsystem->GetVirtualAmiibo()->CloseAmiibo();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EmuWindow_Android::OnFrameDisplayed() {
 | 
			
		||||
    if (!m_first_frame) {
 | 
			
		||||
        Common::Android::RunJNIOnFiber<void>(
 | 
			
		||||
@@ -67,10 +31,9 @@ void EmuWindow_Android::OnFrameDisplayed() {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem,
 | 
			
		||||
                                     ANativeWindow* surface,
 | 
			
		||||
EmuWindow_Android::EmuWindow_Android(ANativeWindow* surface,
 | 
			
		||||
                                     std::shared_ptr<Common::DynamicLibrary> driver_library)
 | 
			
		||||
    : m_input_subsystem{input_subsystem}, m_driver_library{driver_library} {
 | 
			
		||||
    : m_driver_library{driver_library} {
 | 
			
		||||
    LOG_INFO(Frontend, "initializing");
 | 
			
		||||
 | 
			
		||||
    if (!surface) {
 | 
			
		||||
@@ -80,10 +43,4 @@ EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsyste
 | 
			
		||||
 | 
			
		||||
    OnSurfaceChanged(surface);
 | 
			
		||||
    window_info.type = Core::Frontend::WindowSystemType::Android;
 | 
			
		||||
 | 
			
		||||
    m_input_subsystem->Initialize();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
EmuWindow_Android::~EmuWindow_Android() {
 | 
			
		||||
    m_input_subsystem->Shutdown();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -30,21 +30,12 @@ private:
 | 
			
		||||
class EmuWindow_Android final : public Core::Frontend::EmuWindow {
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem, ANativeWindow* surface,
 | 
			
		||||
    EmuWindow_Android(ANativeWindow* surface,
 | 
			
		||||
                      std::shared_ptr<Common::DynamicLibrary> driver_library);
 | 
			
		||||
 | 
			
		||||
    ~EmuWindow_Android();
 | 
			
		||||
    ~EmuWindow_Android() = default;
 | 
			
		||||
 | 
			
		||||
    void OnSurfaceChanged(ANativeWindow* surface);
 | 
			
		||||
    void OnTouchPressed(int id, float x, float y);
 | 
			
		||||
    void OnTouchMoved(int id, float x, float y);
 | 
			
		||||
    void OnTouchReleased(int id);
 | 
			
		||||
    void OnGamepadButtonEvent(int player_index, int button_id, bool pressed);
 | 
			
		||||
    void OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y);
 | 
			
		||||
    void OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x, float gyro_y,
 | 
			
		||||
                              float gyro_z, float accel_x, float accel_y, float accel_z);
 | 
			
		||||
    void OnReadNfcTag(std::span<u8> data);
 | 
			
		||||
    void OnRemoveNfcTag();
 | 
			
		||||
    void OnFrameDisplayed() override;
 | 
			
		||||
 | 
			
		||||
    std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override {
 | 
			
		||||
@@ -55,8 +46,6 @@ public:
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    InputCommon::InputSubsystem* m_input_subsystem{};
 | 
			
		||||
 | 
			
		||||
    float m_window_width{};
 | 
			
		||||
    float m_window_height{};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -88,6 +88,10 @@ FileSys::ManualContentProvider* EmulationSession::GetContentProvider() {
 | 
			
		||||
    return m_manual_provider.get();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
InputCommon::InputSubsystem& EmulationSession::GetInputSubsystem() {
 | 
			
		||||
    return m_input_subsystem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const EmuWindow_Android& EmulationSession::Window() const {
 | 
			
		||||
    return *m_window;
 | 
			
		||||
}
 | 
			
		||||
@@ -198,6 +202,8 @@ void EmulationSession::InitializeSystem(bool reload) {
 | 
			
		||||
        Common::Log::Initialize();
 | 
			
		||||
        Common::Log::SetColorConsoleBackendEnabled(true);
 | 
			
		||||
        Common::Log::Start();
 | 
			
		||||
 | 
			
		||||
        m_input_subsystem.Initialize();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Initialize filesystem.
 | 
			
		||||
@@ -222,8 +228,7 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string
 | 
			
		||||
    std::scoped_lock lock(m_mutex);
 | 
			
		||||
 | 
			
		||||
    // Create the render window.
 | 
			
		||||
    m_window =
 | 
			
		||||
        std::make_unique<EmuWindow_Android>(&m_input_subsystem, m_native_window, m_vulkan_library);
 | 
			
		||||
    m_window = std::make_unique<EmuWindow_Android>(m_native_window, m_vulkan_library);
 | 
			
		||||
 | 
			
		||||
    // Initialize system.
 | 
			
		||||
    jauto android_keyboard = std::make_unique<Common::Android::SoftwareKeyboard::AndroidKeyboard>();
 | 
			
		||||
@@ -355,60 +360,6 @@ void EmulationSession::RunEmulation() {
 | 
			
		||||
    m_applet_id = static_cast<int>(Service::AM::AppletId::Application);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool EmulationSession::IsHandheldOnly() {
 | 
			
		||||
    jconst npad_style_set = m_system.HIDCore().GetSupportedStyleTag();
 | 
			
		||||
 | 
			
		||||
    if (npad_style_set.fullkey == 1) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (npad_style_set.handheld == 0) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return !Settings::IsDockedMode();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EmulationSession::SetDeviceType([[maybe_unused]] int index, int type) {
 | 
			
		||||
    jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
 | 
			
		||||
    controller->SetNpadStyleIndex(static_cast<Core::HID::NpadStyleIndex>(type));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EmulationSession::OnGamepadConnectEvent([[maybe_unused]] int index) {
 | 
			
		||||
    jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
 | 
			
		||||
 | 
			
		||||
    // Ensure that player1 is configured correctly and handheld disconnected
 | 
			
		||||
    if (controller->GetNpadIdType() == Core::HID::NpadIdType::Player1) {
 | 
			
		||||
        jauto handheld = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld);
 | 
			
		||||
 | 
			
		||||
        if (controller->GetNpadStyleIndex() == Core::HID::NpadStyleIndex::Handheld) {
 | 
			
		||||
            handheld->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey);
 | 
			
		||||
            controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey);
 | 
			
		||||
            handheld->Disconnect();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Ensure that handheld is configured correctly and player 1 disconnected
 | 
			
		||||
    if (controller->GetNpadIdType() == Core::HID::NpadIdType::Handheld) {
 | 
			
		||||
        jauto player1 = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1);
 | 
			
		||||
 | 
			
		||||
        if (controller->GetNpadStyleIndex() != Core::HID::NpadStyleIndex::Handheld) {
 | 
			
		||||
            player1->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
 | 
			
		||||
            controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
 | 
			
		||||
            player1->Disconnect();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!controller->IsConnected()) {
 | 
			
		||||
        controller->Connect();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EmulationSession::OnGamepadDisconnectEvent([[maybe_unused]] int index) {
 | 
			
		||||
    jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
 | 
			
		||||
    controller->Disconnect();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Common::Android::SoftwareKeyboard::AndroidKeyboard* EmulationSession::SoftwareKeyboard() {
 | 
			
		||||
    return m_software_keyboard;
 | 
			
		||||
}
 | 
			
		||||
@@ -574,14 +525,14 @@ jobjectArray Java_org_yuzu_yuzu_1emu_utils_GpuDriverHelper_getSystemDriverInfo(
 | 
			
		||||
                                             nullptr, nullptr, file_redirect_dir_, nullptr);
 | 
			
		||||
    auto driver_library = std::make_shared<Common::DynamicLibrary>(handle);
 | 
			
		||||
    InputCommon::InputSubsystem input_subsystem;
 | 
			
		||||
    auto m_window = std::make_unique<EmuWindow_Android>(
 | 
			
		||||
        &input_subsystem, ANativeWindow_fromSurface(env, j_surf), driver_library);
 | 
			
		||||
    auto window =
 | 
			
		||||
        std::make_unique<EmuWindow_Android>(ANativeWindow_fromSurface(env, j_surf), driver_library);
 | 
			
		||||
 | 
			
		||||
    Vulkan::vk::InstanceDispatch dld;
 | 
			
		||||
    Vulkan::vk::Instance vk_instance = Vulkan::CreateInstance(
 | 
			
		||||
        *driver_library, dld, VK_API_VERSION_1_1, Core::Frontend::WindowSystemType::Android);
 | 
			
		||||
 | 
			
		||||
    auto surface = Vulkan::CreateSurface(vk_instance, m_window->GetWindowInfo());
 | 
			
		||||
    auto surface = Vulkan::CreateSurface(vk_instance, window->GetWindowInfo());
 | 
			
		||||
 | 
			
		||||
    auto device = Vulkan::CreateDevice(vk_instance, dld, *surface);
 | 
			
		||||
 | 
			
		||||
@@ -622,103 +573,6 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused(JNIEnv* env, jclass claz
 | 
			
		||||
    return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly(JNIEnv* env, jclass clazz) {
 | 
			
		||||
    return EmulationSession::GetInstance().IsHandheldOnly();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_setDeviceType(JNIEnv* env, jclass clazz,
 | 
			
		||||
                                                             jint j_device, jint j_type) {
 | 
			
		||||
    if (EmulationSession::GetInstance().IsRunning()) {
 | 
			
		||||
        EmulationSession::GetInstance().SetDeviceType(j_device, j_type);
 | 
			
		||||
    }
 | 
			
		||||
    return static_cast<jboolean>(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadConnectEvent(JNIEnv* env, jclass clazz,
 | 
			
		||||
                                                                     jint j_device) {
 | 
			
		||||
    if (EmulationSession::GetInstance().IsRunning()) {
 | 
			
		||||
        EmulationSession::GetInstance().OnGamepadConnectEvent(j_device);
 | 
			
		||||
    }
 | 
			
		||||
    return static_cast<jboolean>(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadDisconnectEvent(JNIEnv* env, jclass clazz,
 | 
			
		||||
                                                                        jint j_device) {
 | 
			
		||||
    if (EmulationSession::GetInstance().IsRunning()) {
 | 
			
		||||
        EmulationSession::GetInstance().OnGamepadDisconnectEvent(j_device);
 | 
			
		||||
    }
 | 
			
		||||
    return static_cast<jboolean>(true);
 | 
			
		||||
}
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadButtonEvent(JNIEnv* env, jclass clazz,
 | 
			
		||||
                                                                    jint j_device, jint j_button,
 | 
			
		||||
                                                                    jint action) {
 | 
			
		||||
    if (EmulationSession::GetInstance().IsRunning()) {
 | 
			
		||||
        // Ensure gamepad is connected
 | 
			
		||||
        EmulationSession::GetInstance().OnGamepadConnectEvent(j_device);
 | 
			
		||||
        EmulationSession::GetInstance().Window().OnGamepadButtonEvent(j_device, j_button,
 | 
			
		||||
                                                                      action != 0);
 | 
			
		||||
    }
 | 
			
		||||
    return static_cast<jboolean>(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadJoystickEvent(JNIEnv* env, jclass clazz,
 | 
			
		||||
                                                                      jint j_device, jint stick_id,
 | 
			
		||||
                                                                      jfloat x, jfloat y) {
 | 
			
		||||
    if (EmulationSession::GetInstance().IsRunning()) {
 | 
			
		||||
        EmulationSession::GetInstance().Window().OnGamepadJoystickEvent(j_device, stick_id, x, y);
 | 
			
		||||
    }
 | 
			
		||||
    return static_cast<jboolean>(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadMotionEvent(
 | 
			
		||||
    JNIEnv* env, jclass clazz, jint j_device, jlong delta_timestamp, jfloat gyro_x, jfloat gyro_y,
 | 
			
		||||
    jfloat gyro_z, jfloat accel_x, jfloat accel_y, jfloat accel_z) {
 | 
			
		||||
    if (EmulationSession::GetInstance().IsRunning()) {
 | 
			
		||||
        EmulationSession::GetInstance().Window().OnGamepadMotionEvent(
 | 
			
		||||
            j_device, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z);
 | 
			
		||||
    }
 | 
			
		||||
    return static_cast<jboolean>(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onReadNfcTag(JNIEnv* env, jclass clazz,
 | 
			
		||||
                                                            jbyteArray j_data) {
 | 
			
		||||
    jboolean isCopy{false};
 | 
			
		||||
    std::span<u8> data(reinterpret_cast<u8*>(env->GetByteArrayElements(j_data, &isCopy)),
 | 
			
		||||
                       static_cast<size_t>(env->GetArrayLength(j_data)));
 | 
			
		||||
 | 
			
		||||
    if (EmulationSession::GetInstance().IsRunning()) {
 | 
			
		||||
        EmulationSession::GetInstance().Window().OnReadNfcTag(data);
 | 
			
		||||
    }
 | 
			
		||||
    return static_cast<jboolean>(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onRemoveNfcTag(JNIEnv* env, jclass clazz) {
 | 
			
		||||
    if (EmulationSession::GetInstance().IsRunning()) {
 | 
			
		||||
        EmulationSession::GetInstance().Window().OnRemoveNfcTag();
 | 
			
		||||
    }
 | 
			
		||||
    return static_cast<jboolean>(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchPressed(JNIEnv* env, jclass clazz, jint id,
 | 
			
		||||
                                                          jfloat x, jfloat y) {
 | 
			
		||||
    if (EmulationSession::GetInstance().IsRunning()) {
 | 
			
		||||
        EmulationSession::GetInstance().Window().OnTouchPressed(id, x, y);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, jclass clazz, jint id,
 | 
			
		||||
                                                        jfloat x, jfloat y) {
 | 
			
		||||
    if (EmulationSession::GetInstance().IsRunning()) {
 | 
			
		||||
        EmulationSession::GetInstance().Window().OnTouchMoved(id, x, y);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchReleased(JNIEnv* env, jclass clazz, jint id) {
 | 
			
		||||
    if (EmulationSession::GetInstance().IsRunning()) {
 | 
			
		||||
        EmulationSession::GetInstance().Window().OnTouchReleased(id);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass clazz,
 | 
			
		||||
                                                            jboolean reload) {
 | 
			
		||||
    // Initialize the emulated system.
 | 
			
		||||
@@ -759,6 +613,7 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGpuDriver(JNIEnv* env, jobject
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) {
 | 
			
		||||
    EmulationSession::GetInstance().System().ApplySettings();
 | 
			
		||||
    EmulationSession::GetInstance().System().HIDCore().ReloadInputDevices();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_logSettings(JNIEnv* env, jobject jobj) {
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ public:
 | 
			
		||||
    const Core::System& System() const;
 | 
			
		||||
    Core::System& System();
 | 
			
		||||
    FileSys::ManualContentProvider* GetContentProvider();
 | 
			
		||||
    InputCommon::InputSubsystem& GetInputSubsystem();
 | 
			
		||||
 | 
			
		||||
    const EmuWindow_Android& Window() const;
 | 
			
		||||
    EmuWindow_Android& Window();
 | 
			
		||||
@@ -50,10 +51,6 @@ public:
 | 
			
		||||
                                                 const std::size_t program_index,
 | 
			
		||||
                                                 const bool frontend_initiated);
 | 
			
		||||
 | 
			
		||||
    bool IsHandheldOnly();
 | 
			
		||||
    void SetDeviceType([[maybe_unused]] int index, int type);
 | 
			
		||||
    void OnGamepadConnectEvent([[maybe_unused]] int index);
 | 
			
		||||
    void OnGamepadDisconnectEvent([[maybe_unused]] int index);
 | 
			
		||||
    Common::Android::SoftwareKeyboard::AndroidKeyboard* SoftwareKeyboard();
 | 
			
		||||
 | 
			
		||||
    static void OnEmulationStarted();
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,6 @@
 | 
			
		||||
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
#include <common/fs/fs_util.h>
 | 
			
		||||
#include <jni.h>
 | 
			
		||||
 | 
			
		||||
#include "android_config.h"
 | 
			
		||||
@@ -425,4 +424,120 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setOverlayControlData(
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInputSettings(JNIEnv* env, jobject obj,
 | 
			
		||||
                                                                         jboolean j_global) {
 | 
			
		||||
    Settings::values.players.SetGlobal(static_cast<bool>(j_global));
 | 
			
		||||
    auto& players = Settings::values.players.GetValue();
 | 
			
		||||
    jobjectArray j_input_settings =
 | 
			
		||||
        env->NewObjectArray(players.size(), Common::Android::GetPlayerInputClass(), nullptr);
 | 
			
		||||
    for (size_t i = 0; i < players.size(); ++i) {
 | 
			
		||||
        auto j_connected = static_cast<jboolean>(players[i].connected);
 | 
			
		||||
 | 
			
		||||
        jobjectArray j_buttons = env->NewObjectArray(
 | 
			
		||||
            players[i].buttons.size(), Common::Android::GetStringClass(), env->NewStringUTF(""));
 | 
			
		||||
        for (size_t j = 0; j < players[i].buttons.size(); ++j) {
 | 
			
		||||
            env->SetObjectArrayElement(j_buttons, j,
 | 
			
		||||
                                       Common::Android::ToJString(env, players[i].buttons[j]));
 | 
			
		||||
        }
 | 
			
		||||
        jobjectArray j_analogs = env->NewObjectArray(
 | 
			
		||||
            players[i].analogs.size(), Common::Android::GetStringClass(), env->NewStringUTF(""));
 | 
			
		||||
        for (size_t j = 0; j < players[i].analogs.size(); ++j) {
 | 
			
		||||
            env->SetObjectArrayElement(j_analogs, j,
 | 
			
		||||
                                       Common::Android::ToJString(env, players[i].analogs[j]));
 | 
			
		||||
        }
 | 
			
		||||
        jobjectArray j_motions = env->NewObjectArray(
 | 
			
		||||
            players[i].motions.size(), Common::Android::GetStringClass(), env->NewStringUTF(""));
 | 
			
		||||
        for (size_t j = 0; j < players[i].motions.size(); ++j) {
 | 
			
		||||
            env->SetObjectArrayElement(j_motions, j,
 | 
			
		||||
                                       Common::Android::ToJString(env, players[i].motions[j]));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        auto j_vibration_enabled = static_cast<jboolean>(players[i].vibration_enabled);
 | 
			
		||||
        auto j_vibration_strength = static_cast<jint>(players[i].vibration_strength);
 | 
			
		||||
 | 
			
		||||
        auto j_body_color_left = static_cast<jlong>(players[i].body_color_left);
 | 
			
		||||
        auto j_body_color_right = static_cast<jlong>(players[i].body_color_right);
 | 
			
		||||
        auto j_button_color_left = static_cast<jlong>(players[i].button_color_left);
 | 
			
		||||
        auto j_button_color_right = static_cast<jlong>(players[i].button_color_right);
 | 
			
		||||
 | 
			
		||||
        auto j_profile_name = Common::Android::ToJString(env, players[i].profile_name);
 | 
			
		||||
 | 
			
		||||
        auto j_use_system_vibrator = players[i].use_system_vibrator;
 | 
			
		||||
 | 
			
		||||
        jobject playerInput = env->NewObject(
 | 
			
		||||
            Common::Android::GetPlayerInputClass(), Common::Android::GetPlayerInputConstructor(),
 | 
			
		||||
            j_connected, j_buttons, j_analogs, j_motions, j_vibration_enabled, j_vibration_strength,
 | 
			
		||||
            j_body_color_left, j_body_color_right, j_button_color_left, j_button_color_right,
 | 
			
		||||
            j_profile_name, j_use_system_vibrator);
 | 
			
		||||
        env->SetObjectArrayElement(j_input_settings, i, playerInput);
 | 
			
		||||
    }
 | 
			
		||||
    return j_input_settings;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInputSettings(JNIEnv* env, jobject obj,
 | 
			
		||||
                                                                 jobjectArray j_value,
 | 
			
		||||
                                                                 jboolean j_global) {
 | 
			
		||||
    auto& players = Settings::values.players.GetValue(static_cast<bool>(j_global));
 | 
			
		||||
    int playersSize = env->GetArrayLength(j_value);
 | 
			
		||||
    for (int i = 0; i < playersSize; ++i) {
 | 
			
		||||
        jobject jplayer = env->GetObjectArrayElement(j_value, i);
 | 
			
		||||
 | 
			
		||||
        players[i].connected = static_cast<bool>(
 | 
			
		||||
            env->GetBooleanField(jplayer, Common::Android::GetPlayerInputConnectedField()));
 | 
			
		||||
 | 
			
		||||
        auto j_buttons_array = static_cast<jobjectArray>(
 | 
			
		||||
            env->GetObjectField(jplayer, Common::Android::GetPlayerInputButtonsField()));
 | 
			
		||||
        int buttons_size = env->GetArrayLength(j_buttons_array);
 | 
			
		||||
        for (int j = 0; j < buttons_size; ++j) {
 | 
			
		||||
            auto button = static_cast<jstring>(env->GetObjectArrayElement(j_buttons_array, j));
 | 
			
		||||
            players[i].buttons[j] = Common::Android::GetJString(env, button);
 | 
			
		||||
        }
 | 
			
		||||
        auto j_analogs_array = static_cast<jobjectArray>(
 | 
			
		||||
            env->GetObjectField(jplayer, Common::Android::GetPlayerInputAnalogsField()));
 | 
			
		||||
        int analogs_size = env->GetArrayLength(j_analogs_array);
 | 
			
		||||
        for (int j = 0; j < analogs_size; ++j) {
 | 
			
		||||
            auto analog = static_cast<jstring>(env->GetObjectArrayElement(j_analogs_array, j));
 | 
			
		||||
            players[i].analogs[j] = Common::Android::GetJString(env, analog);
 | 
			
		||||
        }
 | 
			
		||||
        auto j_motions_array = static_cast<jobjectArray>(
 | 
			
		||||
            env->GetObjectField(jplayer, Common::Android::GetPlayerInputMotionsField()));
 | 
			
		||||
        int motions_size = env->GetArrayLength(j_motions_array);
 | 
			
		||||
        for (int j = 0; j < motions_size; ++j) {
 | 
			
		||||
            auto motion = static_cast<jstring>(env->GetObjectArrayElement(j_motions_array, j));
 | 
			
		||||
            players[i].motions[j] = Common::Android::GetJString(env, motion);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        players[i].vibration_enabled = static_cast<bool>(
 | 
			
		||||
            env->GetBooleanField(jplayer, Common::Android::GetPlayerInputVibrationEnabledField()));
 | 
			
		||||
        players[i].vibration_strength = static_cast<int>(
 | 
			
		||||
            env->GetIntField(jplayer, Common::Android::GetPlayerInputVibrationStrengthField()));
 | 
			
		||||
 | 
			
		||||
        players[i].body_color_left = static_cast<u32>(
 | 
			
		||||
            env->GetLongField(jplayer, Common::Android::GetPlayerInputBodyColorLeftField()));
 | 
			
		||||
        players[i].body_color_right = static_cast<u32>(
 | 
			
		||||
            env->GetLongField(jplayer, Common::Android::GetPlayerInputBodyColorRightField()));
 | 
			
		||||
        players[i].button_color_left = static_cast<u32>(
 | 
			
		||||
            env->GetLongField(jplayer, Common::Android::GetPlayerInputButtonColorLeftField()));
 | 
			
		||||
        players[i].button_color_right = static_cast<u32>(
 | 
			
		||||
            env->GetLongField(jplayer, Common::Android::GetPlayerInputButtonColorRightField()));
 | 
			
		||||
 | 
			
		||||
        auto profileName = static_cast<jstring>(
 | 
			
		||||
            env->GetObjectField(jplayer, Common::Android::GetPlayerInputProfileNameField()));
 | 
			
		||||
        players[i].profile_name = Common::Android::GetJString(env, profileName);
 | 
			
		||||
 | 
			
		||||
        players[i].use_system_vibrator =
 | 
			
		||||
            env->GetBooleanField(jplayer, Common::Android::GetPlayerInputUseSystemVibratorField());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveControlPlayerValues(JNIEnv* env, jobject obj) {
 | 
			
		||||
    Settings::values.players.SetGlobal(false);
 | 
			
		||||
 | 
			
		||||
    // Clear all controls from the config in case the user reverted back to globals
 | 
			
		||||
    per_game_config->ClearControlPlayerValues();
 | 
			
		||||
    for (size_t index = 0; index < Settings::values.players.GetValue().size(); ++index) {
 | 
			
		||||
        per_game_config->SaveAndroidControlPlayerValues(index);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // extern "C"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										631
									
								
								src/android/app/src/main/jni/native_input.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										631
									
								
								src/android/app/src/main/jni/native_input.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,631 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
#include <common/fs/fs.h>
 | 
			
		||||
#include <common/fs/path_util.h>
 | 
			
		||||
#include <common/settings.h>
 | 
			
		||||
#include <hid_core/hid_types.h>
 | 
			
		||||
#include <jni.h>
 | 
			
		||||
 | 
			
		||||
#include "android_config.h"
 | 
			
		||||
#include "common/android/android_common.h"
 | 
			
		||||
#include "common/android/id_cache.h"
 | 
			
		||||
#include "hid_core/frontend/emulated_controller.h"
 | 
			
		||||
#include "hid_core/hid_core.h"
 | 
			
		||||
#include "input_common/drivers/android.h"
 | 
			
		||||
#include "input_common/drivers/touch_screen.h"
 | 
			
		||||
#include "input_common/drivers/virtual_amiibo.h"
 | 
			
		||||
#include "input_common/drivers/virtual_gamepad.h"
 | 
			
		||||
#include "native.h"
 | 
			
		||||
 | 
			
		||||
std::unordered_map<std::string, std::unique_ptr<AndroidConfig>> map_profiles;
 | 
			
		||||
 | 
			
		||||
bool IsHandheldOnly() {
 | 
			
		||||
    const auto npad_style_set =
 | 
			
		||||
        EmulationSession::GetInstance().System().HIDCore().GetSupportedStyleTag();
 | 
			
		||||
 | 
			
		||||
    if (npad_style_set.fullkey == 1) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (npad_style_set.handheld == 0) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return !Settings::IsDockedMode();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::filesystem::path GetNameWithoutExtension(std::filesystem::path filename) {
 | 
			
		||||
    return filename.replace_extension();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool IsProfileNameValid(std::string_view profile_name) {
 | 
			
		||||
    return profile_name.find_first_of("<>:;\"/\\|,.!?*") == std::string::npos;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ProfileExistsInFilesystem(std::string_view profile_name) {
 | 
			
		||||
    return Common::FS::Exists(Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "input" /
 | 
			
		||||
                              fmt::format("{}.ini", profile_name));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ProfileExistsInMap(const std::string& profile_name) {
 | 
			
		||||
    return map_profiles.find(profile_name) != map_profiles.end();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool SaveProfile(const std::string& profile_name, std::size_t player_index) {
 | 
			
		||||
    if (!ProfileExistsInMap(profile_name)) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Settings::values.players.GetValue()[player_index].profile_name = profile_name;
 | 
			
		||||
    map_profiles[profile_name]->SaveAndroidControlPlayerValues(player_index);
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool LoadProfile(std::string& profile_name, std::size_t player_index) {
 | 
			
		||||
    if (!ProfileExistsInMap(profile_name)) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!ProfileExistsInFilesystem(profile_name)) {
 | 
			
		||||
        map_profiles.erase(profile_name);
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    LOG_INFO(Config, "Loading input profile `{}`", profile_name);
 | 
			
		||||
 | 
			
		||||
    Settings::values.players.GetValue()[player_index].profile_name = profile_name;
 | 
			
		||||
    map_profiles[profile_name]->ReadAndroidControlPlayerValues(player_index);
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ApplyControllerConfig(size_t player_index,
 | 
			
		||||
                           const std::function<void(Core::HID::EmulatedController*)>& apply) {
 | 
			
		||||
    auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
 | 
			
		||||
    if (player_index == 0) {
 | 
			
		||||
        auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
 | 
			
		||||
        auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
 | 
			
		||||
        handheld->EnableConfiguration();
 | 
			
		||||
        player_one->EnableConfiguration();
 | 
			
		||||
        apply(handheld);
 | 
			
		||||
        apply(player_one);
 | 
			
		||||
        handheld->DisableConfiguration();
 | 
			
		||||
        player_one->DisableConfiguration();
 | 
			
		||||
        handheld->SaveCurrentConfig();
 | 
			
		||||
        player_one->SaveCurrentConfig();
 | 
			
		||||
    } else {
 | 
			
		||||
        auto* controller = hid_core.GetEmulatedControllerByIndex(player_index);
 | 
			
		||||
        controller->EnableConfiguration();
 | 
			
		||||
        apply(controller);
 | 
			
		||||
        controller->DisableConfiguration();
 | 
			
		||||
        controller->SaveCurrentConfig();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ConnectController(size_t player_index, bool connected) {
 | 
			
		||||
    auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
 | 
			
		||||
    if (player_index == 0) {
 | 
			
		||||
        auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
 | 
			
		||||
        auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
 | 
			
		||||
        handheld->EnableConfiguration();
 | 
			
		||||
        player_one->EnableConfiguration();
 | 
			
		||||
        if (player_one->GetNpadStyleIndex(true) == Core::HID::NpadStyleIndex::Handheld) {
 | 
			
		||||
            if (connected) {
 | 
			
		||||
                handheld->Connect();
 | 
			
		||||
            } else {
 | 
			
		||||
                handheld->Disconnect();
 | 
			
		||||
            }
 | 
			
		||||
            player_one->Disconnect();
 | 
			
		||||
        } else {
 | 
			
		||||
            if (connected) {
 | 
			
		||||
                player_one->Connect();
 | 
			
		||||
            } else {
 | 
			
		||||
                player_one->Disconnect();
 | 
			
		||||
            }
 | 
			
		||||
            handheld->Disconnect();
 | 
			
		||||
        }
 | 
			
		||||
        handheld->DisableConfiguration();
 | 
			
		||||
        player_one->DisableConfiguration();
 | 
			
		||||
        handheld->SaveCurrentConfig();
 | 
			
		||||
        player_one->SaveCurrentConfig();
 | 
			
		||||
    } else {
 | 
			
		||||
        auto* controller = hid_core.GetEmulatedControllerByIndex(player_index);
 | 
			
		||||
        controller->EnableConfiguration();
 | 
			
		||||
        if (connected) {
 | 
			
		||||
            controller->Connect();
 | 
			
		||||
        } else {
 | 
			
		||||
            controller->Disconnect();
 | 
			
		||||
        }
 | 
			
		||||
        controller->DisableConfiguration();
 | 
			
		||||
        controller->SaveCurrentConfig();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extern "C" {
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isHandheldOnly(JNIEnv* env,
 | 
			
		||||
                                                                           jobject j_obj) {
 | 
			
		||||
    return IsHandheldOnly();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadButtonEvent(
 | 
			
		||||
    JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jint j_button_id, jint j_action) {
 | 
			
		||||
    EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetButtonState(
 | 
			
		||||
        Common::Android::GetJString(env, j_guid), j_port, j_button_id, j_action != 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadAxisEvent(
 | 
			
		||||
    JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jint j_stick_id, jfloat j_value) {
 | 
			
		||||
    EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetAxisPosition(
 | 
			
		||||
        Common::Android::GetJString(env, j_guid), j_port, j_stick_id, j_value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadMotionEvent(
 | 
			
		||||
    JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jlong j_delta_timestamp,
 | 
			
		||||
    jfloat j_x_gyro, jfloat j_y_gyro, jfloat j_z_gyro, jfloat j_x_accel, jfloat j_y_accel,
 | 
			
		||||
    jfloat j_z_accel) {
 | 
			
		||||
    EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetMotionState(
 | 
			
		||||
        Common::Android::GetJString(env, j_guid), j_port, j_delta_timestamp, j_x_gyro, j_y_gyro,
 | 
			
		||||
        j_z_gyro, j_x_accel, j_y_accel, j_z_accel);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onReadNfcTag(JNIEnv* env, jobject j_obj,
 | 
			
		||||
                                                                     jbyteArray j_data) {
 | 
			
		||||
    jboolean isCopy{false};
 | 
			
		||||
    std::span<u8> data(reinterpret_cast<u8*>(env->GetByteArrayElements(j_data, &isCopy)),
 | 
			
		||||
                       static_cast<size_t>(env->GetArrayLength(j_data)));
 | 
			
		||||
 | 
			
		||||
    if (EmulationSession::GetInstance().IsRunning()) {
 | 
			
		||||
        EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo()->LoadAmiibo(data);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onRemoveNfcTag(JNIEnv* env, jobject j_obj) {
 | 
			
		||||
    if (EmulationSession::GetInstance().IsRunning()) {
 | 
			
		||||
        EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo()->CloseAmiibo();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchPressed(JNIEnv* env, jobject j_obj,
 | 
			
		||||
                                                                       jint j_id, jfloat j_x_axis,
 | 
			
		||||
                                                                       jfloat j_y_axis) {
 | 
			
		||||
    if (EmulationSession::GetInstance().IsRunning()) {
 | 
			
		||||
        EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchPressed(
 | 
			
		||||
            j_id, j_x_axis, j_y_axis);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchMoved(JNIEnv* env, jobject j_obj,
 | 
			
		||||
                                                                     jint j_id, jfloat j_x_axis,
 | 
			
		||||
                                                                     jfloat j_y_axis) {
 | 
			
		||||
    if (EmulationSession::GetInstance().IsRunning()) {
 | 
			
		||||
        EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchMoved(
 | 
			
		||||
            j_id, j_x_axis, j_y_axis);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchReleased(JNIEnv* env, jobject j_obj,
 | 
			
		||||
                                                                        jint j_id) {
 | 
			
		||||
    if (EmulationSession::GetInstance().IsRunning()) {
 | 
			
		||||
        EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchReleased(j_id);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onOverlayButtonEventImpl(
 | 
			
		||||
    JNIEnv* env, jobject j_obj, jint j_port, jint j_button_id, jint j_action) {
 | 
			
		||||
    if (EmulationSession::GetInstance().IsRunning()) {
 | 
			
		||||
        EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetButtonState(
 | 
			
		||||
            j_port, j_button_id, j_action == 1);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onOverlayJoystickEventImpl(
 | 
			
		||||
    JNIEnv* env, jobject j_obj, jint j_port, jint j_stick_id, jfloat j_x_axis, jfloat j_y_axis) {
 | 
			
		||||
    if (EmulationSession::GetInstance().IsRunning()) {
 | 
			
		||||
        EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetStickPosition(
 | 
			
		||||
            j_port, j_stick_id, j_x_axis, j_y_axis);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onDeviceMotionEvent(
 | 
			
		||||
    JNIEnv* env, jobject j_obj, jint j_port, jlong j_delta_timestamp, jfloat j_x_gyro,
 | 
			
		||||
    jfloat j_y_gyro, jfloat j_z_gyro, jfloat j_x_accel, jfloat j_y_accel, jfloat j_z_accel) {
 | 
			
		||||
    if (EmulationSession::GetInstance().IsRunning()) {
 | 
			
		||||
        EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetMotionState(
 | 
			
		||||
            j_port, j_delta_timestamp, j_x_gyro, j_y_gyro, j_z_gyro, j_x_accel, j_y_accel,
 | 
			
		||||
            j_z_accel);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_reloadInputDevices(JNIEnv* env,
 | 
			
		||||
                                                                           jobject j_obj) {
 | 
			
		||||
    EmulationSession::GetInstance().System().HIDCore().ReloadInputDevices();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_registerController(JNIEnv* env,
 | 
			
		||||
                                                                           jobject j_obj,
 | 
			
		||||
                                                                           jobject j_device) {
 | 
			
		||||
    EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->RegisterController(j_device);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jobjectArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getInputDevices(JNIEnv* env,
 | 
			
		||||
                                                                                jobject j_obj) {
 | 
			
		||||
    auto devices = EmulationSession::GetInstance().GetInputSubsystem().GetInputDevices();
 | 
			
		||||
    jobjectArray jdevices = env->NewObjectArray(devices.size(), Common::Android::GetStringClass(),
 | 
			
		||||
                                                Common::Android::ToJString(env, ""));
 | 
			
		||||
    for (size_t i = 0; i < devices.size(); ++i) {
 | 
			
		||||
        env->SetObjectArrayElement(jdevices, i,
 | 
			
		||||
                                   Common::Android::ToJString(env, devices[i].Serialize()));
 | 
			
		||||
    }
 | 
			
		||||
    return jdevices;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadInputProfiles(JNIEnv* env,
 | 
			
		||||
                                                                          jobject j_obj) {
 | 
			
		||||
    map_profiles.clear();
 | 
			
		||||
    const auto input_profile_loc =
 | 
			
		||||
        Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "input";
 | 
			
		||||
 | 
			
		||||
    if (Common::FS::IsDir(input_profile_loc)) {
 | 
			
		||||
        Common::FS::IterateDirEntries(
 | 
			
		||||
            input_profile_loc,
 | 
			
		||||
            [&](const std::filesystem::path& full_path) {
 | 
			
		||||
                const auto filename = full_path.filename();
 | 
			
		||||
                const auto name_without_ext =
 | 
			
		||||
                    Common::FS::PathToUTF8String(GetNameWithoutExtension(filename));
 | 
			
		||||
 | 
			
		||||
                if (filename.extension() == ".ini" && IsProfileNameValid(name_without_ext)) {
 | 
			
		||||
                    map_profiles.insert_or_assign(
 | 
			
		||||
                        name_without_ext, std::make_unique<AndroidConfig>(
 | 
			
		||||
                                              name_without_ext, Config::ConfigType::InputProfile));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            },
 | 
			
		||||
            Common::FS::DirEntryFilter::File);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jobjectArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getInputProfileNames(
 | 
			
		||||
    JNIEnv* env, jobject j_obj) {
 | 
			
		||||
    std::vector<std::string> profile_names;
 | 
			
		||||
    profile_names.reserve(map_profiles.size());
 | 
			
		||||
 | 
			
		||||
    auto it = map_profiles.cbegin();
 | 
			
		||||
    while (it != map_profiles.cend()) {
 | 
			
		||||
        const auto& [profile_name, config] = *it;
 | 
			
		||||
        if (!ProfileExistsInFilesystem(profile_name)) {
 | 
			
		||||
            it = map_profiles.erase(it);
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        profile_names.push_back(profile_name);
 | 
			
		||||
        ++it;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::stable_sort(profile_names.begin(), profile_names.end());
 | 
			
		||||
 | 
			
		||||
    jobjectArray j_profile_names =
 | 
			
		||||
        env->NewObjectArray(profile_names.size(), Common::Android::GetStringClass(),
 | 
			
		||||
                            Common::Android::ToJString(env, ""));
 | 
			
		||||
    for (size_t i = 0; i < profile_names.size(); ++i) {
 | 
			
		||||
        env->SetObjectArrayElement(j_profile_names, i,
 | 
			
		||||
                                   Common::Android::ToJString(env, profile_names[i]));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return j_profile_names;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isProfileNameValid(JNIEnv* env,
 | 
			
		||||
                                                                               jobject j_obj,
 | 
			
		||||
                                                                               jstring j_name) {
 | 
			
		||||
    return Common::Android::GetJString(env, j_name).find_first_of("<>:;\"/\\|,.!?*") ==
 | 
			
		||||
           std::string::npos;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_createProfile(JNIEnv* env,
 | 
			
		||||
                                                                          jobject j_obj,
 | 
			
		||||
                                                                          jstring j_name,
 | 
			
		||||
                                                                          jint j_player_index) {
 | 
			
		||||
    auto profile_name = Common::Android::GetJString(env, j_name);
 | 
			
		||||
    if (ProfileExistsInMap(profile_name)) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    map_profiles.insert_or_assign(
 | 
			
		||||
        profile_name,
 | 
			
		||||
        std::make_unique<AndroidConfig>(profile_name, Config::ConfigType::InputProfile));
 | 
			
		||||
 | 
			
		||||
    return SaveProfile(profile_name, j_player_index);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_deleteProfile(JNIEnv* env,
 | 
			
		||||
                                                                          jobject j_obj,
 | 
			
		||||
                                                                          jstring j_name,
 | 
			
		||||
                                                                          jint j_player_index) {
 | 
			
		||||
    auto profile_name = Common::Android::GetJString(env, j_name);
 | 
			
		||||
    if (!ProfileExistsInMap(profile_name)) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!ProfileExistsInFilesystem(profile_name) ||
 | 
			
		||||
        Common::FS::RemoveFile(map_profiles[profile_name]->GetConfigFilePath())) {
 | 
			
		||||
        map_profiles.erase(profile_name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Settings::values.players.GetValue()[j_player_index].profile_name = "";
 | 
			
		||||
    return !ProfileExistsInMap(profile_name) && !ProfileExistsInFilesystem(profile_name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadProfile(JNIEnv* env, jobject j_obj,
 | 
			
		||||
                                                                        jstring j_name,
 | 
			
		||||
                                                                        jint j_player_index) {
 | 
			
		||||
    auto profile_name = Common::Android::GetJString(env, j_name);
 | 
			
		||||
    return LoadProfile(profile_name, j_player_index);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_saveProfile(JNIEnv* env, jobject j_obj,
 | 
			
		||||
                                                                        jstring j_name,
 | 
			
		||||
                                                                        jint j_player_index) {
 | 
			
		||||
    auto profile_name = Common::Android::GetJString(env, j_name);
 | 
			
		||||
    return SaveProfile(profile_name, j_player_index);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadPerGameConfiguration(
 | 
			
		||||
    JNIEnv* env, jobject j_obj, jint j_player_index, jint j_selected_index,
 | 
			
		||||
    jstring j_selected_profile_name) {
 | 
			
		||||
    static constexpr size_t HANDHELD_INDEX = 8;
 | 
			
		||||
 | 
			
		||||
    auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
 | 
			
		||||
    Settings::values.players.SetGlobal(false);
 | 
			
		||||
 | 
			
		||||
    auto profile_name = Common::Android::GetJString(env, j_selected_profile_name);
 | 
			
		||||
    auto* emulated_controller = hid_core.GetEmulatedControllerByIndex(j_player_index);
 | 
			
		||||
 | 
			
		||||
    if (j_selected_index == 0) {
 | 
			
		||||
        Settings::values.players.GetValue()[j_player_index].profile_name = "";
 | 
			
		||||
        if (j_player_index == 0) {
 | 
			
		||||
            Settings::values.players.GetValue()[HANDHELD_INDEX] = {};
 | 
			
		||||
        }
 | 
			
		||||
        Settings::values.players.SetGlobal(true);
 | 
			
		||||
        emulated_controller->ReloadFromSettings();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    if (profile_name.empty()) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    auto& player = Settings::values.players.GetValue()[j_player_index];
 | 
			
		||||
    auto& global_player = Settings::values.players.GetValue(true)[j_player_index];
 | 
			
		||||
    player.profile_name = profile_name;
 | 
			
		||||
    global_player.profile_name = profile_name;
 | 
			
		||||
    // Read from the profile into the custom player settings
 | 
			
		||||
    LoadProfile(profile_name, j_player_index);
 | 
			
		||||
    // Make sure the controller is connected
 | 
			
		||||
    player.connected = true;
 | 
			
		||||
 | 
			
		||||
    emulated_controller->ReloadFromSettings();
 | 
			
		||||
 | 
			
		||||
    if (j_player_index > 0) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    // Handle Handheld cases
 | 
			
		||||
    auto& handheld_player = Settings::values.players.GetValue()[HANDHELD_INDEX];
 | 
			
		||||
    auto* handheld_controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
 | 
			
		||||
    if (player.controller_type == Settings::ControllerType::Handheld) {
 | 
			
		||||
        handheld_player = player;
 | 
			
		||||
    } else {
 | 
			
		||||
        handheld_player = {};
 | 
			
		||||
    }
 | 
			
		||||
    handheld_controller->ReloadFromSettings();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_beginMapping(JNIEnv* env, jobject j_obj,
 | 
			
		||||
                                                                     jint jtype) {
 | 
			
		||||
    EmulationSession::GetInstance().GetInputSubsystem().BeginMapping(
 | 
			
		||||
        static_cast<InputCommon::Polling::InputType>(jtype));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getNextInput(JNIEnv* env,
 | 
			
		||||
                                                                        jobject j_obj) {
 | 
			
		||||
    return Common::Android::ToJString(
 | 
			
		||||
        env, EmulationSession::GetInstance().GetInputSubsystem().GetNextInput().Serialize());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_stopMapping(JNIEnv* env, jobject j_obj) {
 | 
			
		||||
    EmulationSession::GetInstance().GetInputSubsystem().StopMapping();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_updateMappingsWithDefaultImpl(
 | 
			
		||||
    JNIEnv* env, jobject j_obj, jint j_player_index, jstring j_device_params,
 | 
			
		||||
    jstring j_display_name) {
 | 
			
		||||
    auto& input_subsystem = EmulationSession::GetInstance().GetInputSubsystem();
 | 
			
		||||
 | 
			
		||||
    // Clear all previous mappings
 | 
			
		||||
    for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) {
 | 
			
		||||
        ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
 | 
			
		||||
            controller->SetButtonParam(button_id, {});
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) {
 | 
			
		||||
        ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
 | 
			
		||||
            controller->SetStickParam(analog_id, {});
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Apply new mappings
 | 
			
		||||
    auto device = Common::ParamPackage(Common::Android::GetJString(env, j_device_params));
 | 
			
		||||
    auto button_mappings = input_subsystem.GetButtonMappingForDevice(device);
 | 
			
		||||
    auto analog_mappings = input_subsystem.GetAnalogMappingForDevice(device);
 | 
			
		||||
    auto display_name = Common::Android::GetJString(env, j_display_name);
 | 
			
		||||
    for (const auto& button_mapping : button_mappings) {
 | 
			
		||||
        const std::size_t index = button_mapping.first;
 | 
			
		||||
        auto named_mapping = button_mapping.second;
 | 
			
		||||
        named_mapping.Set("display", display_name);
 | 
			
		||||
        ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
 | 
			
		||||
            controller->SetButtonParam(index, named_mapping);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    for (const auto& analog_mapping : analog_mappings) {
 | 
			
		||||
        const std::size_t index = analog_mapping.first;
 | 
			
		||||
        auto named_mapping = analog_mapping.second;
 | 
			
		||||
        named_mapping.Set("display", display_name);
 | 
			
		||||
        ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
 | 
			
		||||
            controller->SetStickParam(index, named_mapping);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getButtonParamImpl(JNIEnv* env,
 | 
			
		||||
                                                                              jobject j_obj,
 | 
			
		||||
                                                                              jint j_player_index,
 | 
			
		||||
                                                                              jint j_button) {
 | 
			
		||||
    return Common::Android::ToJString(env, EmulationSession::GetInstance()
 | 
			
		||||
                                               .System()
 | 
			
		||||
                                               .HIDCore()
 | 
			
		||||
                                               .GetEmulatedControllerByIndex(j_player_index)
 | 
			
		||||
                                               ->GetButtonParam(j_button)
 | 
			
		||||
                                               .Serialize());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setButtonParamImpl(
 | 
			
		||||
    JNIEnv* env, jobject j_obj, jint j_player_index, jint j_button_id, jstring j_param) {
 | 
			
		||||
    ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
 | 
			
		||||
        controller->SetButtonParam(j_button_id,
 | 
			
		||||
                                   Common::ParamPackage(Common::Android::GetJString(env, j_param)));
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getStickParamImpl(JNIEnv* env,
 | 
			
		||||
                                                                             jobject j_obj,
 | 
			
		||||
                                                                             jint j_player_index,
 | 
			
		||||
                                                                             jint j_stick) {
 | 
			
		||||
    return Common::Android::ToJString(env, EmulationSession::GetInstance()
 | 
			
		||||
                                               .System()
 | 
			
		||||
                                               .HIDCore()
 | 
			
		||||
                                               .GetEmulatedControllerByIndex(j_player_index)
 | 
			
		||||
                                               ->GetStickParam(j_stick)
 | 
			
		||||
                                               .Serialize());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setStickParamImpl(
 | 
			
		||||
    JNIEnv* env, jobject j_obj, jint j_player_index, jint j_stick_id, jstring j_param) {
 | 
			
		||||
    ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
 | 
			
		||||
        controller->SetStickParam(j_stick_id,
 | 
			
		||||
                                  Common::ParamPackage(Common::Android::GetJString(env, j_param)));
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jint Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getButtonNameImpl(JNIEnv* env,
 | 
			
		||||
                                                                          jobject j_obj,
 | 
			
		||||
                                                                          jstring j_param) {
 | 
			
		||||
    return static_cast<jint>(EmulationSession::GetInstance().GetInputSubsystem().GetButtonName(
 | 
			
		||||
        Common::ParamPackage(Common::Android::GetJString(env, j_param))));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jintArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getSupportedStyleTagsImpl(
 | 
			
		||||
    JNIEnv* env, jobject j_obj, jint j_player_index) {
 | 
			
		||||
    auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
 | 
			
		||||
    const auto npad_style_set = hid_core.GetSupportedStyleTag();
 | 
			
		||||
    std::vector<s32> supported_indexes;
 | 
			
		||||
    if (npad_style_set.fullkey == 1) {
 | 
			
		||||
        supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::Fullkey));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (npad_style_set.joycon_dual == 1) {
 | 
			
		||||
        supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconDual));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (npad_style_set.joycon_left == 1) {
 | 
			
		||||
        supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconLeft));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (npad_style_set.joycon_right == 1) {
 | 
			
		||||
        supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconRight));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (j_player_index == 0 && npad_style_set.handheld == 1) {
 | 
			
		||||
        supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::Handheld));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (npad_style_set.gamecube == 1) {
 | 
			
		||||
        supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::GameCube));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    jintArray j_supported_indexes = env->NewIntArray(supported_indexes.size());
 | 
			
		||||
    env->SetIntArrayRegion(j_supported_indexes, 0, supported_indexes.size(),
 | 
			
		||||
                           supported_indexes.data());
 | 
			
		||||
    return j_supported_indexes;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jint Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getStyleIndexImpl(JNIEnv* env,
 | 
			
		||||
                                                                          jobject j_obj,
 | 
			
		||||
                                                                          jint j_player_index) {
 | 
			
		||||
    return static_cast<s32>(EmulationSession::GetInstance()
 | 
			
		||||
                                .System()
 | 
			
		||||
                                .HIDCore()
 | 
			
		||||
                                .GetEmulatedControllerByIndex(j_player_index)
 | 
			
		||||
                                ->GetNpadStyleIndex(true));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setStyleIndexImpl(JNIEnv* env,
 | 
			
		||||
                                                                          jobject j_obj,
 | 
			
		||||
                                                                          jint j_player_index,
 | 
			
		||||
                                                                          jint j_style_index) {
 | 
			
		||||
    auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
 | 
			
		||||
    auto type = static_cast<Core::HID::NpadStyleIndex>(j_style_index);
 | 
			
		||||
    ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
 | 
			
		||||
        controller->SetNpadStyleIndex(type);
 | 
			
		||||
    });
 | 
			
		||||
    if (j_player_index == 0) {
 | 
			
		||||
        auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
 | 
			
		||||
        auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
 | 
			
		||||
        ConnectController(j_player_index,
 | 
			
		||||
                          player_one->IsConnected(true) || handheld->IsConnected(true));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isControllerImpl(JNIEnv* env,
 | 
			
		||||
                                                                             jobject j_obj,
 | 
			
		||||
                                                                             jstring jparams) {
 | 
			
		||||
    return static_cast<jint>(EmulationSession::GetInstance().GetInputSubsystem().IsController(
 | 
			
		||||
        Common::ParamPackage(Common::Android::GetJString(env, jparams))));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getIsConnected(JNIEnv* env,
 | 
			
		||||
                                                                           jobject j_obj,
 | 
			
		||||
                                                                           jint j_player_index) {
 | 
			
		||||
    auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
 | 
			
		||||
    auto* controller = hid_core.GetEmulatedControllerByIndex(static_cast<size_t>(j_player_index));
 | 
			
		||||
    if (j_player_index == 0 &&
 | 
			
		||||
        controller->GetNpadStyleIndex(true) == Core::HID::NpadStyleIndex::Handheld) {
 | 
			
		||||
        return hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld)->IsConnected(true);
 | 
			
		||||
    }
 | 
			
		||||
    return controller->IsConnected(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_connectControllersImpl(
 | 
			
		||||
    JNIEnv* env, jobject j_obj, jbooleanArray j_connected) {
 | 
			
		||||
    jboolean isCopy = false;
 | 
			
		||||
    auto j_connected_array_size = env->GetArrayLength(j_connected);
 | 
			
		||||
    jboolean* j_connected_array = env->GetBooleanArrayElements(j_connected, &isCopy);
 | 
			
		||||
    for (int i = 0; i < j_connected_array_size; ++i) {
 | 
			
		||||
        ConnectController(i, j_connected_array[i]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_resetControllerMappings(
 | 
			
		||||
    JNIEnv* env, jobject j_obj, jint j_player_index) {
 | 
			
		||||
    // Clear all previous mappings
 | 
			
		||||
    for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) {
 | 
			
		||||
        ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
 | 
			
		||||
            controller->SetButtonParam(button_id, {});
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) {
 | 
			
		||||
        ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
 | 
			
		||||
            controller->SetStickParam(analog_id, {});
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // extern "C"
 | 
			
		||||
							
								
								
									
										142
									
								
								src/android/app/src/main/res/drawable/button_anim.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								src/android/app/src/main/res/drawable/button_anim.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,142 @@
 | 
			
		||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:aapt="http://schemas.android.com/aapt">
 | 
			
		||||
    <aapt:attr name="android:drawable">
 | 
			
		||||
        <vector
 | 
			
		||||
            android:width="1000dp"
 | 
			
		||||
            android:height="1000dp"
 | 
			
		||||
            android:viewportWidth="1000"
 | 
			
		||||
            android:viewportHeight="1000">
 | 
			
		||||
            <group android:name="_R_G">
 | 
			
		||||
                <group
 | 
			
		||||
                    android:name="_R_G_L_0_G"
 | 
			
		||||
                    android:pivotX="100"
 | 
			
		||||
                    android:pivotY="100"
 | 
			
		||||
                    android:scaleX="4.5"
 | 
			
		||||
                    android:scaleY="4.5"
 | 
			
		||||
                    android:translateX="400"
 | 
			
		||||
                    android:translateY="400">
 | 
			
		||||
                    <path
 | 
			
		||||
                        android:name="_R_G_L_0_G_D_0_P_0"
 | 
			
		||||
                        android:fillAlpha="1"
 | 
			
		||||
                        android:fillColor="?attr/colorSecondaryContainer"
 | 
			
		||||
                        android:fillType="nonZero"
 | 
			
		||||
                        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 " />
 | 
			
		||||
                    <path
 | 
			
		||||
                        android:name="_R_G_L_0_G_D_2_P_0"
 | 
			
		||||
                        android:fillAlpha="0.8"
 | 
			
		||||
                        android:fillColor="?attr/colorOnSecondaryContainer"
 | 
			
		||||
                        android:fillType="nonZero"
 | 
			
		||||
                        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 " />
 | 
			
		||||
                </group>
 | 
			
		||||
            </group>
 | 
			
		||||
            <group android:name="time_group" />
 | 
			
		||||
        </vector>
 | 
			
		||||
    </aapt:attr>
 | 
			
		||||
    <target android:name="_R_G_L_0_G">
 | 
			
		||||
        <aapt:attr name="android:animation">
 | 
			
		||||
            <set android:ordering="together">
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="100"
 | 
			
		||||
                    android:propertyName="scaleX"
 | 
			
		||||
                    android:startOffset="0"
 | 
			
		||||
                    android:valueFrom="4.5"
 | 
			
		||||
                    android:valueTo="3.75"
 | 
			
		||||
                    android:valueType="floatType">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="100"
 | 
			
		||||
                    android:propertyName="scaleY"
 | 
			
		||||
                    android:startOffset="0"
 | 
			
		||||
                    android:valueFrom="4.5"
 | 
			
		||||
                    android:valueTo="3.75"
 | 
			
		||||
                    android:valueType="floatType">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="234"
 | 
			
		||||
                    android:propertyName="scaleX"
 | 
			
		||||
                    android:startOffset="100"
 | 
			
		||||
                    android:valueFrom="3.75"
 | 
			
		||||
                    android:valueTo="3.75"
 | 
			
		||||
                    android:valueType="floatType">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="234"
 | 
			
		||||
                    android:propertyName="scaleY"
 | 
			
		||||
                    android:startOffset="100"
 | 
			
		||||
                    android:valueFrom="3.75"
 | 
			
		||||
                    android:valueTo="3.75"
 | 
			
		||||
                    android:valueType="floatType">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="167"
 | 
			
		||||
                    android:propertyName="scaleX"
 | 
			
		||||
                    android:startOffset="334"
 | 
			
		||||
                    android:valueFrom="3.75"
 | 
			
		||||
                    android:valueTo="4.75"
 | 
			
		||||
                    android:valueType="floatType">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="167"
 | 
			
		||||
                    android:propertyName="scaleY"
 | 
			
		||||
                    android:startOffset="334"
 | 
			
		||||
                    android:valueFrom="3.75"
 | 
			
		||||
                    android:valueTo="4.75"
 | 
			
		||||
                    android:valueType="floatType">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="67"
 | 
			
		||||
                    android:propertyName="scaleX"
 | 
			
		||||
                    android:startOffset="501"
 | 
			
		||||
                    android:valueFrom="4.75"
 | 
			
		||||
                    android:valueTo="4.5"
 | 
			
		||||
                    android:valueType="floatType">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="67"
 | 
			
		||||
                    android:propertyName="scaleY"
 | 
			
		||||
                    android:startOffset="501"
 | 
			
		||||
                    android:valueFrom="4.75"
 | 
			
		||||
                    android:valueTo="4.5"
 | 
			
		||||
                    android:valueType="floatType">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
            </set>
 | 
			
		||||
        </aapt:attr>
 | 
			
		||||
    </target>
 | 
			
		||||
    <target android:name="time_group">
 | 
			
		||||
        <aapt:attr name="android:animation">
 | 
			
		||||
            <set android:ordering="together">
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="1034"
 | 
			
		||||
                    android:propertyName="translateX"
 | 
			
		||||
                    android:startOffset="0"
 | 
			
		||||
                    android:valueFrom="0"
 | 
			
		||||
                    android:valueTo="1"
 | 
			
		||||
                    android:valueType="floatType" />
 | 
			
		||||
            </set>
 | 
			
		||||
        </aapt:attr>
 | 
			
		||||
    </target>
 | 
			
		||||
</animated-vector>
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:width="24dp"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportWidth="960"
 | 
			
		||||
    android:viewportHeight="960">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="?attr/colorControlNormal"
 | 
			
		||||
        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" />
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_more_vert.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_more_vert.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportHeight="24"
 | 
			
		||||
    android:viewportWidth="24"
 | 
			
		||||
    android:width="24dp">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="?attr/colorControlNormal"
 | 
			
		||||
        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" />
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_new_label.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_new_label.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:width="24dp"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportWidth="24"
 | 
			
		||||
    android:viewportHeight="24">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="?attr/colorControlNormal"
 | 
			
		||||
        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" />
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										21
									
								
								src/android/app/src/main/res/drawable/ic_overlay.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/android/app/src/main/res/drawable/ic_overlay.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:width="24dp"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportWidth="24"
 | 
			
		||||
    android:viewportHeight="24">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="?attr/colorControlNormal"
 | 
			
		||||
        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" />
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="?attr/colorControlNormal"
 | 
			
		||||
        android:pathData="M15,11.25h1.5v1.5h-1.5z" />
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="?attr/colorControlNormal"
 | 
			
		||||
        android:pathData="M12.5,11.25h1.5v1.5h-1.5z" />
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="?attr/colorControlNormal"
 | 
			
		||||
        android:pathData="M10,11.25h1.5v1.5h-1.5z" />
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="?attr/colorControlNormal"
 | 
			
		||||
        android:pathData="M7.5,11.25h1.5v1.5h-1.5z" />
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_share.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_share.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:width="24dp"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportWidth="24"
 | 
			
		||||
    android:viewportHeight="24">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="?attr/colorControlNormal"
 | 
			
		||||
        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" />
 | 
			
		||||
</vector>
 | 
			
		||||
@@ -0,0 +1,118 @@
 | 
			
		||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:aapt="http://schemas.android.com/aapt">
 | 
			
		||||
    <aapt:attr name="android:drawable">
 | 
			
		||||
        <vector
 | 
			
		||||
            android:width="1000dp"
 | 
			
		||||
            android:height="1000dp"
 | 
			
		||||
            android:viewportWidth="1000"
 | 
			
		||||
            android:viewportHeight="1000">
 | 
			
		||||
            <group android:name="_R_G">
 | 
			
		||||
                <group
 | 
			
		||||
                    android:name="_R_G_L_1_G"
 | 
			
		||||
                    android:pivotX="100"
 | 
			
		||||
                    android:pivotY="100"
 | 
			
		||||
                    android:scaleX="5"
 | 
			
		||||
                    android:scaleY="5"
 | 
			
		||||
                    android:translateX="400"
 | 
			
		||||
                    android:translateY="400">
 | 
			
		||||
                    <path
 | 
			
		||||
                        android:name="_R_G_L_1_G_D_0_P_0"
 | 
			
		||||
                        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 "
 | 
			
		||||
                        android:strokeWidth="1"
 | 
			
		||||
                        android:strokeAlpha="0.6"
 | 
			
		||||
                        android:strokeColor="?attr/colorOutline"
 | 
			
		||||
                        android:strokeLineCap="round"
 | 
			
		||||
                        android:strokeLineJoin="round" />
 | 
			
		||||
                </group>
 | 
			
		||||
                <group
 | 
			
		||||
                    android:name="_R_G_L_0_G_T_1"
 | 
			
		||||
                    android:scaleX="5"
 | 
			
		||||
                    android:scaleY="5"
 | 
			
		||||
                    android:translateX="500"
 | 
			
		||||
                    android:translateY="500">
 | 
			
		||||
                    <group
 | 
			
		||||
                        android:name="_R_G_L_0_G"
 | 
			
		||||
                        android:translateX="-100"
 | 
			
		||||
                        android:translateY="-100">
 | 
			
		||||
                        <path
 | 
			
		||||
                            android:name="_R_G_L_0_G_D_0_P_0"
 | 
			
		||||
                            android:fillAlpha="1"
 | 
			
		||||
                            android:fillColor="?attr/colorSecondaryContainer"
 | 
			
		||||
                            android:fillType="nonZero"
 | 
			
		||||
                            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 " />
 | 
			
		||||
                        <path
 | 
			
		||||
                            android:name="_R_G_L_0_G_D_2_P_0"
 | 
			
		||||
                            android:fillAlpha="0.8"
 | 
			
		||||
                            android:fillColor="?attr/colorOnSecondaryContainer"
 | 
			
		||||
                            android:fillType="nonZero"
 | 
			
		||||
                            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 " />
 | 
			
		||||
                    </group>
 | 
			
		||||
                </group>
 | 
			
		||||
            </group>
 | 
			
		||||
            <group android:name="time_group" />
 | 
			
		||||
        </vector>
 | 
			
		||||
    </aapt:attr>
 | 
			
		||||
    <target android:name="_R_G_L_0_G_T_1">
 | 
			
		||||
        <aapt:attr name="android:animation">
 | 
			
		||||
            <set android:ordering="together">
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="267"
 | 
			
		||||
                    android:pathData="M 500,500C 500,500 364,500 364,500"
 | 
			
		||||
                    android:propertyName="translateXY"
 | 
			
		||||
                    android:propertyXName="translateX"
 | 
			
		||||
                    android:propertyYName="translateY"
 | 
			
		||||
                    android:startOffset="0">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="234"
 | 
			
		||||
                    android:pathData="M 364,500C 364,500 364,500 364,500"
 | 
			
		||||
                    android:propertyName="translateXY"
 | 
			
		||||
                    android:propertyXName="translateX"
 | 
			
		||||
                    android:propertyYName="translateY"
 | 
			
		||||
                    android:startOffset="267">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="133"
 | 
			
		||||
                    android:pathData="M 364,500C 364,500 525,500 525,500"
 | 
			
		||||
                    android:propertyName="translateXY"
 | 
			
		||||
                    android:propertyXName="translateX"
 | 
			
		||||
                    android:propertyYName="translateY"
 | 
			
		||||
                    android:startOffset="501">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="100"
 | 
			
		||||
                    android:pathData="M 525,500C 525,500 500,500 500,500"
 | 
			
		||||
                    android:propertyName="translateXY"
 | 
			
		||||
                    android:propertyXName="translateX"
 | 
			
		||||
                    android:propertyYName="translateY"
 | 
			
		||||
                    android:startOffset="634">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
            </set>
 | 
			
		||||
        </aapt:attr>
 | 
			
		||||
    </target>
 | 
			
		||||
    <target android:name="time_group">
 | 
			
		||||
        <aapt:attr name="android:animation">
 | 
			
		||||
            <set android:ordering="together">
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="968"
 | 
			
		||||
                    android:propertyName="translateX"
 | 
			
		||||
                    android:startOffset="0"
 | 
			
		||||
                    android:valueFrom="0"
 | 
			
		||||
                    android:valueTo="1"
 | 
			
		||||
                    android:valueType="floatType" />
 | 
			
		||||
            </set>
 | 
			
		||||
        </aapt:attr>
 | 
			
		||||
    </target>
 | 
			
		||||
</animated-vector>
 | 
			
		||||
@@ -0,0 +1,173 @@
 | 
			
		||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:aapt="http://schemas.android.com/aapt">
 | 
			
		||||
    <aapt:attr name="android:drawable">
 | 
			
		||||
        <vector
 | 
			
		||||
            android:width="1000dp"
 | 
			
		||||
            android:height="1000dp"
 | 
			
		||||
            android:viewportWidth="1000"
 | 
			
		||||
            android:viewportHeight="1000">
 | 
			
		||||
            <group android:name="_R_G">
 | 
			
		||||
                <group
 | 
			
		||||
                    android:name="_R_G_L_1_G"
 | 
			
		||||
                    android:pivotX="100"
 | 
			
		||||
                    android:pivotY="100"
 | 
			
		||||
                    android:scaleX="5"
 | 
			
		||||
                    android:scaleY="5"
 | 
			
		||||
                    android:translateX="400"
 | 
			
		||||
                    android:translateY="400">
 | 
			
		||||
                    <path
 | 
			
		||||
                        android:name="_R_G_L_1_G_D_0_P_0"
 | 
			
		||||
                        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 "
 | 
			
		||||
                        android:strokeWidth="1"
 | 
			
		||||
                        android:strokeAlpha="0.6"
 | 
			
		||||
                        android:strokeColor="?attr/colorOutline"
 | 
			
		||||
                        android:strokeLineCap="round"
 | 
			
		||||
                        android:strokeLineJoin="round" />
 | 
			
		||||
                </group>
 | 
			
		||||
                <group
 | 
			
		||||
                    android:name="_R_G_L_0_G_T_1"
 | 
			
		||||
                    android:scaleX="5"
 | 
			
		||||
                    android:scaleY="5"
 | 
			
		||||
                    android:translateX="500"
 | 
			
		||||
                    android:translateY="500">
 | 
			
		||||
                    <group
 | 
			
		||||
                        android:name="_R_G_L_0_G"
 | 
			
		||||
                        android:translateX="-100"
 | 
			
		||||
                        android:translateY="-100">
 | 
			
		||||
                        <path
 | 
			
		||||
                            android:name="_R_G_L_0_G_D_0_P_0"
 | 
			
		||||
                            android:fillAlpha="1"
 | 
			
		||||
                            android:fillColor="?attr/colorSecondaryContainer"
 | 
			
		||||
                            android:fillType="nonZero"
 | 
			
		||||
                            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 " />
 | 
			
		||||
                        <path
 | 
			
		||||
                            android:name="_R_G_L_0_G_D_2_P_0"
 | 
			
		||||
                            android:fillAlpha="0.8"
 | 
			
		||||
                            android:fillColor="?attr/colorOnSecondaryContainer"
 | 
			
		||||
                            android:fillType="nonZero"
 | 
			
		||||
                            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 " />
 | 
			
		||||
                    </group>
 | 
			
		||||
                </group>
 | 
			
		||||
            </group>
 | 
			
		||||
            <group android:name="time_group" />
 | 
			
		||||
        </vector>
 | 
			
		||||
    </aapt:attr>
 | 
			
		||||
    <target android:name="_R_G_L_0_G_T_1">
 | 
			
		||||
        <aapt:attr name="android:animation">
 | 
			
		||||
            <set android:ordering="together">
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="267"
 | 
			
		||||
                    android:pathData="M 500,500C 500,500 364,500 364,500"
 | 
			
		||||
                    android:propertyName="translateXY"
 | 
			
		||||
                    android:propertyXName="translateX"
 | 
			
		||||
                    android:propertyYName="translateY"
 | 
			
		||||
                    android:startOffset="0">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="234"
 | 
			
		||||
                    android:pathData="M 364,500C 364,500 364,500 364,500"
 | 
			
		||||
                    android:propertyName="translateXY"
 | 
			
		||||
                    android:propertyXName="translateX"
 | 
			
		||||
                    android:propertyYName="translateY"
 | 
			
		||||
                    android:startOffset="267">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="133"
 | 
			
		||||
                    android:pathData="M 364,500C 364,500 525,500 525,500"
 | 
			
		||||
                    android:propertyName="translateXY"
 | 
			
		||||
                    android:propertyXName="translateX"
 | 
			
		||||
                    android:propertyYName="translateY"
 | 
			
		||||
                    android:startOffset="501">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="100"
 | 
			
		||||
                    android:pathData="M 525,500C 525,500 500,500 500,500"
 | 
			
		||||
                    android:propertyName="translateXY"
 | 
			
		||||
                    android:propertyXName="translateX"
 | 
			
		||||
                    android:propertyYName="translateY"
 | 
			
		||||
                    android:startOffset="634">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="400"
 | 
			
		||||
                    android:pathData="M 500,500C 500,500 500,500 500,500"
 | 
			
		||||
                    android:propertyName="translateXY"
 | 
			
		||||
                    android:propertyXName="translateX"
 | 
			
		||||
                    android:propertyYName="translateY"
 | 
			
		||||
                    android:startOffset="734">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="267"
 | 
			
		||||
                    android:pathData="M 500,500C 500,500 500,364 500,364"
 | 
			
		||||
                    android:propertyName="translateXY"
 | 
			
		||||
                    android:propertyXName="translateX"
 | 
			
		||||
                    android:propertyYName="translateY"
 | 
			
		||||
                    android:startOffset="1134">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="234"
 | 
			
		||||
                    android:pathData="M 500,364C 500,364 500,364 500,364"
 | 
			
		||||
                    android:propertyName="translateXY"
 | 
			
		||||
                    android:propertyXName="translateX"
 | 
			
		||||
                    android:propertyYName="translateY"
 | 
			
		||||
                    android:startOffset="1401">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="133"
 | 
			
		||||
                    android:pathData="M 500,364C 500,364 500,535 500,535"
 | 
			
		||||
                    android:propertyName="translateXY"
 | 
			
		||||
                    android:propertyXName="translateX"
 | 
			
		||||
                    android:propertyYName="translateY"
 | 
			
		||||
                    android:startOffset="1635">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="100"
 | 
			
		||||
                    android:pathData="M 500,535C 500,535 500,500 500,500"
 | 
			
		||||
                    android:propertyName="translateXY"
 | 
			
		||||
                    android:propertyXName="translateX"
 | 
			
		||||
                    android:propertyYName="translateY"
 | 
			
		||||
                    android:startOffset="1768">
 | 
			
		||||
                    <aapt:attr name="android:interpolator">
 | 
			
		||||
                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
 | 
			
		||||
                    </aapt:attr>
 | 
			
		||||
                </objectAnimator>
 | 
			
		||||
            </set>
 | 
			
		||||
        </aapt:attr>
 | 
			
		||||
    </target>
 | 
			
		||||
    <target android:name="time_group">
 | 
			
		||||
        <aapt:attr name="android:animation">
 | 
			
		||||
            <set android:ordering="together">
 | 
			
		||||
                <objectAnimator
 | 
			
		||||
                    android:duration="2269"
 | 
			
		||||
                    android:propertyName="translateX"
 | 
			
		||||
                    android:startOffset="0"
 | 
			
		||||
                    android:valueFrom="0"
 | 
			
		||||
                    android:valueTo="1"
 | 
			
		||||
                    android:valueType="floatType" />
 | 
			
		||||
            </set>
 | 
			
		||||
        </aapt:attr>
 | 
			
		||||
    </target>
 | 
			
		||||
</animated-vector>
 | 
			
		||||
@@ -0,0 +1,63 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:app="http://schemas.android.com/apk/res-auto"
 | 
			
		||||
    xmlns:tools="http://schemas.android.com/tools"
 | 
			
		||||
    android:id="@+id/setting_body"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="wrap_content"
 | 
			
		||||
    android:background="?android:attr/selectableItemBackground"
 | 
			
		||||
    android:clickable="true"
 | 
			
		||||
    android:focusable="true"
 | 
			
		||||
    android:gravity="center_vertical"
 | 
			
		||||
    android:minHeight="72dp"
 | 
			
		||||
    android:padding="16dp"
 | 
			
		||||
    android:nextFocusLeft="@id/button_options">
 | 
			
		||||
 | 
			
		||||
    <LinearLayout
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:gravity="center_vertical"
 | 
			
		||||
        android:orientation="horizontal">
 | 
			
		||||
 | 
			
		||||
        <LinearLayout
 | 
			
		||||
            android:layout_width="0dp"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:orientation="vertical"
 | 
			
		||||
            android:layout_weight="1">
 | 
			
		||||
 | 
			
		||||
            <com.google.android.material.textview.MaterialTextView
 | 
			
		||||
                android:id="@+id/text_setting_name"
 | 
			
		||||
                style="@style/TextAppearance.Material3.HeadlineMedium"
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:textAlignment="viewStart"
 | 
			
		||||
                android:textSize="17sp"
 | 
			
		||||
                app:lineHeight="22dp"
 | 
			
		||||
                tools:text="Setting Name" />
 | 
			
		||||
 | 
			
		||||
            <com.google.android.material.textview.MaterialTextView
 | 
			
		||||
                android:id="@+id/text_setting_value"
 | 
			
		||||
                style="@style/TextAppearance.Material3.LabelMedium"
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_marginTop="@dimen/spacing_small"
 | 
			
		||||
                android:textAlignment="viewStart"
 | 
			
		||||
                android:textStyle="bold"
 | 
			
		||||
                android:textSize="13sp"
 | 
			
		||||
                tools:text="1x" />
 | 
			
		||||
 | 
			
		||||
        </LinearLayout>
 | 
			
		||||
 | 
			
		||||
        <Button
 | 
			
		||||
            android:id="@+id/button_options"
 | 
			
		||||
            style="?attr/materialIconButtonStyle"
 | 
			
		||||
            android:layout_width="wrap_content"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:nextFocusRight="@id/setting_body"
 | 
			
		||||
            app:icon="@drawable/ic_more_vert"
 | 
			
		||||
            app:iconSize="24dp"
 | 
			
		||||
            app:iconTint="?attr/colorOnSurface" />
 | 
			
		||||
 | 
			
		||||
    </LinearLayout>
 | 
			
		||||
 | 
			
		||||
</RelativeLayout>
 | 
			
		||||
@@ -0,0 +1,6 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:id="@+id/list_profiles"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="wrap_content"
 | 
			
		||||
    android:fadeScrollbars="false" />
 | 
			
		||||
							
								
								
									
										26
									
								
								src/android/app/src/main/res/layout/dialog_mapping.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/android/app/src/main/res/layout/dialog_mapping.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="wrap_content"
 | 
			
		||||
    xmlns:tools="http://schemas.android.com/tools"
 | 
			
		||||
    android:defaultFocusHighlightEnabled="false"
 | 
			
		||||
    android:focusable="true"
 | 
			
		||||
    android:focusableInTouchMode="true"
 | 
			
		||||
    android:focusedByDefault="true"
 | 
			
		||||
    android:orientation="horizontal"
 | 
			
		||||
    android:gravity="center">
 | 
			
		||||
 | 
			
		||||
    <ImageView
 | 
			
		||||
        android:id="@+id/image_stick_animation"
 | 
			
		||||
        android:layout_width="@dimen/mapping_anim_size"
 | 
			
		||||
        android:layout_height="@dimen/mapping_anim_size"
 | 
			
		||||
        tools:src="@drawable/stick_two_direction_anim" />
 | 
			
		||||
 | 
			
		||||
    <ImageView
 | 
			
		||||
        android:id="@+id/image_button_animation"
 | 
			
		||||
        android:layout_width="@dimen/mapping_anim_size"
 | 
			
		||||
        android:layout_height="@dimen/mapping_anim_size"
 | 
			
		||||
        android:layout_marginStart="48dp"
 | 
			
		||||
        tools:src="@drawable/button_anim" />
 | 
			
		||||
 | 
			
		||||
</LinearLayout>
 | 
			
		||||
@@ -0,0 +1,74 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:app="http://schemas.android.com/apk/res-auto"
 | 
			
		||||
    xmlns:tools="http://schemas.android.com/tools"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="wrap_content"
 | 
			
		||||
    android:focusable="false"
 | 
			
		||||
    android:paddingHorizontal="20dp"
 | 
			
		||||
    android:paddingVertical="16dp">
 | 
			
		||||
 | 
			
		||||
    <com.google.android.material.textview.MaterialTextView
 | 
			
		||||
        android:id="@+id/title"
 | 
			
		||||
        style="@style/TextAppearance.Material3.HeadlineMedium"
 | 
			
		||||
        android:layout_width="0dp"
 | 
			
		||||
        android:layout_height="0dp"
 | 
			
		||||
        android:textAlignment="viewStart"
 | 
			
		||||
        android:gravity="start|center_vertical"
 | 
			
		||||
        android:textSize="17sp"
 | 
			
		||||
        android:layout_marginEnd="16dp"
 | 
			
		||||
        app:layout_constraintBottom_toBottomOf="@+id/button_layout"
 | 
			
		||||
        app:layout_constraintEnd_toStartOf="@+id/button_layout"
 | 
			
		||||
        app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
        app:layout_constraintTop_toTopOf="parent"
 | 
			
		||||
        app:lineHeight="28dp"
 | 
			
		||||
        tools:text="My profile" />
 | 
			
		||||
 | 
			
		||||
    <LinearLayout
 | 
			
		||||
        android:id="@+id/button_layout"
 | 
			
		||||
        android:layout_width="wrap_content"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:gravity="center_vertical"
 | 
			
		||||
        android:orientation="horizontal"
 | 
			
		||||
        app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
        app:layout_constraintTop_toTopOf="parent">
 | 
			
		||||
 | 
			
		||||
        <Button
 | 
			
		||||
            android:id="@+id/button_new"
 | 
			
		||||
            style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
 | 
			
		||||
            android:layout_width="wrap_content"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:contentDescription="@string/create_new_profile"
 | 
			
		||||
            android:tooltipText="@string/create_new_profile"
 | 
			
		||||
            app:icon="@drawable/ic_new_label" />
 | 
			
		||||
 | 
			
		||||
        <Button
 | 
			
		||||
            android:id="@+id/button_delete"
 | 
			
		||||
            style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
 | 
			
		||||
            android:layout_width="wrap_content"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:contentDescription="@string/delete"
 | 
			
		||||
            android:tooltipText="@string/delete"
 | 
			
		||||
            app:icon="@drawable/ic_delete" />
 | 
			
		||||
 | 
			
		||||
        <Button
 | 
			
		||||
            android:id="@+id/button_save"
 | 
			
		||||
            style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
 | 
			
		||||
            android:layout_width="wrap_content"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:contentDescription="@string/save"
 | 
			
		||||
            android:tooltipText="@string/save"
 | 
			
		||||
            app:icon="@drawable/ic_save" />
 | 
			
		||||
 | 
			
		||||
        <Button
 | 
			
		||||
            android:id="@+id/button_load"
 | 
			
		||||
            style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
 | 
			
		||||
            android:layout_width="wrap_content"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:contentDescription="@string/load"
 | 
			
		||||
            android:tooltipText="@string/load"
 | 
			
		||||
            app:icon="@drawable/ic_import" />
 | 
			
		||||
 | 
			
		||||
    </LinearLayout>
 | 
			
		||||
 | 
			
		||||
</androidx.constraintlayout.widget.ConstraintLayout>
 | 
			
		||||
@@ -0,0 +1,63 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:app="http://schemas.android.com/apk/res-auto"
 | 
			
		||||
    xmlns:tools="http://schemas.android.com/tools"
 | 
			
		||||
    android:id="@+id/setting_body"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="wrap_content"
 | 
			
		||||
    android:background="?android:attr/selectableItemBackground"
 | 
			
		||||
    android:clickable="true"
 | 
			
		||||
    android:focusable="true"
 | 
			
		||||
    android:gravity="center_vertical"
 | 
			
		||||
    android:minHeight="72dp"
 | 
			
		||||
    android:padding="16dp"
 | 
			
		||||
    android:nextFocusRight="@id/button_options">
 | 
			
		||||
 | 
			
		||||
    <LinearLayout
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:gravity="center_vertical"
 | 
			
		||||
        android:orientation="horizontal">
 | 
			
		||||
 | 
			
		||||
        <LinearLayout
 | 
			
		||||
            android:layout_width="0dp"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:orientation="vertical"
 | 
			
		||||
            android:layout_weight="1">
 | 
			
		||||
 | 
			
		||||
            <com.google.android.material.textview.MaterialTextView
 | 
			
		||||
                android:id="@+id/text_setting_name"
 | 
			
		||||
                style="@style/TextAppearance.Material3.HeadlineMedium"
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:textAlignment="viewStart"
 | 
			
		||||
                android:textSize="17sp"
 | 
			
		||||
                app:lineHeight="22dp"
 | 
			
		||||
                tools:text="Setting Name" />
 | 
			
		||||
 | 
			
		||||
            <com.google.android.material.textview.MaterialTextView
 | 
			
		||||
                android:id="@+id/text_setting_value"
 | 
			
		||||
                style="@style/TextAppearance.Material3.LabelMedium"
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_marginTop="@dimen/spacing_small"
 | 
			
		||||
                android:textAlignment="viewStart"
 | 
			
		||||
                android:textStyle="bold"
 | 
			
		||||
                android:textSize="13sp"
 | 
			
		||||
                tools:text="1x" />
 | 
			
		||||
 | 
			
		||||
        </LinearLayout>
 | 
			
		||||
 | 
			
		||||
        <Button
 | 
			
		||||
            android:id="@+id/button_options"
 | 
			
		||||
            style="?attr/materialIconButtonStyle"
 | 
			
		||||
            android:layout_width="wrap_content"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:nextFocusLeft="@id/setting_body"
 | 
			
		||||
            app:icon="@drawable/ic_more_vert"
 | 
			
		||||
            app:iconSize="24dp"
 | 
			
		||||
            app:iconTint="?attr/colorOnSurface" />
 | 
			
		||||
 | 
			
		||||
    </LinearLayout>
 | 
			
		||||
 | 
			
		||||
</RelativeLayout>
 | 
			
		||||
@@ -17,8 +17,13 @@
 | 
			
		||||
        android:title="@string/per_game_settings" />
 | 
			
		||||
 | 
			
		||||
    <item
 | 
			
		||||
        android:id="@+id/menu_overlay_controls"
 | 
			
		||||
        android:id="@+id/menu_controls"
 | 
			
		||||
        android:icon="@drawable/ic_controller"
 | 
			
		||||
        android:title="@string/preferences_controls" />
 | 
			
		||||
 | 
			
		||||
    <item
 | 
			
		||||
        android:id="@+id/menu_overlay_controls"
 | 
			
		||||
        android:icon="@drawable/ic_overlay"
 | 
			
		||||
        android:title="@string/emulation_input_overlay" />
 | 
			
		||||
 | 
			
		||||
    <item
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										34
									
								
								src/android/app/src/main/res/menu/menu_input_options.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/android/app/src/main/res/menu/menu_input_options.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
 | 
			
		||||
    <item
 | 
			
		||||
        android:id="@+id/invert_axis"
 | 
			
		||||
        android:title="@string/invert_axis"
 | 
			
		||||
        android:visible="false" />
 | 
			
		||||
 | 
			
		||||
    <item
 | 
			
		||||
        android:id="@+id/invert_button"
 | 
			
		||||
        android:title="@string/invert_button"
 | 
			
		||||
        android:visible="false" />
 | 
			
		||||
 | 
			
		||||
    <item
 | 
			
		||||
        android:id="@+id/toggle_button"
 | 
			
		||||
        android:title="@string/toggle_button"
 | 
			
		||||
        android:visible="false" />
 | 
			
		||||
 | 
			
		||||
    <item
 | 
			
		||||
        android:id="@+id/turbo_button"
 | 
			
		||||
        android:title="@string/turbo_button"
 | 
			
		||||
        android:visible="false" />
 | 
			
		||||
 | 
			
		||||
    <item
 | 
			
		||||
        android:id="@+id/set_threshold"
 | 
			
		||||
        android:title="@string/set_threshold"
 | 
			
		||||
        android:visible="false" />
 | 
			
		||||
 | 
			
		||||
    <item
 | 
			
		||||
        android:id="@+id/toggle_axis"
 | 
			
		||||
        android:title="@string/toggle_axis"
 | 
			
		||||
        android:visible="false" />
 | 
			
		||||
 | 
			
		||||
</menu>
 | 
			
		||||
@@ -26,7 +26,7 @@
 | 
			
		||||
 | 
			
		||||
    <fragment
 | 
			
		||||
        android:id="@+id/settingsSearchFragment"
 | 
			
		||||
        android:name="org.yuzu.yuzu_emu.fragments.SettingsSearchFragment"
 | 
			
		||||
        android:name="org.yuzu.yuzu_emu.features.settings.ui.SettingsSearchFragment"
 | 
			
		||||
        android:label="SettingsSearchFragment" />
 | 
			
		||||
 | 
			
		||||
</navigation>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,4 +2,6 @@
 | 
			
		||||
<resources>
 | 
			
		||||
    <dimen name="spacing_navigation">0dp</dimen>
 | 
			
		||||
    <dimen name="spacing_navigation_rail">80dp</dimen>
 | 
			
		||||
 | 
			
		||||
    <dimen name="mapping_anim_size">100dp</dimen>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -18,4 +18,6 @@
 | 
			
		||||
 | 
			
		||||
    <dimen name="dialog_margin">20dp</dimen>
 | 
			
		||||
    <dimen name="elevated_app_bar">3dp</dimen>
 | 
			
		||||
 | 
			
		||||
    <dimen name="mapping_anim_size">75dp</dimen>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -255,6 +255,92 @@
 | 
			
		||||
    <string name="audio_volume">Volume</string>
 | 
			
		||||
    <string name="audio_volume_description">Specifies the volume of audio output.</string>
 | 
			
		||||
 | 
			
		||||
    <!-- Input strings -->
 | 
			
		||||
    <string name="buttons">Buttons</string>
 | 
			
		||||
    <string name="button_a">A</string>
 | 
			
		||||
    <string name="button_b">B</string>
 | 
			
		||||
    <string name="button_x">X</string>
 | 
			
		||||
    <string name="button_y">Y</string>
 | 
			
		||||
    <string name="button_plus">Plus</string>
 | 
			
		||||
    <string name="button_minus">Minus</string>
 | 
			
		||||
    <string name="button_home">Home</string>
 | 
			
		||||
    <string name="button_capture">Capture</string>
 | 
			
		||||
    <string name="start_pause">Start/Pause</string>
 | 
			
		||||
    <string name="dpad">D-Pad</string>
 | 
			
		||||
    <string name="up">Up</string>
 | 
			
		||||
    <string name="down">Down</string>
 | 
			
		||||
    <string name="left">Left</string>
 | 
			
		||||
    <string name="right">Right</string>
 | 
			
		||||
    <string name="left_stick">Left stick</string>
 | 
			
		||||
    <string name="control_stick">Control stick</string>
 | 
			
		||||
    <string name="right_stick">Right stick</string>
 | 
			
		||||
    <string name="c_stick">C-Stick</string>
 | 
			
		||||
    <string name="pressed">Pressed</string>
 | 
			
		||||
    <string name="range">Range</string>
 | 
			
		||||
    <string name="deadzone">Deadzone</string>
 | 
			
		||||
    <string name="modifier">Modifier</string>
 | 
			
		||||
    <string name="modifier_range">Modifier range</string>
 | 
			
		||||
    <string name="triggers">Triggers</string>
 | 
			
		||||
    <string name="button_l">L</string>
 | 
			
		||||
    <string name="button_r">R</string>
 | 
			
		||||
    <string name="button_zl">ZL</string>
 | 
			
		||||
    <string name="button_zr">ZR</string>
 | 
			
		||||
    <string name="button_sl_left">Left SL</string>
 | 
			
		||||
    <string name="button_sr_left">Left SR</string>
 | 
			
		||||
    <string name="button_sl_right">Right SL</string>
 | 
			
		||||
    <string name="button_sr_right">Right SR</string>
 | 
			
		||||
    <string name="button_z">Z</string>
 | 
			
		||||
    <string name="invalid">Invalid</string>
 | 
			
		||||
    <string name="not_set">Not set</string>
 | 
			
		||||
    <string name="unknown">Unknown</string>
 | 
			
		||||
    <string name="qualified_hat">%1$s%2$s%3$sHat %4$s</string>
 | 
			
		||||
    <string name="qualified_button_stick_axis">%1$s%2$s%3$sAxis %4$s</string>
 | 
			
		||||
    <string name="qualified_button">%1$s%2$s%3$sButton %4$s</string>
 | 
			
		||||
    <string name="qualified_axis">Axis %1$s%2$s</string>
 | 
			
		||||
    <string name="unused">Unused</string>
 | 
			
		||||
    <string name="input_prompt">Move or press an input</string>
 | 
			
		||||
    <string name="unsupported_input">Unsupported input type</string>
 | 
			
		||||
    <string name="input_mapping_filter">Input mapping filter</string>
 | 
			
		||||
    <string name="input_mapping_filter_description">Select a device to filter mapping inputs</string>
 | 
			
		||||
    <string name="auto_map">Auto-map a controller</string>
 | 
			
		||||
    <string name="auto_map_description">Select a device to attempt auto-mapping</string>
 | 
			
		||||
    <string name="attempted_auto_map">Attempted auto-map with %1$s</string>
 | 
			
		||||
    <string name="controller_type">Controller type</string>
 | 
			
		||||
    <string name="pro_controller">Pro Controller</string>
 | 
			
		||||
    <string name="handheld">Handheld</string>
 | 
			
		||||
    <string name="dual_joycons">Dual Joycons</string>
 | 
			
		||||
    <string name="left_joycon">Left Joycon</string>
 | 
			
		||||
    <string name="right_joycon">Right Joycon</string>
 | 
			
		||||
    <string name="gamecube_controller">GameCube Controller</string>
 | 
			
		||||
    <string name="invert_axis">Invert axis</string>
 | 
			
		||||
    <string name="invert_button">Invert button</string>
 | 
			
		||||
    <string name="toggle_button">Toggle button</string>
 | 
			
		||||
    <string name="turbo_button">Turbo button</string>
 | 
			
		||||
    <string name="set_threshold">Set threshold</string>
 | 
			
		||||
    <string name="toggle_axis">Toggle axis</string>
 | 
			
		||||
    <string name="connected">Connected</string>
 | 
			
		||||
    <string name="use_system_vibrator">Use system vibrator</string>
 | 
			
		||||
    <string name="input_overlay">Input overlay</string>
 | 
			
		||||
    <string name="vibration">Vibration</string>
 | 
			
		||||
    <string name="vibration_strength">Vibration strength</string>
 | 
			
		||||
    <string name="profile">Profile</string>
 | 
			
		||||
    <string name="create_new_profile">Create new profile</string>
 | 
			
		||||
    <string name="enter_profile_name">Enter profile name</string>
 | 
			
		||||
    <string name="profile_name_already_exists">Profile name already exists</string>
 | 
			
		||||
    <string name="invalid_profile_name">Invalid profile name</string>
 | 
			
		||||
    <string name="use_global_input_configuration">Use global input configuration</string>
 | 
			
		||||
    <string name="player_num_profile">Player %d profile</string>
 | 
			
		||||
    <string name="delete_input_profile">Delete input profile</string>
 | 
			
		||||
    <string name="delete_input_profile_description">Are you sure that you want to delete this profile? This is not recoverable.</string>
 | 
			
		||||
    <string name="stick_map_description">Move a stick left and then up or press a button</string>
 | 
			
		||||
    <string name="button_map_description">Press a button or move a trigger/stick</string>
 | 
			
		||||
    <string name="map_dpad_direction">Map to D-Pad %1$s</string>
 | 
			
		||||
    <string name="map_control">Map to %1$s</string>
 | 
			
		||||
    <string name="failed_to_load_profile">Failed to load profile</string>
 | 
			
		||||
    <string name="failed_to_save_profile">Failed to save profile</string>
 | 
			
		||||
    <string name="reset_mapping">Reset mappings</string>
 | 
			
		||||
    <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>
 | 
			
		||||
 | 
			
		||||
    <!-- Miscellaneous -->
 | 
			
		||||
    <string name="slider_default">Default</string>
 | 
			
		||||
    <string name="ini_saved">Saved settings</string>
 | 
			
		||||
@@ -292,6 +378,10 @@
 | 
			
		||||
    <string name="more_options">More options</string>
 | 
			
		||||
    <string name="use_global_setting">Use global setting</string>
 | 
			
		||||
    <string name="operation_completed_successfully">The operation completed successfully</string>
 | 
			
		||||
    <string name="retry">Retry</string>
 | 
			
		||||
    <string name="confirm">Confirm</string>
 | 
			
		||||
    <string name="load">Load</string>
 | 
			
		||||
    <string name="save">Save</string>
 | 
			
		||||
 | 
			
		||||
    <!-- GPU driver installation -->
 | 
			
		||||
    <string name="select_gpu_driver">Select GPU driver</string>
 | 
			
		||||
@@ -313,6 +403,9 @@
 | 
			
		||||
    <string name="preferences_graphics_description">Accuracy level, resolution, shader cache</string>
 | 
			
		||||
    <string name="preferences_audio">Audio</string>
 | 
			
		||||
    <string name="preferences_audio_description">Output engine, volume</string>
 | 
			
		||||
    <string name="preferences_controls">Controls</string>
 | 
			
		||||
    <string name="preferences_controls_description">Map controller input</string>
 | 
			
		||||
    <string name="preferences_player">Player %d</string>
 | 
			
		||||
    <string name="preferences_theme">Theme and color</string>
 | 
			
		||||
    <string name="preferences_debug">Debug</string>
 | 
			
		||||
    <string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string>
 | 
			
		||||
 
 | 
			
		||||
@@ -65,6 +65,30 @@ static jclass s_boolean_class;
 | 
			
		||||
static jmethodID s_boolean_constructor;
 | 
			
		||||
static jfieldID s_boolean_value_field;
 | 
			
		||||
 | 
			
		||||
static jclass s_player_input_class;
 | 
			
		||||
static jmethodID s_player_input_constructor;
 | 
			
		||||
static jfieldID s_player_input_connected_field;
 | 
			
		||||
static jfieldID s_player_input_buttons_field;
 | 
			
		||||
static jfieldID s_player_input_analogs_field;
 | 
			
		||||
static jfieldID s_player_input_motions_field;
 | 
			
		||||
static jfieldID s_player_input_vibration_enabled_field;
 | 
			
		||||
static jfieldID s_player_input_vibration_strength_field;
 | 
			
		||||
static jfieldID s_player_input_body_color_left_field;
 | 
			
		||||
static jfieldID s_player_input_body_color_right_field;
 | 
			
		||||
static jfieldID s_player_input_button_color_left_field;
 | 
			
		||||
static jfieldID s_player_input_button_color_right_field;
 | 
			
		||||
static jfieldID s_player_input_profile_name_field;
 | 
			
		||||
static jfieldID s_player_input_use_system_vibrator_field;
 | 
			
		||||
 | 
			
		||||
static jclass s_yuzu_input_device_interface;
 | 
			
		||||
static jmethodID s_yuzu_input_device_get_name;
 | 
			
		||||
static jmethodID s_yuzu_input_device_get_guid;
 | 
			
		||||
static jmethodID s_yuzu_input_device_get_port;
 | 
			
		||||
static jmethodID s_yuzu_input_device_get_supports_vibration;
 | 
			
		||||
static jmethodID s_yuzu_input_device_vibrate;
 | 
			
		||||
static jmethodID s_yuzu_input_device_get_axes;
 | 
			
		||||
static jmethodID s_yuzu_input_device_has_keys;
 | 
			
		||||
 | 
			
		||||
static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
 | 
			
		||||
 | 
			
		||||
namespace Common::Android {
 | 
			
		||||
@@ -276,6 +300,94 @@ jfieldID GetBooleanValueField() {
 | 
			
		||||
    return s_boolean_value_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jclass GetPlayerInputClass() {
 | 
			
		||||
    return s_player_input_class;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jmethodID GetPlayerInputConstructor() {
 | 
			
		||||
    return s_player_input_constructor;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetPlayerInputConnectedField() {
 | 
			
		||||
    return s_player_input_connected_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetPlayerInputButtonsField() {
 | 
			
		||||
    return s_player_input_buttons_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetPlayerInputAnalogsField() {
 | 
			
		||||
    return s_player_input_analogs_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetPlayerInputMotionsField() {
 | 
			
		||||
    return s_player_input_motions_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetPlayerInputVibrationEnabledField() {
 | 
			
		||||
    return s_player_input_vibration_enabled_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetPlayerInputVibrationStrengthField() {
 | 
			
		||||
    return s_player_input_vibration_strength_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetPlayerInputBodyColorLeftField() {
 | 
			
		||||
    return s_player_input_body_color_left_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetPlayerInputBodyColorRightField() {
 | 
			
		||||
    return s_player_input_body_color_right_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetPlayerInputButtonColorLeftField() {
 | 
			
		||||
    return s_player_input_button_color_left_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetPlayerInputButtonColorRightField() {
 | 
			
		||||
    return s_player_input_button_color_right_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetPlayerInputProfileNameField() {
 | 
			
		||||
    return s_player_input_profile_name_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetPlayerInputUseSystemVibratorField() {
 | 
			
		||||
    return s_player_input_use_system_vibrator_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jclass GetYuzuInputDeviceInterface() {
 | 
			
		||||
    return s_yuzu_input_device_interface;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jmethodID GetYuzuDeviceGetName() {
 | 
			
		||||
    return s_yuzu_input_device_get_name;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jmethodID GetYuzuDeviceGetGUID() {
 | 
			
		||||
    return s_yuzu_input_device_get_guid;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jmethodID GetYuzuDeviceGetPort() {
 | 
			
		||||
    return s_yuzu_input_device_get_port;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jmethodID GetYuzuDeviceGetSupportsVibration() {
 | 
			
		||||
    return s_yuzu_input_device_get_supports_vibration;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jmethodID GetYuzuDeviceVibrate() {
 | 
			
		||||
    return s_yuzu_input_device_vibrate;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jmethodID GetYuzuDeviceGetAxes() {
 | 
			
		||||
    return s_yuzu_input_device_get_axes;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jmethodID GetYuzuDeviceHasKeys() {
 | 
			
		||||
    return s_yuzu_input_device_has_keys;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef __cplusplus
 | 
			
		||||
extern "C" {
 | 
			
		||||
#endif
 | 
			
		||||
@@ -387,6 +499,55 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
 | 
			
		||||
    s_boolean_value_field = env->GetFieldID(boolean_class, "value", "Z");
 | 
			
		||||
    env->DeleteLocalRef(boolean_class);
 | 
			
		||||
 | 
			
		||||
    const jclass player_input_class =
 | 
			
		||||
        env->FindClass("org/yuzu/yuzu_emu/features/input/model/PlayerInput");
 | 
			
		||||
    s_player_input_class = reinterpret_cast<jclass>(env->NewGlobalRef(player_input_class));
 | 
			
		||||
    s_player_input_constructor = env->GetMethodID(
 | 
			
		||||
        player_input_class, "<init>",
 | 
			
		||||
        "(Z[Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;ZIJJJJLjava/lang/String;Z)V");
 | 
			
		||||
    s_player_input_connected_field = env->GetFieldID(player_input_class, "connected", "Z");
 | 
			
		||||
    s_player_input_buttons_field =
 | 
			
		||||
        env->GetFieldID(player_input_class, "buttons", "[Ljava/lang/String;");
 | 
			
		||||
    s_player_input_analogs_field =
 | 
			
		||||
        env->GetFieldID(player_input_class, "analogs", "[Ljava/lang/String;");
 | 
			
		||||
    s_player_input_motions_field =
 | 
			
		||||
        env->GetFieldID(player_input_class, "motions", "[Ljava/lang/String;");
 | 
			
		||||
    s_player_input_vibration_enabled_field =
 | 
			
		||||
        env->GetFieldID(player_input_class, "vibrationEnabled", "Z");
 | 
			
		||||
    s_player_input_vibration_strength_field =
 | 
			
		||||
        env->GetFieldID(player_input_class, "vibrationStrength", "I");
 | 
			
		||||
    s_player_input_body_color_left_field =
 | 
			
		||||
        env->GetFieldID(player_input_class, "bodyColorLeft", "J");
 | 
			
		||||
    s_player_input_body_color_right_field =
 | 
			
		||||
        env->GetFieldID(player_input_class, "bodyColorRight", "J");
 | 
			
		||||
    s_player_input_button_color_left_field =
 | 
			
		||||
        env->GetFieldID(player_input_class, "buttonColorLeft", "J");
 | 
			
		||||
    s_player_input_button_color_right_field =
 | 
			
		||||
        env->GetFieldID(player_input_class, "buttonColorRight", "J");
 | 
			
		||||
    s_player_input_profile_name_field =
 | 
			
		||||
        env->GetFieldID(player_input_class, "profileName", "Ljava/lang/String;");
 | 
			
		||||
    s_player_input_use_system_vibrator_field =
 | 
			
		||||
        env->GetFieldID(player_input_class, "useSystemVibrator", "Z");
 | 
			
		||||
    env->DeleteLocalRef(player_input_class);
 | 
			
		||||
 | 
			
		||||
    const jclass yuzu_input_device_interface =
 | 
			
		||||
        env->FindClass("org/yuzu/yuzu_emu/features/input/YuzuInputDevice");
 | 
			
		||||
    s_yuzu_input_device_interface =
 | 
			
		||||
        reinterpret_cast<jclass>(env->NewGlobalRef(yuzu_input_device_interface));
 | 
			
		||||
    s_yuzu_input_device_get_name =
 | 
			
		||||
        env->GetMethodID(yuzu_input_device_interface, "getName", "()Ljava/lang/String;");
 | 
			
		||||
    s_yuzu_input_device_get_guid =
 | 
			
		||||
        env->GetMethodID(yuzu_input_device_interface, "getGUID", "()Ljava/lang/String;");
 | 
			
		||||
    s_yuzu_input_device_get_port = env->GetMethodID(yuzu_input_device_interface, "getPort", "()I");
 | 
			
		||||
    s_yuzu_input_device_get_supports_vibration =
 | 
			
		||||
        env->GetMethodID(yuzu_input_device_interface, "getSupportsVibration", "()Z");
 | 
			
		||||
    s_yuzu_input_device_vibrate = env->GetMethodID(yuzu_input_device_interface, "vibrate", "(F)V");
 | 
			
		||||
    s_yuzu_input_device_get_axes =
 | 
			
		||||
        env->GetMethodID(yuzu_input_device_interface, "getAxes", "()[Ljava/lang/Integer;");
 | 
			
		||||
    s_yuzu_input_device_has_keys =
 | 
			
		||||
        env->GetMethodID(yuzu_input_device_interface, "hasKeys", "([I)[Z");
 | 
			
		||||
    env->DeleteLocalRef(yuzu_input_device_interface);
 | 
			
		||||
 | 
			
		||||
    // Initialize Android Storage
 | 
			
		||||
    Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
 | 
			
		||||
 | 
			
		||||
@@ -416,6 +577,8 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
 | 
			
		||||
    env->DeleteGlobalRef(s_double_class);
 | 
			
		||||
    env->DeleteGlobalRef(s_integer_class);
 | 
			
		||||
    env->DeleteGlobalRef(s_boolean_class);
 | 
			
		||||
    env->DeleteGlobalRef(s_player_input_class);
 | 
			
		||||
    env->DeleteGlobalRef(s_yuzu_input_device_interface);
 | 
			
		||||
 | 
			
		||||
    // UnInitialize applets
 | 
			
		||||
    SoftwareKeyboard::CleanupJNI(env);
 | 
			
		||||
 
 | 
			
		||||
@@ -85,4 +85,28 @@ jclass GetBooleanClass();
 | 
			
		||||
jmethodID GetBooleanConstructor();
 | 
			
		||||
jfieldID GetBooleanValueField();
 | 
			
		||||
 | 
			
		||||
jclass GetPlayerInputClass();
 | 
			
		||||
jmethodID GetPlayerInputConstructor();
 | 
			
		||||
jfieldID GetPlayerInputConnectedField();
 | 
			
		||||
jfieldID GetPlayerInputButtonsField();
 | 
			
		||||
jfieldID GetPlayerInputAnalogsField();
 | 
			
		||||
jfieldID GetPlayerInputMotionsField();
 | 
			
		||||
jfieldID GetPlayerInputVibrationEnabledField();
 | 
			
		||||
jfieldID GetPlayerInputVibrationStrengthField();
 | 
			
		||||
jfieldID GetPlayerInputBodyColorLeftField();
 | 
			
		||||
jfieldID GetPlayerInputBodyColorRightField();
 | 
			
		||||
jfieldID GetPlayerInputButtonColorLeftField();
 | 
			
		||||
jfieldID GetPlayerInputButtonColorRightField();
 | 
			
		||||
jfieldID GetPlayerInputProfileNameField();
 | 
			
		||||
jfieldID GetPlayerInputUseSystemVibratorField();
 | 
			
		||||
 | 
			
		||||
jclass GetYuzuInputDeviceInterface();
 | 
			
		||||
jmethodID GetYuzuDeviceGetName();
 | 
			
		||||
jmethodID GetYuzuDeviceGetGUID();
 | 
			
		||||
jmethodID GetYuzuDeviceGetPort();
 | 
			
		||||
jmethodID GetYuzuDeviceGetSupportsVibration();
 | 
			
		||||
jmethodID GetYuzuDeviceVibrate();
 | 
			
		||||
jmethodID GetYuzuDeviceGetAxes();
 | 
			
		||||
jmethodID GetYuzuDeviceHasKeys();
 | 
			
		||||
 | 
			
		||||
} // namespace Common::Android
 | 
			
		||||
 
 | 
			
		||||
@@ -395,6 +395,10 @@ struct PlayerInput {
 | 
			
		||||
    u32 button_color_left;
 | 
			
		||||
    u32 button_color_right;
 | 
			
		||||
    std::string profile_name;
 | 
			
		||||
 | 
			
		||||
    // This is meant to tell the Android frontend whether to use a device's built-in vibration
 | 
			
		||||
    // motor or a controller's vibrations.
 | 
			
		||||
    bool use_system_vibrator;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct TouchscreenInput {
 | 
			
		||||
 
 | 
			
		||||
@@ -138,6 +138,7 @@ void Config::ReadPlayerValues(const std::size_t player_index) {
 | 
			
		||||
        if (profile_name.empty()) {
 | 
			
		||||
            // Use the global input config
 | 
			
		||||
            player = Settings::values.players.GetValue(true)[player_index];
 | 
			
		||||
            player.profile_name = "";
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        player.profile_name = profile_name;
 | 
			
		||||
 
 | 
			
		||||
@@ -176,16 +176,19 @@ void EmulatedController::LoadDevices() {
 | 
			
		||||
        camera_params[1] = Common::ParamPackage{"engine:camera,camera:1"};
 | 
			
		||||
        ring_params[1] = Common::ParamPackage{"engine:joycon,axis_x:100,axis_y:101"};
 | 
			
		||||
        nfc_params[0] = Common::ParamPackage{"engine:virtual_amiibo,nfc:1"};
 | 
			
		||||
        android_params = Common::ParamPackage{"engine:android,port:100"};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    output_params[LeftIndex] = left_joycon;
 | 
			
		||||
    output_params[RightIndex] = right_joycon;
 | 
			
		||||
    output_params[2] = camera_params[1];
 | 
			
		||||
    output_params[3] = nfc_params[0];
 | 
			
		||||
    output_params[4] = android_params;
 | 
			
		||||
    output_params[LeftIndex].Set("output", true);
 | 
			
		||||
    output_params[RightIndex].Set("output", true);
 | 
			
		||||
    output_params[2].Set("output", true);
 | 
			
		||||
    output_params[3].Set("output", true);
 | 
			
		||||
    output_params[4].Set("output", true);
 | 
			
		||||
 | 
			
		||||
    LoadTASParams();
 | 
			
		||||
    LoadVirtualGamepadParams();
 | 
			
		||||
@@ -578,6 +581,9 @@ void EmulatedController::DisableConfiguration() {
 | 
			
		||||
 | 
			
		||||
    // Get Joycon colors before turning on the controller
 | 
			
		||||
    for (const auto& color_device : color_devices) {
 | 
			
		||||
        if (color_device == nullptr) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
        color_device->ForceUpdate();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -1277,6 +1283,10 @@ bool EmulatedController::SetVibration(DeviceIndex device_index, const VibrationV
 | 
			
		||||
        .high_frequency = vibration.high_frequency,
 | 
			
		||||
        .type = type,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Send vibrations to Android's input overlay
 | 
			
		||||
    output_devices[4]->SetVibration(status);
 | 
			
		||||
 | 
			
		||||
    return output_devices[index]->SetVibration(status) == Common::Input::DriverResult::Success;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@
 | 
			
		||||
 | 
			
		||||
namespace Core::HID {
 | 
			
		||||
const std::size_t max_emulated_controllers = 2;
 | 
			
		||||
const std::size_t output_devices_size = 4;
 | 
			
		||||
const std::size_t output_devices_size = 5;
 | 
			
		||||
struct ControllerMotionInfo {
 | 
			
		||||
    Common::Input::MotionStatus raw_status{};
 | 
			
		||||
    MotionInput emulated{};
 | 
			
		||||
@@ -597,6 +597,7 @@ private:
 | 
			
		||||
    CameraParams camera_params;
 | 
			
		||||
    RingAnalogParams ring_params;
 | 
			
		||||
    NfcParams nfc_params;
 | 
			
		||||
    Common::ParamPackage android_params;
 | 
			
		||||
    OutputParams output_params;
 | 
			
		||||
 | 
			
		||||
    ButtonDevices button_devices;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,6 @@
 | 
			
		||||
# SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
add_library(input_common STATIC
 | 
			
		||||
    drivers/android.cpp
 | 
			
		||||
    drivers/android.h
 | 
			
		||||
    drivers/camera.cpp
 | 
			
		||||
    drivers/camera.h
 | 
			
		||||
    drivers/keyboard.cpp
 | 
			
		||||
@@ -94,3 +92,11 @@ target_link_libraries(input_common PUBLIC hid_core PRIVATE common Boost::headers
 | 
			
		||||
if (YUZU_USE_PRECOMPILED_HEADERS)
 | 
			
		||||
    target_precompile_headers(input_common PRIVATE precompiled_headers.h)
 | 
			
		||||
endif()
 | 
			
		||||
 | 
			
		||||
if (ANDROID)
 | 
			
		||||
    target_sources(input_common PRIVATE
 | 
			
		||||
        drivers/android.cpp
 | 
			
		||||
        drivers/android.h
 | 
			
		||||
    )
 | 
			
		||||
    target_link_libraries(input_common PRIVATE android)
 | 
			
		||||
endif()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,30 +1,47 @@
 | 
			
		||||
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
#include <set>
 | 
			
		||||
#include <common/settings_input.h>
 | 
			
		||||
#include <jni.h>
 | 
			
		||||
#include "common/android/android_common.h"
 | 
			
		||||
#include "common/android/id_cache.h"
 | 
			
		||||
#include "input_common/drivers/android.h"
 | 
			
		||||
 | 
			
		||||
namespace InputCommon {
 | 
			
		||||
 | 
			
		||||
Android::Android(std::string input_engine_) : InputEngine(std::move(input_engine_)) {}
 | 
			
		||||
 | 
			
		||||
void Android::RegisterController(std::size_t controller_number) {
 | 
			
		||||
    PreSetController(GetIdentifier(controller_number));
 | 
			
		||||
void Android::RegisterController(jobject j_input_device) {
 | 
			
		||||
    auto env = Common::Android::GetEnvForThread();
 | 
			
		||||
    const std::string guid = Common::Android::GetJString(
 | 
			
		||||
        env, static_cast<jstring>(
 | 
			
		||||
                 env->CallObjectMethod(j_input_device, Common::Android::GetYuzuDeviceGetGUID())));
 | 
			
		||||
    const s32 port = env->CallIntMethod(j_input_device, Common::Android::GetYuzuDeviceGetPort());
 | 
			
		||||
    const auto identifier = GetIdentifier(guid, static_cast<size_t>(port));
 | 
			
		||||
    PreSetController(identifier);
 | 
			
		||||
 | 
			
		||||
    if (input_devices.find(identifier) != input_devices.end()) {
 | 
			
		||||
        env->DeleteGlobalRef(input_devices[identifier]);
 | 
			
		||||
    }
 | 
			
		||||
    auto new_device = env->NewGlobalRef(j_input_device);
 | 
			
		||||
    input_devices[identifier] = new_device;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Android::SetButtonState(std::size_t controller_number, int button_id, bool value) {
 | 
			
		||||
    const auto identifier = GetIdentifier(controller_number);
 | 
			
		||||
void Android::SetButtonState(std::string guid, size_t port, int button_id, bool value) {
 | 
			
		||||
    const auto identifier = GetIdentifier(guid, port);
 | 
			
		||||
    SetButton(identifier, button_id, value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Android::SetAxisState(std::size_t controller_number, int axis_id, float value) {
 | 
			
		||||
    const auto identifier = GetIdentifier(controller_number);
 | 
			
		||||
void Android::SetAxisPosition(std::string guid, size_t port, int axis_id, float value) {
 | 
			
		||||
    const auto identifier = GetIdentifier(guid, port);
 | 
			
		||||
    SetAxis(identifier, axis_id, value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Android::SetMotionState(std::size_t controller_number, u64 delta_timestamp, float gyro_x,
 | 
			
		||||
void Android::SetMotionState(std::string guid, size_t port, u64 delta_timestamp, float gyro_x,
 | 
			
		||||
                             float gyro_y, float gyro_z, float accel_x, float accel_y,
 | 
			
		||||
                             float accel_z) {
 | 
			
		||||
    const auto identifier = GetIdentifier(controller_number);
 | 
			
		||||
    const auto identifier = GetIdentifier(guid, port);
 | 
			
		||||
    const BasicMotion motion_data{
 | 
			
		||||
        .gyro_x = gyro_x,
 | 
			
		||||
        .gyro_y = gyro_y,
 | 
			
		||||
@@ -37,10 +54,295 @@ void Android::SetMotionState(std::size_t controller_number, u64 delta_timestamp,
 | 
			
		||||
    SetMotion(identifier, 0, motion_data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PadIdentifier Android::GetIdentifier(std::size_t controller_number) const {
 | 
			
		||||
Common::Input::DriverResult Android::SetVibration(
 | 
			
		||||
    [[maybe_unused]] const PadIdentifier& identifier,
 | 
			
		||||
    [[maybe_unused]] const Common::Input::VibrationStatus& vibration) {
 | 
			
		||||
    auto device = input_devices.find(identifier);
 | 
			
		||||
    if (device != input_devices.end()) {
 | 
			
		||||
        Common::Android::RunJNIOnFiber<void>([&](JNIEnv* env) {
 | 
			
		||||
            float average_intensity =
 | 
			
		||||
                static_cast<float>((vibration.high_amplitude + vibration.low_amplitude) / 2.0);
 | 
			
		||||
            env->CallVoidMethod(device->second, Common::Android::GetYuzuDeviceVibrate(),
 | 
			
		||||
                                average_intensity);
 | 
			
		||||
        });
 | 
			
		||||
        return Common::Input::DriverResult::Success;
 | 
			
		||||
    }
 | 
			
		||||
    return Common::Input::DriverResult::NotSupported;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Android::IsVibrationEnabled([[maybe_unused]] const PadIdentifier& identifier) {
 | 
			
		||||
    auto device = input_devices.find(identifier);
 | 
			
		||||
    if (device != input_devices.end()) {
 | 
			
		||||
        return Common::Android::RunJNIOnFiber<bool>([&](JNIEnv* env) {
 | 
			
		||||
            return static_cast<bool>(env->CallBooleanMethod(
 | 
			
		||||
                device->second, Common::Android::GetYuzuDeviceGetSupportsVibration()));
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::vector<Common::ParamPackage> Android::GetInputDevices() const {
 | 
			
		||||
    std::vector<Common::ParamPackage> devices;
 | 
			
		||||
    auto env = Common::Android::GetEnvForThread();
 | 
			
		||||
    for (const auto& [key, value] : input_devices) {
 | 
			
		||||
        auto name_object = static_cast<jstring>(
 | 
			
		||||
            env->CallObjectMethod(value, Common::Android::GetYuzuDeviceGetName()));
 | 
			
		||||
        const std::string name =
 | 
			
		||||
            fmt::format("{} {}", Common::Android::GetJString(env, name_object), key.port);
 | 
			
		||||
        devices.emplace_back(Common::ParamPackage{
 | 
			
		||||
            {"engine", GetEngineName()},
 | 
			
		||||
            {"display", std::move(name)},
 | 
			
		||||
            {"guid", key.guid.RawString()},
 | 
			
		||||
            {"port", std::to_string(key.port)},
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    return devices;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::set<s32> Android::GetDeviceAxes(JNIEnv* env, jobject& j_device) const {
 | 
			
		||||
    auto j_axes = static_cast<jobjectArray>(
 | 
			
		||||
        env->CallObjectMethod(j_device, Common::Android::GetYuzuDeviceGetAxes()));
 | 
			
		||||
    std::set<s32> axes;
 | 
			
		||||
    for (int i = 0; i < env->GetArrayLength(j_axes); ++i) {
 | 
			
		||||
        jobject axis = env->GetObjectArrayElement(j_axes, i);
 | 
			
		||||
        axes.insert(env->GetIntField(axis, Common::Android::GetIntegerValueField()));
 | 
			
		||||
    }
 | 
			
		||||
    return axes;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Common::ParamPackage Android::BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x,
 | 
			
		||||
                                                         int axis_y) const {
 | 
			
		||||
    Common::ParamPackage params;
 | 
			
		||||
    params.Set("engine", GetEngineName());
 | 
			
		||||
    params.Set("port", static_cast<int>(identifier.port));
 | 
			
		||||
    params.Set("guid", identifier.guid.RawString());
 | 
			
		||||
    params.Set("axis_x", axis_x);
 | 
			
		||||
    params.Set("axis_y", axis_y);
 | 
			
		||||
    params.Set("offset_x", 0);
 | 
			
		||||
    params.Set("offset_y", 0);
 | 
			
		||||
    params.Set("invert_x", "+");
 | 
			
		||||
 | 
			
		||||
    // Invert Y-Axis by default
 | 
			
		||||
    params.Set("invert_y", "-");
 | 
			
		||||
    return params;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Common::ParamPackage Android::BuildAnalogParamPackageForButton(PadIdentifier identifier, s32 axis,
 | 
			
		||||
                                                               bool invert) const {
 | 
			
		||||
    Common::ParamPackage params{};
 | 
			
		||||
    params.Set("engine", GetEngineName());
 | 
			
		||||
    params.Set("port", static_cast<int>(identifier.port));
 | 
			
		||||
    params.Set("guid", identifier.guid.RawString());
 | 
			
		||||
    params.Set("axis", axis);
 | 
			
		||||
    params.Set("threshold", "0.5");
 | 
			
		||||
    params.Set("invert", invert ? "-" : "+");
 | 
			
		||||
    return params;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Common::ParamPackage Android::BuildButtonParamPackageForButton(PadIdentifier identifier,
 | 
			
		||||
                                                               s32 button) const {
 | 
			
		||||
    Common::ParamPackage params{};
 | 
			
		||||
    params.Set("engine", GetEngineName());
 | 
			
		||||
    params.Set("port", static_cast<int>(identifier.port));
 | 
			
		||||
    params.Set("guid", identifier.guid.RawString());
 | 
			
		||||
    params.Set("button", button);
 | 
			
		||||
    return params;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Android::MatchVID(Common::UUID device, const std::vector<std::string>& vids) const {
 | 
			
		||||
    for (size_t i = 0; i < vids.size(); ++i) {
 | 
			
		||||
        auto fucker = device.RawString();
 | 
			
		||||
        if (fucker.find(vids[i]) != std::string::npos) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AnalogMapping Android::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
 | 
			
		||||
    if (!params.Has("guid") || !params.Has("port")) {
 | 
			
		||||
        return {};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    auto identifier =
 | 
			
		||||
        GetIdentifier(params.Get("guid", ""), static_cast<size_t>(params.Get("port", 0)));
 | 
			
		||||
    auto& j_device = input_devices[identifier];
 | 
			
		||||
    if (j_device == nullptr) {
 | 
			
		||||
        return {};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    auto env = Common::Android::GetEnvForThread();
 | 
			
		||||
    std::set<s32> axes = GetDeviceAxes(env, j_device);
 | 
			
		||||
    if (axes.size() == 0) {
 | 
			
		||||
        return {};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    AnalogMapping mapping = {};
 | 
			
		||||
    if (axes.find(AXIS_X) != axes.end() && axes.find(AXIS_Y) != axes.end()) {
 | 
			
		||||
        mapping.insert_or_assign(Settings::NativeAnalog::LStick,
 | 
			
		||||
                                 BuildParamPackageForAnalog(identifier, AXIS_X, AXIS_Y));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (axes.find(AXIS_RX) != axes.end() && axes.find(AXIS_RY) != axes.end()) {
 | 
			
		||||
        mapping.insert_or_assign(Settings::NativeAnalog::RStick,
 | 
			
		||||
                                 BuildParamPackageForAnalog(identifier, AXIS_RX, AXIS_RY));
 | 
			
		||||
    } else if (axes.find(AXIS_Z) != axes.end() && axes.find(AXIS_RZ) != axes.end()) {
 | 
			
		||||
        mapping.insert_or_assign(Settings::NativeAnalog::RStick,
 | 
			
		||||
                                 BuildParamPackageForAnalog(identifier, AXIS_Z, AXIS_RZ));
 | 
			
		||||
    }
 | 
			
		||||
    return mapping;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ButtonMapping Android::GetButtonMappingForDevice(const Common::ParamPackage& params) {
 | 
			
		||||
    if (!params.Has("guid") || !params.Has("port")) {
 | 
			
		||||
        return {};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    auto identifier =
 | 
			
		||||
        GetIdentifier(params.Get("guid", ""), static_cast<size_t>(params.Get("port", 0)));
 | 
			
		||||
    auto& j_device = input_devices[identifier];
 | 
			
		||||
    if (j_device == nullptr) {
 | 
			
		||||
        return {};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    auto env = Common::Android::GetEnvForThread();
 | 
			
		||||
    jintArray j_keys = env->NewIntArray(static_cast<int>(keycode_ids.size()));
 | 
			
		||||
    env->SetIntArrayRegion(j_keys, 0, static_cast<int>(keycode_ids.size()), keycode_ids.data());
 | 
			
		||||
    auto j_has_keys_object = static_cast<jbooleanArray>(
 | 
			
		||||
        env->CallObjectMethod(j_device, Common::Android::GetYuzuDeviceHasKeys(), j_keys));
 | 
			
		||||
    jboolean isCopy = false;
 | 
			
		||||
    jboolean* j_has_keys = env->GetBooleanArrayElements(j_has_keys_object, &isCopy);
 | 
			
		||||
 | 
			
		||||
    std::set<s32> available_keys;
 | 
			
		||||
    for (size_t i = 0; i < keycode_ids.size(); ++i) {
 | 
			
		||||
        if (j_has_keys[i]) {
 | 
			
		||||
            available_keys.insert(keycode_ids[i]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Some devices use axes instead of buttons for certain controls so we need all the axes here
 | 
			
		||||
    std::set<s32> axes = GetDeviceAxes(env, j_device);
 | 
			
		||||
 | 
			
		||||
    ButtonMapping mapping = {};
 | 
			
		||||
    if (axes.find(AXIS_HAT_X) != axes.end() && axes.find(AXIS_HAT_Y) != axes.end()) {
 | 
			
		||||
        mapping.insert_or_assign(Settings::NativeButton::DUp,
 | 
			
		||||
                                 BuildAnalogParamPackageForButton(identifier, AXIS_HAT_Y, true));
 | 
			
		||||
        mapping.insert_or_assign(Settings::NativeButton::DDown,
 | 
			
		||||
                                 BuildAnalogParamPackageForButton(identifier, AXIS_HAT_Y, false));
 | 
			
		||||
        mapping.insert_or_assign(Settings::NativeButton::DLeft,
 | 
			
		||||
                                 BuildAnalogParamPackageForButton(identifier, AXIS_HAT_X, true));
 | 
			
		||||
        mapping.insert_or_assign(Settings::NativeButton::DRight,
 | 
			
		||||
                                 BuildAnalogParamPackageForButton(identifier, AXIS_HAT_X, false));
 | 
			
		||||
    } else if (available_keys.find(KEYCODE_DPAD_UP) != available_keys.end() &&
 | 
			
		||||
               available_keys.find(KEYCODE_DPAD_DOWN) != available_keys.end() &&
 | 
			
		||||
               available_keys.find(KEYCODE_DPAD_LEFT) != available_keys.end() &&
 | 
			
		||||
               available_keys.find(KEYCODE_DPAD_RIGHT) != available_keys.end()) {
 | 
			
		||||
        mapping.insert_or_assign(Settings::NativeButton::DUp,
 | 
			
		||||
                                 BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_UP));
 | 
			
		||||
        mapping.insert_or_assign(Settings::NativeButton::DDown,
 | 
			
		||||
                                 BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_DOWN));
 | 
			
		||||
        mapping.insert_or_assign(Settings::NativeButton::DLeft,
 | 
			
		||||
                                 BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_LEFT));
 | 
			
		||||
        mapping.insert_or_assign(Settings::NativeButton::DRight,
 | 
			
		||||
                                 BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_RIGHT));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (axes.find(AXIS_LTRIGGER) != axes.end()) {
 | 
			
		||||
        mapping.insert_or_assign(Settings::NativeButton::ZL, BuildAnalogParamPackageForButton(
 | 
			
		||||
                                                                 identifier, AXIS_LTRIGGER, false));
 | 
			
		||||
    } else if (available_keys.find(KEYCODE_BUTTON_L2) != available_keys.end()) {
 | 
			
		||||
        mapping.insert_or_assign(Settings::NativeButton::ZL,
 | 
			
		||||
                                 BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_L2));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (axes.find(AXIS_RTRIGGER) != axes.end()) {
 | 
			
		||||
        mapping.insert_or_assign(Settings::NativeButton::ZR, BuildAnalogParamPackageForButton(
 | 
			
		||||
                                                                 identifier, AXIS_RTRIGGER, false));
 | 
			
		||||
    } else if (available_keys.find(KEYCODE_BUTTON_R2) != available_keys.end()) {
 | 
			
		||||
        mapping.insert_or_assign(Settings::NativeButton::ZR,
 | 
			
		||||
                                 BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_R2));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (available_keys.find(KEYCODE_BUTTON_A) != available_keys.end()) {
 | 
			
		||||
        if (MatchVID(identifier.guid, flipped_ab_vids)) {
 | 
			
		||||
            mapping.insert_or_assign(Settings::NativeButton::B, BuildButtonParamPackageForButton(
 | 
			
		||||
                                                                    identifier, KEYCODE_BUTTON_A));
 | 
			
		||||
        } else {
 | 
			
		||||
            mapping.insert_or_assign(Settings::NativeButton::A, BuildButtonParamPackageForButton(
 | 
			
		||||
                                                                    identifier, KEYCODE_BUTTON_A));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (available_keys.find(KEYCODE_BUTTON_B) != available_keys.end()) {
 | 
			
		||||
        if (MatchVID(identifier.guid, flipped_ab_vids)) {
 | 
			
		||||
            mapping.insert_or_assign(Settings::NativeButton::A, BuildButtonParamPackageForButton(
 | 
			
		||||
                                                                    identifier, KEYCODE_BUTTON_B));
 | 
			
		||||
        } else {
 | 
			
		||||
            mapping.insert_or_assign(Settings::NativeButton::B, BuildButtonParamPackageForButton(
 | 
			
		||||
                                                                    identifier, KEYCODE_BUTTON_B));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (available_keys.find(KEYCODE_BUTTON_X) != available_keys.end()) {
 | 
			
		||||
        if (MatchVID(identifier.guid, flipped_xy_vids)) {
 | 
			
		||||
            mapping.insert_or_assign(Settings::NativeButton::Y, BuildButtonParamPackageForButton(
 | 
			
		||||
                                                                    identifier, KEYCODE_BUTTON_X));
 | 
			
		||||
        } else {
 | 
			
		||||
            mapping.insert_or_assign(Settings::NativeButton::X, BuildButtonParamPackageForButton(
 | 
			
		||||
                                                                    identifier, KEYCODE_BUTTON_X));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (available_keys.find(KEYCODE_BUTTON_Y) != available_keys.end()) {
 | 
			
		||||
        if (MatchVID(identifier.guid, flipped_xy_vids)) {
 | 
			
		||||
            mapping.insert_or_assign(Settings::NativeButton::X, BuildButtonParamPackageForButton(
 | 
			
		||||
                                                                    identifier, KEYCODE_BUTTON_Y));
 | 
			
		||||
        } else {
 | 
			
		||||
            mapping.insert_or_assign(Settings::NativeButton::Y, BuildButtonParamPackageForButton(
 | 
			
		||||
                                                                    identifier, KEYCODE_BUTTON_Y));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (available_keys.find(KEYCODE_BUTTON_L1) != available_keys.end()) {
 | 
			
		||||
        mapping.insert_or_assign(Settings::NativeButton::L,
 | 
			
		||||
                                 BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_L1));
 | 
			
		||||
    }
 | 
			
		||||
    if (available_keys.find(KEYCODE_BUTTON_R1) != available_keys.end()) {
 | 
			
		||||
        mapping.insert_or_assign(Settings::NativeButton::R,
 | 
			
		||||
                                 BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_R1));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (available_keys.find(KEYCODE_BUTTON_THUMBL) != available_keys.end()) {
 | 
			
		||||
        mapping.insert_or_assign(
 | 
			
		||||
            Settings::NativeButton::LStick,
 | 
			
		||||
            BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_THUMBL));
 | 
			
		||||
    }
 | 
			
		||||
    if (available_keys.find(KEYCODE_BUTTON_THUMBR) != available_keys.end()) {
 | 
			
		||||
        mapping.insert_or_assign(
 | 
			
		||||
            Settings::NativeButton::RStick,
 | 
			
		||||
            BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_THUMBR));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (available_keys.find(KEYCODE_BUTTON_START) != available_keys.end()) {
 | 
			
		||||
        mapping.insert_or_assign(
 | 
			
		||||
            Settings::NativeButton::Plus,
 | 
			
		||||
            BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_START));
 | 
			
		||||
    }
 | 
			
		||||
    if (available_keys.find(KEYCODE_BUTTON_SELECT) != available_keys.end()) {
 | 
			
		||||
        mapping.insert_or_assign(
 | 
			
		||||
            Settings::NativeButton::Minus,
 | 
			
		||||
            BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_SELECT));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return mapping;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Common::Input::ButtonNames Android::GetUIName(
 | 
			
		||||
    [[maybe_unused]] const Common::ParamPackage& params) const {
 | 
			
		||||
    return Common::Input::ButtonNames::Value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PadIdentifier Android::GetIdentifier(const std::string& guid, size_t port) const {
 | 
			
		||||
    return {
 | 
			
		||||
        .guid = Common::UUID{},
 | 
			
		||||
        .port = controller_number,
 | 
			
		||||
        .guid = Common::UUID{guid},
 | 
			
		||||
        .port = port,
 | 
			
		||||
        .pad = 0,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,8 @@
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <set>
 | 
			
		||||
#include <jni.h>
 | 
			
		||||
#include "input_common/input_engine.h"
 | 
			
		||||
 | 
			
		||||
namespace InputCommon {
 | 
			
		||||
@@ -15,40 +17,121 @@ public:
 | 
			
		||||
    explicit Android(std::string input_engine_);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Registers controller number to accept new inputs
 | 
			
		||||
     * @param controller_number the controller number that will take this action
 | 
			
		||||
     * Registers controller number to accept new inputs.
 | 
			
		||||
     * @param j_input_device YuzuInputDevice object from the Android frontend to register.
 | 
			
		||||
     */
 | 
			
		||||
    void RegisterController(std::size_t controller_number);
 | 
			
		||||
    void RegisterController(jobject j_input_device);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the status of all buttons bound with the key to pressed
 | 
			
		||||
     * @param controller_number the controller number that will take this action
 | 
			
		||||
     * @param button_id the id of the button
 | 
			
		||||
     * @param value indicates if the button is pressed or not
 | 
			
		||||
     * Sets the status of a button on a specific controller.
 | 
			
		||||
     * @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
 | 
			
		||||
     * @param port Port determined by controller connection order.
 | 
			
		||||
     * @param button_id The Android Keycode corresponding to this event.
 | 
			
		||||
     * @param value Whether the button is pressed or not.
 | 
			
		||||
     */
 | 
			
		||||
    void SetButtonState(std::size_t controller_number, int button_id, bool value);
 | 
			
		||||
    void SetButtonState(std::string guid, size_t port, int button_id, bool value);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the status of a analog input to a specific player index
 | 
			
		||||
     * @param controller_number the controller number that will take this action
 | 
			
		||||
     * @param axis_id the id of the axis to move
 | 
			
		||||
     * @param value the analog position of the axis
 | 
			
		||||
     * Sets the status of an axis on a specific controller.
 | 
			
		||||
     * @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
 | 
			
		||||
     * @param port Port determined by controller connection order.
 | 
			
		||||
     * @param axis_id The Android axis ID corresponding to this event.
 | 
			
		||||
     * @param value Value along the given axis.
 | 
			
		||||
     */
 | 
			
		||||
    void SetAxisState(std::size_t controller_number, int axis_id, float value);
 | 
			
		||||
    void SetAxisPosition(std::string guid, size_t port, int axis_id, float value);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the status of the motion sensor to a specific player index
 | 
			
		||||
     * @param controller_number the controller number that will take this action
 | 
			
		||||
     * @param delta_timestamp time passed since last reading
 | 
			
		||||
     * @param gyro_x,gyro_y,gyro_z the gyro sensor readings
 | 
			
		||||
     * @param accel_x,accel_y,accel_z the accelerometer reading
 | 
			
		||||
     * Sets the status of the motion sensor on a specific controller
 | 
			
		||||
     * @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
 | 
			
		||||
     * @param port Port determined by controller connection order.
 | 
			
		||||
     * @param delta_timestamp Time passed since the last read.
 | 
			
		||||
     * @param gyro_x,gyro_y,gyro_z Gyro sensor readings.
 | 
			
		||||
     * @param accel_x,accel_y,accel_z Accelerometer sensor readings.
 | 
			
		||||
     */
 | 
			
		||||
    void SetMotionState(std::size_t controller_number, u64 delta_timestamp, float gyro_x,
 | 
			
		||||
    void SetMotionState(std::string guid, size_t port, u64 delta_timestamp, float gyro_x,
 | 
			
		||||
                        float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z);
 | 
			
		||||
 | 
			
		||||
    Common::Input::DriverResult SetVibration(
 | 
			
		||||
        const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override;
 | 
			
		||||
 | 
			
		||||
    bool IsVibrationEnabled(const PadIdentifier& identifier) override;
 | 
			
		||||
 | 
			
		||||
    std::vector<Common::ParamPackage> GetInputDevices() const override;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the axes reported by the YuzuInputDevice.
 | 
			
		||||
     * @param env JNI environment pointer.
 | 
			
		||||
     * @param j_device YuzuInputDevice from the Android frontend.
 | 
			
		||||
     * @return Set of the axes reported by the underlying Android InputDevice
 | 
			
		||||
     */
 | 
			
		||||
    std::set<s32> GetDeviceAxes(JNIEnv* env, jobject& j_device) const;
 | 
			
		||||
 | 
			
		||||
    Common::ParamPackage BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x,
 | 
			
		||||
                                                    int axis_y) const;
 | 
			
		||||
 | 
			
		||||
    Common::ParamPackage BuildAnalogParamPackageForButton(PadIdentifier identifier, s32 axis,
 | 
			
		||||
                                                          bool invert) const;
 | 
			
		||||
 | 
			
		||||
    Common::ParamPackage BuildButtonParamPackageForButton(PadIdentifier identifier,
 | 
			
		||||
                                                          s32 button) const;
 | 
			
		||||
 | 
			
		||||
    bool MatchVID(Common::UUID device, const std::vector<std::string>& vids) const;
 | 
			
		||||
 | 
			
		||||
    AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
 | 
			
		||||
 | 
			
		||||
    ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
 | 
			
		||||
 | 
			
		||||
    Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    std::unordered_map<PadIdentifier, jobject> input_devices;
 | 
			
		||||
 | 
			
		||||
    /// Returns the correct identifier corresponding to the player index
 | 
			
		||||
    PadIdentifier GetIdentifier(std::size_t controller_number) const;
 | 
			
		||||
    PadIdentifier GetIdentifier(const std::string& guid, size_t port) const;
 | 
			
		||||
 | 
			
		||||
    static constexpr s32 AXIS_X = 0;
 | 
			
		||||
    static constexpr s32 AXIS_Y = 1;
 | 
			
		||||
    static constexpr s32 AXIS_Z = 11;
 | 
			
		||||
    static constexpr s32 AXIS_RX = 12;
 | 
			
		||||
    static constexpr s32 AXIS_RY = 13;
 | 
			
		||||
    static constexpr s32 AXIS_RZ = 14;
 | 
			
		||||
    static constexpr s32 AXIS_HAT_X = 15;
 | 
			
		||||
    static constexpr s32 AXIS_HAT_Y = 16;
 | 
			
		||||
    static constexpr s32 AXIS_LTRIGGER = 17;
 | 
			
		||||
    static constexpr s32 AXIS_RTRIGGER = 18;
 | 
			
		||||
 | 
			
		||||
    static constexpr s32 KEYCODE_DPAD_UP = 19;
 | 
			
		||||
    static constexpr s32 KEYCODE_DPAD_DOWN = 20;
 | 
			
		||||
    static constexpr s32 KEYCODE_DPAD_LEFT = 21;
 | 
			
		||||
    static constexpr s32 KEYCODE_DPAD_RIGHT = 22;
 | 
			
		||||
    static constexpr s32 KEYCODE_BUTTON_A = 96;
 | 
			
		||||
    static constexpr s32 KEYCODE_BUTTON_B = 97;
 | 
			
		||||
    static constexpr s32 KEYCODE_BUTTON_X = 99;
 | 
			
		||||
    static constexpr s32 KEYCODE_BUTTON_Y = 100;
 | 
			
		||||
    static constexpr s32 KEYCODE_BUTTON_L1 = 102;
 | 
			
		||||
    static constexpr s32 KEYCODE_BUTTON_R1 = 103;
 | 
			
		||||
    static constexpr s32 KEYCODE_BUTTON_L2 = 104;
 | 
			
		||||
    static constexpr s32 KEYCODE_BUTTON_R2 = 105;
 | 
			
		||||
    static constexpr s32 KEYCODE_BUTTON_THUMBL = 106;
 | 
			
		||||
    static constexpr s32 KEYCODE_BUTTON_THUMBR = 107;
 | 
			
		||||
    static constexpr s32 KEYCODE_BUTTON_START = 108;
 | 
			
		||||
    static constexpr s32 KEYCODE_BUTTON_SELECT = 109;
 | 
			
		||||
    const std::vector<s32> keycode_ids{
 | 
			
		||||
        KEYCODE_DPAD_UP,       KEYCODE_DPAD_DOWN,     KEYCODE_DPAD_LEFT,    KEYCODE_DPAD_RIGHT,
 | 
			
		||||
        KEYCODE_BUTTON_A,      KEYCODE_BUTTON_B,      KEYCODE_BUTTON_X,     KEYCODE_BUTTON_Y,
 | 
			
		||||
        KEYCODE_BUTTON_L1,     KEYCODE_BUTTON_R1,     KEYCODE_BUTTON_L2,    KEYCODE_BUTTON_R2,
 | 
			
		||||
        KEYCODE_BUTTON_THUMBL, KEYCODE_BUTTON_THUMBR, KEYCODE_BUTTON_START, KEYCODE_BUTTON_SELECT,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const std::string sony_vid{"054c"};
 | 
			
		||||
    const std::string nintendo_vid{"057e"};
 | 
			
		||||
    const std::string razer_vid{"1532"};
 | 
			
		||||
    const std::string redmagic_vid{"3537"};
 | 
			
		||||
    const std::string backbone_labs_vid{"358a"};
 | 
			
		||||
    const std::vector<std::string> flipped_ab_vids{sony_vid, nintendo_vid, razer_vid, redmagic_vid,
 | 
			
		||||
                                                   backbone_labs_vid};
 | 
			
		||||
    const std::vector<std::string> flipped_xy_vids{sony_vid, razer_vid, redmagic_vid,
 | 
			
		||||
                                                   backbone_labs_vid};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} // namespace InputCommon
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include "common/input.h"
 | 
			
		||||
#include "common/param_package.h"
 | 
			
		||||
#include "input_common/drivers/android.h"
 | 
			
		||||
#include "input_common/drivers/camera.h"
 | 
			
		||||
#include "input_common/drivers/keyboard.h"
 | 
			
		||||
#include "input_common/drivers/mouse.h"
 | 
			
		||||
@@ -28,6 +27,10 @@
 | 
			
		||||
#include "input_common/drivers/sdl_driver.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef ANDROID
 | 
			
		||||
#include "input_common/drivers/android.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace InputCommon {
 | 
			
		||||
 | 
			
		||||
/// Dummy engine to get periodic updates
 | 
			
		||||
@@ -79,7 +82,9 @@ struct InputSubsystem::Impl {
 | 
			
		||||
        RegisterEngine("cemuhookudp", udp_client);
 | 
			
		||||
        RegisterEngine("tas", tas_input);
 | 
			
		||||
        RegisterEngine("camera", camera);
 | 
			
		||||
#ifdef ANDROID
 | 
			
		||||
        RegisterEngine("android", android);
 | 
			
		||||
#endif
 | 
			
		||||
        RegisterEngine("virtual_amiibo", virtual_amiibo);
 | 
			
		||||
        RegisterEngine("virtual_gamepad", virtual_gamepad);
 | 
			
		||||
#ifdef HAVE_SDL2
 | 
			
		||||
@@ -111,7 +116,9 @@ struct InputSubsystem::Impl {
 | 
			
		||||
        UnregisterEngine(udp_client);
 | 
			
		||||
        UnregisterEngine(tas_input);
 | 
			
		||||
        UnregisterEngine(camera);
 | 
			
		||||
#ifdef ANDROID
 | 
			
		||||
        UnregisterEngine(android);
 | 
			
		||||
#endif
 | 
			
		||||
        UnregisterEngine(virtual_amiibo);
 | 
			
		||||
        UnregisterEngine(virtual_gamepad);
 | 
			
		||||
#ifdef HAVE_SDL2
 | 
			
		||||
@@ -128,12 +135,16 @@ struct InputSubsystem::Impl {
 | 
			
		||||
            Common::ParamPackage{{"display", "Any"}, {"engine", "any"}},
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
#ifndef ANDROID
 | 
			
		||||
        auto keyboard_devices = keyboard->GetInputDevices();
 | 
			
		||||
        devices.insert(devices.end(), keyboard_devices.begin(), keyboard_devices.end());
 | 
			
		||||
        auto mouse_devices = mouse->GetInputDevices();
 | 
			
		||||
        devices.insert(devices.end(), mouse_devices.begin(), mouse_devices.end());
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef ANDROID
 | 
			
		||||
        auto android_devices = android->GetInputDevices();
 | 
			
		||||
        devices.insert(devices.end(), android_devices.begin(), android_devices.end());
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef HAVE_LIBUSB
 | 
			
		||||
        auto gcadapter_devices = gcadapter->GetInputDevices();
 | 
			
		||||
        devices.insert(devices.end(), gcadapter_devices.begin(), gcadapter_devices.end());
 | 
			
		||||
@@ -162,9 +173,11 @@ struct InputSubsystem::Impl {
 | 
			
		||||
        if (engine == mouse->GetEngineName()) {
 | 
			
		||||
            return mouse;
 | 
			
		||||
        }
 | 
			
		||||
#ifdef ANDROID
 | 
			
		||||
        if (engine == android->GetEngineName()) {
 | 
			
		||||
            return android;
 | 
			
		||||
        }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef HAVE_LIBUSB
 | 
			
		||||
        if (engine == gcadapter->GetEngineName()) {
 | 
			
		||||
            return gcadapter;
 | 
			
		||||
@@ -245,9 +258,11 @@ struct InputSubsystem::Impl {
 | 
			
		||||
        if (engine == mouse->GetEngineName()) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
#ifdef ANDROID
 | 
			
		||||
        if (engine == android->GetEngineName()) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef HAVE_LIBUSB
 | 
			
		||||
        if (engine == gcadapter->GetEngineName()) {
 | 
			
		||||
            return true;
 | 
			
		||||
@@ -276,7 +291,9 @@ struct InputSubsystem::Impl {
 | 
			
		||||
    void BeginConfiguration() {
 | 
			
		||||
        keyboard->BeginConfiguration();
 | 
			
		||||
        mouse->BeginConfiguration();
 | 
			
		||||
#ifdef ANDROID
 | 
			
		||||
        android->BeginConfiguration();
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef HAVE_LIBUSB
 | 
			
		||||
        gcadapter->BeginConfiguration();
 | 
			
		||||
#endif
 | 
			
		||||
@@ -290,7 +307,9 @@ struct InputSubsystem::Impl {
 | 
			
		||||
    void EndConfiguration() {
 | 
			
		||||
        keyboard->EndConfiguration();
 | 
			
		||||
        mouse->EndConfiguration();
 | 
			
		||||
#ifdef ANDROID
 | 
			
		||||
        android->EndConfiguration();
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef HAVE_LIBUSB
 | 
			
		||||
        gcadapter->EndConfiguration();
 | 
			
		||||
#endif
 | 
			
		||||
@@ -321,7 +340,6 @@ struct InputSubsystem::Impl {
 | 
			
		||||
    std::shared_ptr<TasInput::Tas> tas_input;
 | 
			
		||||
    std::shared_ptr<CemuhookUDP::UDPClient> udp_client;
 | 
			
		||||
    std::shared_ptr<Camera> camera;
 | 
			
		||||
    std::shared_ptr<Android> android;
 | 
			
		||||
    std::shared_ptr<VirtualAmiibo> virtual_amiibo;
 | 
			
		||||
    std::shared_ptr<VirtualGamepad> virtual_gamepad;
 | 
			
		||||
 | 
			
		||||
@@ -333,6 +351,10 @@ struct InputSubsystem::Impl {
 | 
			
		||||
    std::shared_ptr<SDLDriver> sdl;
 | 
			
		||||
    std::shared_ptr<Joycons> joycon;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef ANDROID
 | 
			
		||||
    std::shared_ptr<Android> android;
 | 
			
		||||
#endif
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {}
 | 
			
		||||
@@ -387,6 +409,7 @@ const Camera* InputSubsystem::GetCamera() const {
 | 
			
		||||
    return impl->camera.get();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef ANDROID
 | 
			
		||||
Android* InputSubsystem::GetAndroid() {
 | 
			
		||||
    return impl->android.get();
 | 
			
		||||
}
 | 
			
		||||
@@ -394,6 +417,7 @@ Android* InputSubsystem::GetAndroid() {
 | 
			
		||||
const Android* InputSubsystem::GetAndroid() const {
 | 
			
		||||
    return impl->android.get();
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
VirtualAmiibo* InputSubsystem::GetVirtualAmiibo() {
 | 
			
		||||
    return impl->virtual_amiibo.get();
 | 
			
		||||
 
 | 
			
		||||
@@ -90,6 +90,7 @@ void QtConfig::ReadQtPlayerValues(const std::size_t player_index) {
 | 
			
		||||
        if (profile_name.empty()) {
 | 
			
		||||
            // Use the global input config
 | 
			
		||||
            player = Settings::values.players.GetValue(true)[player_index];
 | 
			
		||||
            player.profile_name = "";
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user