summaryrefslogtreecommitdiffstats
path: root/src/android/app/src/main/java/org
diff options
context:
space:
mode:
Diffstat (limited to 'src/android/app/src/main/java/org')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt176
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt39
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/NativeInput.kt416
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuInputDevice.kt93
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuVibrator.kt76
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/AnalogDirection.kt11
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/ButtonName.kt19
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/InputType.kt13
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeAnalog.kt14
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeButton.kt38
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeTrigger.kt10
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NpadStyleIndex.kt30
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/PlayerInput.kt83
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt15
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/AnalogInputSetting.kt31
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ButtonInputSetting.kt29
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt9
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt7
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputProfileSetting.kt32
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputSetting.kt134
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/IntSingleChoiceSetting.kt38
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ModifierInputSetting.kt31
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt11
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt233
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt14
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt15
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt11
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt10
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt9
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt300
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileAdapter.kt68
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt155
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/NewInputProfileDialogFragment.kt79
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt247
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt (renamed from src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt)76
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt86
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt794
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSearchFragment.kt (renamed from src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt)6
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt (renamed from src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt)43
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt6
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputProfileViewHolder.kt33
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt71
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt11
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt64
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt6
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt16
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt9
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt9
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt14
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt99
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt7
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt25
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt15
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt456
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt15
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt6
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ParamPackage.kt141
60 files changed, 3568 insertions, 943 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
index 6ebb46af7..fd229c855 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
@@ -30,34 +30,6 @@ import org.yuzu.yuzu_emu.model.GameVerificationResult
* 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
- }
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
index 76778c10a..72943f33e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
@@ -7,6 +7,7 @@ import android.app.Application
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()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
index 7a8d03610..0b70fccec 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
@@ -39,6 +39,7 @@ import org.yuzu.yuzu_emu.NativeLibrary
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],
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/NativeInput.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/NativeInput.kt
new file mode 100644
index 000000000..15d776311
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/NativeInput.kt
@@ -0,0 +1,416 @@
+// 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)
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuInputDevice.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuInputDevice.kt
new file mode 100644
index 000000000..15cc38c7f
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuInputDevice.kt
@@ -0,0 +1,93 @@
+// 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)
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuVibrator.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuVibrator.kt
new file mode 100644
index 000000000..aac49ecae
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/YuzuVibrator.kt
@@ -0,0 +1,76 @@
+// 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)
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/AnalogDirection.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/AnalogDirection.kt
new file mode 100644
index 000000000..0a5fab2ae
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/AnalogDirection.kt
@@ -0,0 +1,11 @@
+// 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")
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/ButtonName.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/ButtonName.kt
new file mode 100644
index 000000000..b8846ecad
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/ButtonName.kt
@@ -0,0 +1,19 @@
+// 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
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/InputType.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/InputType.kt
new file mode 100644
index 000000000..f725231cb
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/InputType.kt
@@ -0,0 +1,13 @@
+// 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)
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeAnalog.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeAnalog.kt
new file mode 100644
index 000000000..c3b7a785d
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeAnalog.kt
@@ -0,0 +1,14 @@
+// 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
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeButton.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeButton.kt
new file mode 100644
index 000000000..c5ccd7115
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeButton.kt
@@ -0,0 +1,38 @@
+// 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
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeTrigger.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeTrigger.kt
new file mode 100644
index 000000000..625f352b4
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NativeTrigger.kt
@@ -0,0 +1,10 @@
+// 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)
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NpadStyleIndex.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NpadStyleIndex.kt
new file mode 100644
index 000000000..e2a3d7aff
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/NpadStyleIndex.kt
@@ -0,0 +1,30 @@
+// 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
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/PlayerInput.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/PlayerInput.kt
new file mode 100644
index 000000000..d35de80c4
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/input/model/PlayerInput.kt
@@ -0,0 +1,83 @@
+// 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
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
index 862c6c483..4f6b93bd2 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
@@ -4,17 +4,30 @@
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"
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/AnalogInputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/AnalogInputSetting.kt
new file mode 100644
index 000000000..a2996725e
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/AnalogInputSetting.kt
@@ -0,0 +1,31 @@
+// 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)
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ButtonInputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ButtonInputSetting.kt
new file mode 100644
index 000000000..786d09a7a
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ButtonInputSetting.kt
@@ -0,0 +1,29 @@
+// 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)
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt
index 1d81f5f2b..58febff1d 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt
@@ -3,13 +3,16 @@
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)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt
index d31ce1c31..8a6a51d5c 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt
@@ -3,8 +3,11 @@
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
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputProfileSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputProfileSetting.kt
new file mode 100644
index 000000000..c46de08c5
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputProfileSetting.kt
@@ -0,0 +1,32 @@
+// 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)
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputSetting.kt
new file mode 100644
index 000000000..2d118bff3
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/InputSetting.kt
@@ -0,0 +1,134 @@
+// 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)
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/IntSingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/IntSingleChoiceSetting.kt
new file mode 100644
index 000000000..e024c793a
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/IntSingleChoiceSetting.kt
@@ -0,0 +1,38 @@
+// 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
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ModifierInputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ModifierInputSetting.kt
new file mode 100644
index 000000000..a1db3cc87
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/ModifierInputSetting.kt
@@ -0,0 +1,31 @@
+// 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)
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt
index 425160024..06f607424 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt
@@ -4,13 +4,16 @@
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
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
index 21ca97bc1..8f724835e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
@@ -3,8 +3,12 @@
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))
}
}
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt
index 97a5a9e59..ea5e099ed 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt
@@ -3,16 +3,20 @@
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) =
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt
index b9b709bf7..6a5cdf48b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt
@@ -3,6 +3,7 @@
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) =
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt
index ba7920f50..5260ff4dc 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt
@@ -3,15 +3,18 @@
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()) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt
index 94953b18a..c722393dd 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt
@@ -8,10 +8,12 @@ import androidx.annotation.StringRes
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
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt
index 44d47dd69..4984bf52e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt
@@ -3,15 +3,18 @@
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 {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt
new file mode 100644
index 000000000..16a1d0504
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputDialogFragment.kt
@@ -0,0 +1,300 @@
+// 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
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileAdapter.kt
new file mode 100644
index 000000000..5656e9d8d
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileAdapter.kt
@@ -0,0 +1,68 @@
+// 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
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt
new file mode 100644
index 000000000..9b24d41c1
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/InputProfileDialogFragment.kt
@@ -0,0 +1,155 @@
+// 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
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/NewInputProfileDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/NewInputProfileDialogFragment.kt
new file mode 100644
index 000000000..6e52bea80
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/NewInputProfileDialogFragment.kt
@@ -0,0 +1,79 @@
+// 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
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
index 6f072241a..681a18b3b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
@@ -25,9 +25,9 @@ import org.yuzu.yuzu_emu.NativeLibrary
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()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
index be9b3031b..45c8faa10 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
@@ -8,12 +8,11 @@ import android.icu.util.Calendar
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,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt
index 60e029f34..5d1ea5d29 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt
@@ -1,7 +1,7 @@
// 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)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
index 6f6e7be10..0cf944b43 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
@@ -24,8 +24,9 @@ import kotlinx.coroutines.flow.collectLatest
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
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
index db1a58147..e491c29a2 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -3,11 +3,17 @@
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
)
)
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSearchFragment.kt
index a135b80b4..51740a2ac 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSearchFragment.kt
@@ -1,7 +1,7 @@
// 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)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt
index 5cb6a5d57..fbdca04e9 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt
@@ -1,20 +1,26 @@
// 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
+ }
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
index 5ad0899dd..a43f7b1fe 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
@@ -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
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt
index f5bcf705c..0815c36e2 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/HeaderViewHolder.kt
@@ -16,7 +16,7 @@ class HeaderViewHolder(val binding: ListItemSettingsHeaderBinding, adapter: Sett
}
override fun bind(item: SettingsItem) {
- binding.textHeaderName.setText(item.nameId)
+ binding.textHeaderName.text = item.title
}
override fun onClick(clicked: View) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputProfileViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputProfileViewHolder.kt
new file mode 100644
index 000000000..81161d5d3
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputProfileViewHolder.kt
@@ -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
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt
new file mode 100644
index 000000000..1f1f08190
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/InputViewHolder.kt
@@ -0,0 +1,71 @@
+// 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)
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt
index 507184238..2841520a5 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt
@@ -5,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()
}
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
index 02dab3785..9705d428c 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
@@ -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,32 +18,38 @@ 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 ||
!NativeConfig.isPerGameConfigLoaded()
@@ -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
+ )
+ }
}
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
index 596c18012..fcfac040e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
@@ -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
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt
index 20d35a17d..165c765b3 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt
@@ -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 {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
index d26bf9374..f779a7b60 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
@@ -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 ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
index 6b25cc525..c737ed5e8 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
@@ -277,6 +277,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
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
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
index 87e130d3e..14a2504b6 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
@@ -91,6 +91,20 @@ class HomeSettingsFragment : Fragment() {
)
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,
R.string.install_gpu_driver_description,
R.drawable.ic_build,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
index c87486c90..66907085a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
@@ -24,10 +24,10 @@ import androidx.core.content.ContextCompat
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
)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt
index b14a4f96e..fee3d04ee 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableButton.kt
@@ -9,7 +9,8 @@ import android.graphics.Canvas
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
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt
index 8aef6f5a5..0cb6ff244 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt
@@ -9,7 +9,8 @@ import android.graphics.Canvas
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
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt
index 113bf7c24..4b07107fc 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt
@@ -13,7 +13,9 @@ import kotlin.math.atan2
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) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt
index e63382e1d..2c7356e6a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt
@@ -6,439 +6,89 @@ package org.yuzu.yuzu_emu.utils
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
)
+ return true
}
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)
- }
+ 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 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
- }
- }
- }
-
- 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)
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
index a4c14b3a7..7228f25d2 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
@@ -6,6 +6,8 @@ package org.yuzu.yuzu_emu.utils
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()
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt
index 68ed66565..331b7ddca 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NfcReader.kt
@@ -14,7 +14,7 @@ import android.os.Build
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())
)
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ParamPackage.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ParamPackage.kt
new file mode 100644
index 000000000..83fc7da3c
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ParamPackage.kt
@@ -0,0 +1,141 @@
+// 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()
+}