early-access version 4146

This commit is contained in:
pineappleEA 2024-02-18 07:19:28 +01:00
parent ebd0305be1
commit d2d37b01f7
135 changed files with 7043 additions and 1983 deletions

View File

@ -1,7 +1,7 @@
yuzu emulator early access
=============
This is the source code for early-access 4145.
This is the source code for early-access 4146.
## Legal Notice

View File

@ -14,6 +14,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<application
android:name="org.yuzu.yuzu_emu.YuzuApplication"

View File

@ -3,24 +3,21 @@
package org.yuzu.yuzu_emu
import android.app.Dialog
import android.content.DialogInterface
import android.net.Uri
import android.os.Bundle
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.Surface
import android.view.View
import android.widget.TextView
import androidx.annotation.Keep
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.lang.ref.WeakReference
import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.fragments.CoreErrorDialogFragment
import org.yuzu.yuzu_emu.utils.DocumentsTree
import org.yuzu.yuzu_emu.utils.FileUtil
import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
import org.yuzu.yuzu_emu.model.InstallResult
import org.yuzu.yuzu_emu.model.Patch
import org.yuzu.yuzu_emu.model.GameVerificationResult
@ -30,34 +27,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 +96,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)
/**
@ -318,46 +181,13 @@ object NativeLibrary {
ErrorUnknown
}
private var coreErrorAlertResult = false
private val coreErrorAlertLock = Object()
class CoreErrorDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val title = requireArguments().serializable<String>("title")
val message = requireArguments().serializable<String>("message")
return MaterialAlertDialogBuilder(requireActivity())
.setTitle(title)
.setMessage(message)
.setPositiveButton(R.string.continue_button, null)
.setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int ->
coreErrorAlertResult = false
synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() }
}
.create()
}
override fun onDismiss(dialog: DialogInterface) {
coreErrorAlertResult = true
synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() }
}
companion object {
fun newInstance(title: String?, message: String?): CoreErrorDialogFragment {
val frag = CoreErrorDialogFragment()
val args = Bundle()
args.putString("title", title)
args.putString("message", message)
frag.arguments = args
return frag
}
}
}
var coreErrorAlertResult = false
val coreErrorAlertLock = Object()
private fun onCoreErrorImpl(title: String, message: String) {
val emulationActivity = sEmulationActivity.get()
if (emulationActivity == null) {
error("[NativeLibrary] EmulationActivity not present")
Log.error("[NativeLibrary] EmulationActivity not present")
return
}
@ -373,7 +203,7 @@ object NativeLibrary {
fun onCoreError(error: CoreError?, details: String): Boolean {
val emulationActivity = sEmulationActivity.get()
if (emulationActivity == null) {
error("[NativeLibrary] EmulationActivity not present")
Log.error("[NativeLibrary] EmulationActivity not present")
return false
}
@ -404,7 +234,7 @@ object NativeLibrary {
}
// Show the AlertDialog on the main thread.
emulationActivity.runOnUiThread(Runnable { onCoreErrorImpl(title, message) })
emulationActivity.runOnUiThread { onCoreErrorImpl(title, message) }
// Wait for the lock to notify that it is complete.
synchronized(coreErrorAlertLock) { coreErrorAlertLock.wait() }
@ -629,46 +459,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
}
}

View File

@ -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()

View File

@ -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],

View File

@ -3,15 +3,15 @@
package org.yuzu.yuzu_emu.adapters
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.model.Driver
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class DriverAdapter(private val driverViewModel: DriverViewModel) :
@ -44,25 +44,15 @@ class DriverAdapter(private val driverViewModel: DriverViewModel) :
}
// Delay marquee by 3s
title.postDelayed(
{
title.isSelected = true
title.ellipsize = TextUtils.TruncateAt.MARQUEE
version.isSelected = true
version.ellipsize = TextUtils.TruncateAt.MARQUEE
description.isSelected = true
description.ellipsize = TextUtils.TruncateAt.MARQUEE
},
3000
)
title.marquee()
version.marquee()
description.marquee()
title.text = model.title
version.text = model.version
description.text = model.description
if (model.title != binding.root.context.getString(R.string.system_gpu_driver)) {
buttonDelete.visibility = View.VISIBLE
} else {
buttonDelete.visibility = View.GONE
}
buttonDelete.setVisible(
model.title != binding.root.context.getString(R.string.system_gpu_driver)
)
}
}
}

View File

@ -4,7 +4,6 @@
package org.yuzu.yuzu_emu.adapters
import android.net.Uri
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.fragment.app.FragmentActivity
@ -12,6 +11,7 @@ import org.yuzu.yuzu_emu.databinding.CardFolderBinding
import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment
import org.yuzu.yuzu_emu.model.GameDir
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) :
@ -29,13 +29,7 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
override fun bind(model: GameDir) {
binding.apply {
path.text = Uri.parse(model.uriString).path
path.postDelayed(
{
path.isSelected = true
path.ellipsize = TextUtils.TruncateAt.MARQUEE
},
3000
)
path.marquee()
buttonEdit.setOnClickListener {
GameFolderPropertiesDialogFragment.newInstance(model)

View File

@ -4,7 +4,6 @@
package org.yuzu.yuzu_emu.adapters
import android.net.Uri
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageView
@ -27,6 +26,7 @@ import org.yuzu.yuzu_emu.databinding.CardGameBinding
import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.utils.GameIconUtils
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class GameAdapter(private val activity: AppCompatActivity) :
@ -44,14 +44,7 @@ class GameAdapter(private val activity: AppCompatActivity) :
binding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ")
binding.textGameTitle.postDelayed(
{
binding.textGameTitle.ellipsize = TextUtils.TruncateAt.MARQUEE
binding.textGameTitle.isSelected = true
},
3000
)
binding.textGameTitle.marquee()
binding.cardGame.setOnClickListener { onClick(model) }
binding.cardGame.setOnLongClickListener { onLongClick(model) }
}

View File

@ -3,21 +3,18 @@
package org.yuzu.yuzu_emu.adapters
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding
import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding
import org.yuzu.yuzu_emu.model.GameProperty
import org.yuzu.yuzu_emu.model.InstallableProperty
import org.yuzu.yuzu_emu.model.SubmenuProperty
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.utils.collect
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class GamePropertiesAdapter(
@ -76,23 +73,15 @@ class GamePropertiesAdapter(
)
)
binding.details.postDelayed({
binding.details.isSelected = true
binding.details.ellipsize = TextUtils.TruncateAt.MARQUEE
}, 3000)
binding.details.marquee()
if (submenuProperty.details != null) {
binding.details.visibility = View.VISIBLE
binding.details.setVisible(true)
binding.details.text = submenuProperty.details.invoke()
} else if (submenuProperty.detailsFlow != null) {
binding.details.visibility = View.VISIBLE
viewLifecycle.lifecycleScope.launch {
viewLifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
submenuProperty.detailsFlow.collect { binding.details.text = it }
}
}
binding.details.setVisible(true)
submenuProperty.detailsFlow.collect(viewLifecycle) { binding.details.text = it }
} else {
binding.details.visibility = View.GONE
binding.details.setVisible(false)
}
}
}
@ -112,14 +101,10 @@ class GamePropertiesAdapter(
)
)
if (installableProperty.install != null) {
binding.buttonInstall.visibility = View.VISIBLE
binding.buttonInstall.setOnClickListener { installableProperty.install.invoke() }
}
if (installableProperty.export != null) {
binding.buttonExport.visibility = View.VISIBLE
binding.buttonExport.setOnClickListener { installableProperty.export.invoke() }
}
binding.buttonInstall.setVisible(installableProperty.install != null)
binding.buttonInstall.setOnClickListener { installableProperty.install?.invoke() }
binding.buttonExport.setVisible(installableProperty.export != null)
binding.buttonExport.setOnClickListener { installableProperty.export?.invoke() }
}
}

View File

@ -3,22 +3,19 @@
package org.yuzu.yuzu_emu.adapters
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
import org.yuzu.yuzu_emu.model.HomeSetting
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.utils.collect
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class HomeSettingAdapter(
@ -59,18 +56,8 @@ class HomeSettingAdapter(
binding.optionIcon.alpha = 0.5f
}
viewLifecycle.lifecycleScope.launch {
viewLifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
model.details.collect { updateOptionDetails(it) }
}
}
binding.optionDetail.postDelayed(
{
binding.optionDetail.ellipsize = TextUtils.TruncateAt.MARQUEE
binding.optionDetail.isSelected = true
},
3000
)
model.details.collect(viewLifecycle) { updateOptionDetails(it) }
binding.optionDetail.marquee()
binding.root.setOnClickListener { onClick(model) }
}
@ -90,7 +77,7 @@ class HomeSettingAdapter(
private fun updateOptionDetails(detailString: String) {
if (detailString.isNotEmpty()) {
binding.optionDetail.text = detailString
binding.optionDetail.visibility = View.VISIBLE
binding.optionDetail.setVisible(true)
}
}
}

View File

@ -4,10 +4,10 @@
package org.yuzu.yuzu_emu.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import org.yuzu.yuzu_emu.databinding.CardInstallableBinding
import org.yuzu.yuzu_emu.model.Installable
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class InstallableAdapter(installables: List<Installable>) :
@ -26,14 +26,10 @@ class InstallableAdapter(installables: List<Installable>) :
binding.title.setText(model.titleId)
binding.description.setText(model.descriptionId)
if (model.install != null) {
binding.buttonInstall.visibility = View.VISIBLE
binding.buttonInstall.setOnClickListener { model.install.invoke() }
}
if (model.export != null) {
binding.buttonExport.visibility = View.VISIBLE
binding.buttonExport.setOnClickListener { model.export.invoke() }
}
binding.buttonInstall.setVisible(model.install != null)
binding.buttonInstall.setOnClickListener { model.install?.invoke() }
binding.buttonExport.setVisible(model.export != null)
binding.buttonExport.setOnClickListener { model.export?.invoke() }
}
}
}

View File

@ -4,12 +4,12 @@
package org.yuzu.yuzu_emu.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.fragments.LicenseBottomSheetDialogFragment
import org.yuzu.yuzu_emu.model.License
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<License>) :
@ -25,7 +25,7 @@ class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<Lic
binding.apply {
textSettingName.text = root.context.getString(model.titleId)
textSettingDescription.text = root.context.getString(model.descriptionId)
textSettingValue.visibility = View.GONE
textSettingValue.setVisible(false)
root.setOnClickListener { onClick(model) }
}

View File

@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.adapters
import android.text.Html
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat
@ -17,6 +16,7 @@ import org.yuzu.yuzu_emu.model.SetupCallback
import org.yuzu.yuzu_emu.model.SetupPage
import org.yuzu.yuzu_emu.model.StepState
import org.yuzu.yuzu_emu.utils.ViewUtils
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :
@ -30,8 +30,8 @@ class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :
AbstractViewHolder<SetupPage>(binding), SetupCallback {
override fun bind(model: SetupPage) {
if (model.stepCompleted.invoke() == StepState.COMPLETE) {
binding.buttonAction.visibility = View.INVISIBLE
binding.textConfirmation.visibility = View.VISIBLE
binding.buttonAction.setVisible(visible = false, gone = false)
binding.textConfirmation.setVisible(true)
}
binding.icon.setImageDrawable(

View File

@ -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)
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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")
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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"

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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))
}
}
}

View File

@ -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) =

View File

@ -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) =

View File

@ -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()) {

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}
}
}

View File

@ -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

View File

@ -0,0 +1,148 @@
// 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.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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
import org.yuzu.yuzu_emu.utils.collect
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)
settingsViewModel.shouldShowDeleteProfileDialog.collect(viewLifecycleOwner) {
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
}
}
}

View File

@ -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
}
}
}

View File

@ -13,21 +13,16 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.navArgs
import com.google.android.material.color.MaterialColors
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
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() {
@ -70,39 +65,23 @@ class SettingsActivity : AppCompatActivity() {
)
}
lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
settingsViewModel.shouldRecreate.collectLatest {
if (it) {
settingsViewModel.setShouldRecreate(false)
recreate()
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
settingsViewModel.shouldNavigateBack.collectLatest {
if (it) {
settingsViewModel.setShouldNavigateBack(false)
navigateBack()
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
settingsViewModel.shouldShowResetSettingsDialog.collectLatest {
if (it) {
settingsViewModel.setShouldShowResetSettingsDialog(false)
ResetSettingsDialogFragment().show(
supportFragmentManager,
ResetSettingsDialogFragment.TAG
)
}
}
}
settingsViewModel.shouldRecreate.collect(
this,
resetState = { settingsViewModel.setShouldRecreate(false) }
) { if (it) recreate() }
settingsViewModel.shouldNavigateBack.collect(
this,
resetState = { settingsViewModel.setShouldNavigateBack(false) }
) { if (it) navigateBack() }
settingsViewModel.shouldShowResetSettingsDialog.collect(
this,
resetState = { settingsViewModel.setShouldShowResetSettingsDialog(false) }
) {
if (it) {
ResetSettingsDialogFragment().show(
supportFragmentManager,
ResetSettingsDialogFragment.TAG
)
}
}
@ -137,6 +116,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()

View File

@ -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,

View File

@ -0,0 +1,278 @@
// SPDX-FileCopyrightText: 2023 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.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
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 com.google.android.material.slider.Slider
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.utils.ParamPackage
import org.yuzu.yuzu_emu.utils.collect
class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener {
private var type = 0
private var position = 0
private var defaultCancelListener =
DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> closeDialog() }
private val settingsViewModel: SettingsViewModel by activityViewModels()
private lateinit var sliderBinding: DialogSliderBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
type = requireArguments().getInt(TYPE)
position = requireArguments().getInt(POSITION)
if (settingsViewModel.clickedItem == null) dismiss()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return when (type) {
TYPE_RESET_SETTING -> {
MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.reset_setting_confirmation)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
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()
}
SettingsItem.TYPE_SINGLE_CHOICE -> {
val item = settingsViewModel.clickedItem as SingleChoiceSetting
val value = getSelectionForSingleChoiceValue(item)
MaterialAlertDialogBuilder(requireContext())
.setTitle(item.title)
.setSingleChoiceItems(item.choicesId, value, this)
.create()
}
SettingsItem.TYPE_SLIDER -> {
sliderBinding = DialogSliderBinding.inflate(layoutInflater)
val item = settingsViewModel.clickedItem as SliderSetting
settingsViewModel.setSliderTextValue(item.getSelectedValue().toFloat(), item.units)
sliderBinding.slider.apply {
valueFrom = item.min.toFloat()
valueTo = item.max.toFloat()
value = settingsViewModel.sliderProgress.value.toFloat()
addOnChangeListener { _: Slider, value: Float, _: Boolean ->
settingsViewModel.setSliderTextValue(value, item.units)
}
}
MaterialAlertDialogBuilder(requireContext())
.setTitle(item.title)
.setView(sliderBinding.root)
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, defaultCancelListener)
.create()
}
SettingsItem.TYPE_STRING_SINGLE_CHOICE -> {
val item = settingsViewModel.clickedItem as StringSingleChoiceSetting
MaterialAlertDialogBuilder(requireContext())
.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()
}
else -> super.onCreateDialog(savedInstanceState)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return when (type) {
SettingsItem.TYPE_SLIDER -> sliderBinding.root
else -> super.onCreateView(inflater, container, savedInstanceState)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
when (type) {
SettingsItem.TYPE_SLIDER -> {
settingsViewModel.sliderTextValue.collect(viewLifecycleOwner) {
sliderBinding.textValue.text = it
}
settingsViewModel.sliderProgress.collect(viewLifecycleOwner) {
sliderBinding.slider.value = it.toFloat()
}
}
}
}
override fun onClick(dialog: DialogInterface, which: Int) {
when (settingsViewModel.clickedItem) {
is SingleChoiceSetting -> {
val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
val value = getValueForSingleChoiceSelection(scSetting, which)
scSetting.setSelectedValue(value)
}
is StringSingleChoiceSetting -> {
val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
val value = scSetting.getValueAt(which)
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)
}
}
closeDialog()
}
private fun closeDialog() {
settingsViewModel.setAdapterItemChanged(position)
settingsViewModel.clickedItem = null
settingsViewModel.setSliderProgress(-1f)
dismiss()
}
private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int {
val valuesId = item.valuesId
return if (valuesId > 0) {
val valuesArray = requireContext().resources.getIntArray(valuesId)
valuesArray[which]
} else {
which
}
}
private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
val value = item.getSelectedValue()
val valuesId = item.valuesId
if (valuesId > 0) {
val valuesArray = requireContext().resources.getIntArray(valuesId)
for (index in valuesArray.indices) {
val current = valuesArray[index]
if (current == value) {
return index
}
}
} else {
return value
}
return -1
}
companion object {
const val TAG = "SettingsDialogFragment"
const val TYPE_RESET_SETTING = -1
const val TITLE = "Title"
const val TYPE = "Type"
const val POSITION = "Position"
fun newInstance(
settingsViewModel: SettingsViewModel,
clickedItem: SettingsItem,
type: Int,
position: Int
): SettingsDialogFragment {
when (type) {
SettingsItem.TYPE_HEADER,
SettingsItem.TYPE_SWITCH,
SettingsItem.TYPE_SUBMENU,
SettingsItem.TYPE_DATETIME_SETTING,
SettingsItem.TYPE_RUNNABLE ->
throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!")
SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress(
(clickedItem as SliderSetting).getSelectedValue().toFloat()
)
}
settingsViewModel.clickedItem = clickedItem
val args = Bundle()
args.putInt(TYPE, type)
args.putInt(POSITION, position)
val fragment = SettingsDialogFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -13,20 +13,17 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
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
import org.yuzu.yuzu_emu.utils.collect
class SettingsFragment : Fragment() {
private lateinit var presenter: SettingsFragmentPresenter
@ -45,6 +42,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(
@ -56,9 +59,9 @@ class SettingsFragment : Fragment() {
return binding.root
}
// This is using the correct scope, lint is just acting up
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
@SuppressLint("NotifyDataSetChanged")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
settingsAdapter = SettingsAdapter(this, requireContext())
presenter = SettingsFragmentPresenter(
settingsViewModel,
@ -71,7 +74,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
@ -82,16 +95,37 @@ class SettingsFragment : Fragment() {
settingsViewModel.setShouldNavigateBack(true)
}
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
settingsViewModel.shouldReloadSettingsList.collectLatest {
if (it) {
settingsViewModel.setShouldReloadSettingsList(false)
presenter.loadSettingsList()
}
}
}
settingsViewModel.shouldReloadSettingsList.collect(
viewLifecycleOwner,
resetState = { settingsViewModel.setShouldReloadSettingsList(false) }
) { if (it) presenter.loadSettingsList() }
settingsViewModel.adapterItemChanged.collect(
viewLifecycleOwner,
resetState = { settingsViewModel.setAdapterItemChanged(-1) }
) { if (it != -1) settingsAdapter?.notifyItemChanged(it) }
settingsViewModel.datasetChanged.collect(
viewLifecycleOwner,
resetState = { settingsViewModel.setDatasetChanged(false) }
) { if (it) settingsAdapter?.notifyDataSetChanged() }
settingsViewModel.reloadListAndNotifyDataset.collect(
viewLifecycleOwner,
resetState = { settingsViewModel.setReloadListAndNotifyDataset(false) }
) { if (it) presenter.loadSettingsList(true) }
settingsViewModel.shouldShowResetInputDialog.collect(
viewLifecycleOwner,
resetState = { settingsViewModel.setShouldShowResetInputDialog(false) }
) {
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)
}
}
@ -115,6 +149,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

View File

@ -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
)
)
}

View File

@ -0,0 +1,183 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.divider.MaterialDividerItemDecoration
import com.google.android.material.transition.MaterialSharedAxis
import info.debatty.java.stringsimilarity.Cosine
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.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import org.yuzu.yuzu_emu.utils.collect
class SettingsSearchFragment : Fragment() {
private var _binding: FragmentSettingsSearchBinding? = null
private val binding get() = _binding!!
private var settingsAdapter: SettingsAdapter? = null
private val settingsViewModel: SettingsViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSettingsSearchBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (savedInstanceState != null) {
binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
}
settingsAdapter = SettingsAdapter(this, requireContext())
val dividerDecoration = MaterialDividerItemDecoration(
requireContext(),
LinearLayoutManager.VERTICAL
)
dividerDecoration.isLastItemDecorated = false
binding.settingsList.apply {
adapter = settingsAdapter
layoutManager = LinearLayoutManager(requireContext())
addItemDecoration(dividerDecoration)
}
focusSearch()
binding.backButton.setOnClickListener { settingsViewModel.setShouldNavigateBack(true) }
binding.searchBackground.setOnClickListener { focusSearch() }
binding.clearButton.setOnClickListener { binding.searchText.setText("") }
binding.searchText.doOnTextChanged { _, _, _, _ ->
search()
binding.settingsList.smoothScrollToPosition(0)
}
settingsViewModel.shouldReloadSettingsList.collect(viewLifecycleOwner) {
if (it) {
settingsViewModel.setShouldReloadSettingsList(false)
search()
}
}
search()
setInsets()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(SEARCH_TEXT, binding.searchText.text.toString())
}
private fun search() {
val searchTerm = binding.searchText.text.toString().lowercase()
binding.clearButton.setVisible(visible = searchTerm.isNotEmpty(), gone = false)
if (searchTerm.isEmpty()) {
binding.noResultsView.setVisible(visible = false, gone = false)
settingsAdapter?.submitList(emptyList())
return
}
val baseList = SettingsItem.settingsItems
val similarityAlgorithm = if (searchTerm.length > 2) Cosine() else Cosine(1)
val sortedList: List<SettingsItem> = baseList.mapNotNull { item ->
val title = item.value.title.lowercase()
val similarity = similarityAlgorithm.similarity(searchTerm, title)
if (similarity > 0.08) {
Pair(similarity, item)
} else {
null
}
}.sortedByDescending { it.first }.mapNotNull {
val item = it.second.value
val pairedSettingKey = item.setting.pairedSettingKey
val optionalSetting: SettingsItem? = if (pairedSettingKey.isNotEmpty()) {
val pairedSettingValue = NativeConfig.getBoolean(pairedSettingKey, false)
if (pairedSettingValue) it.second.value else null
} else {
it.second.value
}
optionalSetting
}
settingsAdapter?.submitList(sortedList)
binding.noResultsView.setVisible(visible = sortedList.isEmpty(), gone = false)
}
private fun focusSearch() {
binding.searchText.requestFocus()
val imm = requireActivity()
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT)
}
private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { _: View, windowInsets: WindowInsetsCompat ->
val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med)
val sideMargin = resources.getDimensionPixelSize(R.dimen.spacing_medlarge)
val topMargin = resources.getDimensionPixelSize(R.dimen.spacing_chip)
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right
binding.settingsList.updatePadding(bottom = barInsets.bottom + extraListSpacing)
binding.frameSearch.updatePadding(
left = leftInsets + sideMargin,
top = barInsets.top + topMargin,
right = rightInsets + sideMargin
)
binding.noResultsView.updatePadding(
left = leftInsets,
right = rightInsets,
bottom = barInsets.bottom
)
binding.settingsList.updateMargins(
left = leftInsets + sideMargin,
right = rightInsets + sideMargin
)
binding.divider.updateMargins(
left = leftInsets + sideMargin,
right = rightInsets + sideMargin
)
windowInsets
}
companion object {
const val SEARCH_TEXT = "SearchText"
}
}

View File

@ -0,0 +1,112 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
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)
val shouldNavigateBack: StateFlow<Boolean> get() = _shouldNavigateBack
private val _shouldNavigateBack = MutableStateFlow(false)
val shouldShowResetSettingsDialog: StateFlow<Boolean> get() = _shouldShowResetSettingsDialog
private val _shouldShowResetSettingsDialog = MutableStateFlow(false)
val shouldReloadSettingsList: StateFlow<Boolean> get() = _shouldReloadSettingsList
private val _shouldReloadSettingsList = MutableStateFlow(false)
val sliderProgress: StateFlow<Int> get() = _sliderProgress
private val _sliderProgress = MutableStateFlow(-1)
val sliderTextValue: StateFlow<String> get() = _sliderTextValue
private val _sliderTextValue = MutableStateFlow("")
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
}
fun setShouldNavigateBack(value: Boolean) {
_shouldNavigateBack.value = value
}
fun setShouldShowResetSettingsDialog(value: Boolean) {
_shouldShowResetSettingsDialog.value = value
}
fun setShouldReloadSettingsList(value: Boolean) {
_shouldReloadSettingsList.value = value
}
fun setSliderTextValue(value: Float, units: String) {
_sliderProgress.value = value.toInt()
_sliderTextValue.value = String.format(
YuzuApplication.appContext.getString(R.string.value_with_units),
value.toInt().toString(),
units
)
}
fun setSliderProgress(value: Float) {
_sliderProgress.value = value.toInt()
}
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
}
}

View File

@ -14,6 +14,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
@ -21,28 +22,19 @@ 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.textSettingDescription.visibility = View.VISIBLE
} else {
binding.textSettingDescription.visibility = View.GONE
}
binding.textSettingValue.visibility = View.VISIBLE
binding.textSettingName.text = item.title
binding.textSettingDescription.setVisible(item.description.isNotEmpty())
binding.textSettingDescription.text = item.description
binding.textSettingValue.setVisible(true)
val epochTime = setting.getValue()
val instant = Instant.ofEpochMilli(epochTime * 1000)
val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
binding.textSettingValue.text = dateFormatter.format(zonedTime)
binding.buttonClear.visibility = if (setting.setting.global ||
!NativeConfig.isPerGameConfigLoaded()
) {
View.GONE
} else {
View.VISIBLE
}
binding.buttonClear.setVisible(
!setting.setting.global || NativeConfig.isPerGameConfigLoaded()
)
binding.buttonClear.setOnClickListener {
adapter.onClearClick(setting, bindingAdapterPosition)
}

View File

@ -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) {

View File

@ -0,0 +1,34 @@
// 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
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
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.setVisible(false)
binding.buttonClear.setVisible(false)
binding.icon.setVisible(false)
binding.buttonClear.setVisible(false)
}
override fun onClick(clicked: View) =
adapter.onInputProfileClick(setting, bindingAdapterPosition)
override fun onLongClick(clicked: View): Boolean = false
}

View File

@ -0,0 +1,60 @@
// 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
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
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()
when (item) {
is AnalogInputSetting -> {
val param = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
binding.buttonOptions.setVisible(
param.get("engine", "") == "analog_from_button" ||
param.has("axis_x") || param.has("axis_y")
)
}
is ButtonInputSetting -> {
val param = NativeInput.getButtonParam(item.playerIndex, item.nativeButton)
binding.buttonOptions.setVisible(
param.has("code") || param.has("button") || param.has("hat") ||
param.has("axis")
)
}
is ModifierInputSetting -> {
val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
binding.buttonOptions.setVisible(params.has("modifier"))
}
}
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)
}

View File

@ -5,11 +5,11 @@ 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
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
@ -17,34 +17,28 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
override fun bind(item: SettingsItem) {
setting = item as RunnableSetting
if (item.iconId != 0) {
binding.icon.visibility = View.VISIBLE
binding.icon.setVisible(setting.iconId != 0)
if (setting.iconId != 0) {
binding.icon.setImageDrawable(
ResourcesCompat.getDrawable(
binding.icon.resources,
item.iconId,
setting.iconId,
binding.icon.context.theme
)
)
} else {
binding.icon.visibility = View.GONE
}
binding.textSettingName.setText(item.nameId)
if (item.descriptionId != 0) {
binding.textSettingDescription.setText(item.descriptionId)
binding.textSettingDescription.visibility = View.VISIBLE
} else {
binding.textSettingDescription.visibility = View.GONE
}
binding.textSettingValue.visibility = View.GONE
binding.buttonClear.visibility = View.GONE
binding.textSettingName.text = setting.title
binding.textSettingDescription.setVisible(setting.description.isNotEmpty())
binding.textSettingDescription.text = item.description
binding.textSettingValue.setVisible(false)
binding.buttonClear.setVisible(false)
setStyle(setting.isEditable, binding)
}
override fun onClick(clicked: View) {
if (!setting.isRuntimeRunnable && !NativeLibrary.isRunning()) {
if (setting.isRunnable) {
setting.runnable.invoke()
}
}

View File

@ -5,11 +5,13 @@ 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
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
@ -17,40 +19,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.textSettingDescription.visibility = View.VISIBLE
} else {
binding.textSettingDescription.visibility = View.GONE
}
binding.textSettingName.text = setting.title
binding.textSettingDescription.setVisible(item.description.isNotEmpty())
binding.textSettingDescription.text = item.description
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
binding.textSettingValue.setVisible(true)
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())
}
}
binding.buttonClear.visibility = if (setting.setting.global ||
!NativeConfig.isPerGameConfigLoaded()
) {
View.GONE
} else {
View.VISIBLE
if (binding.textSettingValue.text.isEmpty()) {
binding.textSettingValue.setVisible(false)
}
binding.buttonClear.setVisible(
!setting.setting.global || NativeConfig.isPerGameConfigLoaded()
)
binding.buttonClear.setOnClickListener {
adapter.onClearClick(setting, bindingAdapterPosition)
}
@ -63,16 +63,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
)
}
}
}

View File

@ -10,6 +10,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
@ -17,27 +18,19 @@ 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.textSettingDescription.visibility = View.VISIBLE
} else {
binding.textSettingDescription.visibility = View.GONE
}
binding.textSettingValue.visibility = View.VISIBLE
binding.textSettingName.text = setting.title
binding.textSettingDescription.setVisible(item.description.isNotEmpty())
binding.textSettingDescription.text = setting.description
binding.textSettingValue.setVisible(true)
binding.textSettingValue.text = String.format(
binding.textSettingValue.context.getString(R.string.value_with_units),
setting.getSelectedValue(),
setting.units
)
binding.buttonClear.visibility = if (setting.setting.global ||
!NativeConfig.isPerGameConfigLoaded()
) {
View.GONE
} else {
View.VISIBLE
}
binding.buttonClear.setVisible(
!setting.setting.global || NativeConfig.isPerGameConfigLoaded()
)
binding.buttonClear.setOnClickListener {
adapter.onClearClick(setting, bindingAdapterPosition)
}

View File

@ -9,39 +9,34 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.model.view.SubmenuSetting
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
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) {
binding.icon.visibility = View.VISIBLE
setting = item as SubmenuSetting
binding.icon.setVisible(setting.iconId != 0)
if (setting.iconId != 0) {
binding.icon.setImageDrawable(
ResourcesCompat.getDrawable(
binding.icon.resources,
item.iconId,
setting.iconId,
binding.icon.context.theme
)
)
} else {
binding.icon.visibility = View.GONE
}
binding.textSettingName.setText(item.nameId)
if (item.descriptionId != 0) {
binding.textSettingDescription.setText(item.descriptionId)
binding.textSettingDescription.visibility = View.VISIBLE
} else {
binding.textSettingDescription.visibility = View.GONE
}
binding.textSettingValue.visibility = View.GONE
binding.buttonClear.visibility = View.GONE
binding.textSettingName.text = setting.title
binding.textSettingDescription.setVisible(setting.description.isNotEmpty())
binding.textSettingDescription.text = setting.description
binding.textSettingValue.setVisible(false)
binding.buttonClear.setVisible(false)
}
override fun onClick(clicked: View) {
adapter.onSubmenuClick(item)
adapter.onSubmenuClick(setting)
}
override fun onLongClick(clicked: View): Boolean {

View File

@ -10,6 +10,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
@ -18,28 +19,19 @@ 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.textSettingDescription.visibility = View.VISIBLE
} else {
binding.textSettingDescription.text = ""
binding.textSettingDescription.visibility = View.GONE
}
binding.textSettingName.text = setting.title
binding.textSettingDescription.setVisible(setting.description.isNotEmpty())
binding.textSettingDescription.text = setting.description
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 ||
!NativeConfig.isPerGameConfigLoaded()
) {
View.GONE
} else {
View.VISIBLE
}
binding.buttonClear.setVisible(
!setting.setting.global || NativeConfig.isPerGameConfigLoaded()
)
binding.buttonClear.setOnClickListener {
adapter.onClearClick(setting, bindingAdapterPosition)
}

View File

@ -3,7 +3,6 @@
package org.yuzu.yuzu_emu.fragments
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
@ -16,9 +15,6 @@ import androidx.core.view.updatePadding
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
@ -32,6 +28,7 @@ import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.utils.AddonUtil
import org.yuzu.yuzu_emu.utils.FileUtil.copyFilesTo
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import org.yuzu.yuzu_emu.utils.collect
import java.io.File
class AddonsFragment : Fragment() {
@ -60,8 +57,6 @@ class AddonsFragment : Fragment() {
return binding.root
}
// This is using the correct scope, lint is just acting up
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = false, animated = false)
@ -78,57 +73,41 @@ class AddonsFragment : Fragment() {
adapter = AddonAdapter(addonViewModel)
}
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
addonViewModel.addonList.collect {
(binding.listAddons.adapter as AddonAdapter).submitList(it)
}
}
addonViewModel.addonList.collect(viewLifecycleOwner) {
(binding.listAddons.adapter as AddonAdapter).submitList(it)
}
addonViewModel.showModInstallPicker.collect(
viewLifecycleOwner,
resetState = { addonViewModel.showModInstallPicker(false) }
) { if (it) installAddon.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) }
addonViewModel.showModNoticeDialog.collect(
viewLifecycleOwner,
resetState = { addonViewModel.showModNoticeDialog(false) }
) {
if (it) {
MessageDialogFragment.newInstance(
requireActivity(),
titleId = R.string.addon_notice,
descriptionId = R.string.addon_notice_description,
dismissible = false,
positiveAction = { addonViewModel.showModInstallPicker(true) },
negativeAction = {},
negativeButtonTitleId = R.string.close
).show(parentFragmentManager, MessageDialogFragment.TAG)
}
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
addonViewModel.showModInstallPicker.collect {
if (it) {
installAddon.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
addonViewModel.showModInstallPicker(false)
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
addonViewModel.showModNoticeDialog.collect {
if (it) {
MessageDialogFragment.newInstance(
requireActivity(),
titleId = R.string.addon_notice,
descriptionId = R.string.addon_notice_description,
dismissible = false,
positiveAction = { addonViewModel.showModInstallPicker(true) },
negativeAction = {},
negativeButtonTitleId = R.string.close
).show(parentFragmentManager, MessageDialogFragment.TAG)
addonViewModel.showModNoticeDialog(false)
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
addonViewModel.addonToDelete.collect {
if (it != null) {
MessageDialogFragment.newInstance(
requireActivity(),
titleId = R.string.confirm_uninstall,
descriptionId = R.string.confirm_uninstall_description,
positiveAction = { addonViewModel.onDeleteAddon(it) },
negativeAction = {}
).show(parentFragmentManager, MessageDialogFragment.TAG)
addonViewModel.setAddonToDelete(null)
}
}
}
}
addonViewModel.addonToDelete.collect(
viewLifecycleOwner,
resetState = { addonViewModel.setAddonToDelete(null) }
) {
if (it != null) {
MessageDialogFragment.newInstance(
requireActivity(),
titleId = R.string.confirm_uninstall,
descriptionId = R.string.confirm_uninstall_description,
positiveAction = { addonViewModel.onDeleteAddon(it) },
negativeAction = {}
).show(parentFragmentManager, MessageDialogFragment.TAG)
}
}

View File

@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
class CoreErrorDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
MaterialAlertDialogBuilder(requireActivity())
.setTitle(requireArguments().getString(TITLE))
.setMessage(requireArguments().getString(MESSAGE))
.setPositiveButton(R.string.continue_button, null)
.setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int ->
NativeLibrary.coreErrorAlertResult = false
synchronized(NativeLibrary.coreErrorAlertLock) {
NativeLibrary.coreErrorAlertLock.notify()
}
}
.create()
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
NativeLibrary.coreErrorAlertResult = true
synchronized(NativeLibrary.coreErrorAlertLock) { NativeLibrary.coreErrorAlertLock.notify() }
}
companion object {
const val TITLE = "Title"
const val MESSAGE = "Message"
fun newInstance(title: String, message: String): CoreErrorDialogFragment {
val frag = CoreErrorDialogFragment()
val args = Bundle()
args.putString(TITLE, title)
args.putString(MESSAGE, message)
frag.arguments = args
return frag
}
}
}

View File

@ -3,7 +3,6 @@
package org.yuzu.yuzu_emu.fragments
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -14,9 +13,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.GridLayoutManager
@ -35,6 +31,7 @@ import org.yuzu.yuzu_emu.utils.FileUtil
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import org.yuzu.yuzu_emu.utils.collect
import java.io.File
import java.io.IOException
@ -63,8 +60,6 @@ class DriverManagerFragment : Fragment() {
return binding.root
}
// This is using the correct scope, lint is just acting up
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = false, animated = true)
@ -89,15 +84,8 @@ class DriverManagerFragment : Fragment() {
}
}
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
driverViewModel.showClearButton.collect {
binding.toolbarDrivers.menu
.findItem(R.id.menu_driver_use_global).isVisible = it
}
}
}
driverViewModel.showClearButton.collect(viewLifecycleOwner) {
binding.toolbarDrivers.menu.findItem(R.id.menu_driver_use_global).isVisible = it
}
}

View File

@ -10,14 +10,11 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.utils.collect
class DriversLoadingDialogFragment : DialogFragment() {
private val driverViewModel: DriverViewModel by activityViewModels()
@ -44,13 +41,7 @@ class DriversLoadingDialogFragment : DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
driverViewModel.isInteractionAllowed.collect { if (it) dismiss() }
}
}
}
driverViewModel.isInteractionAllowed.collect(viewLifecycleOwner) { if (it) dismiss() }
}
companion object {

View File

@ -32,9 +32,6 @@ import androidx.drawerlayout.widget.DrawerLayout
import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import androidx.window.layout.FoldingFeature
@ -42,9 +39,6 @@ import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.slider.Slider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
@ -63,6 +57,7 @@ import org.yuzu.yuzu_emu.model.EmulationViewModel
import org.yuzu.yuzu_emu.overlay.model.OverlayControl
import org.yuzu.yuzu_emu.overlay.model.OverlayLayout
import org.yuzu.yuzu_emu.utils.*
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import java.lang.NullPointerException
class EmulationFragment : Fragment(), SurfaceHolder.Callback {
@ -90,14 +85,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
if (context is EmulationActivity) {
emulationActivity = context
NativeLibrary.setEmulationActivity(context)
lifecycleScope.launch(Dispatchers.Main) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
WindowInfoTracker.getOrCreate(context)
.windowLayoutInfo(context)
.collect { updateFoldableLayout(context, it) }
}
}
} else {
throw IllegalStateException("EmulationFragment must have EmulationActivity parent")
}
@ -168,8 +155,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
return binding.root
}
// This is using the correct scope, lint is just acting up
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (requireActivity().isFinishing) {
@ -277,6 +262,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
@ -341,129 +335,86 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
binding.loadingTitle.isSelected = true
binding.loadingText.isSelected = true
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
WindowInfoTracker.getOrCreate(requireContext())
.windowLayoutInfo(requireActivity())
.collect {
updateFoldableLayout(requireActivity() as EmulationActivity, it)
}
}
WindowInfoTracker.getOrCreate(requireContext())
.windowLayoutInfo(requireActivity()).collect(viewLifecycleOwner) {
updateFoldableLayout(requireActivity() as EmulationActivity, it)
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
emulationViewModel.shaderProgress.collectLatest {
if (it > 0 && it != emulationViewModel.totalShaders.value) {
binding.loadingProgressIndicator.isIndeterminate = false
emulationViewModel.shaderProgress.collect(viewLifecycleOwner) {
if (it > 0 && it != emulationViewModel.totalShaders.value) {
binding.loadingProgressIndicator.isIndeterminate = false
if (it < binding.loadingProgressIndicator.max) {
binding.loadingProgressIndicator.progress = it
}
}
if (it < binding.loadingProgressIndicator.max) {
binding.loadingProgressIndicator.progress = it
}
}
if (it == emulationViewModel.totalShaders.value) {
binding.loadingText.setText(R.string.loading)
binding.loadingProgressIndicator.isIndeterminate = true
}
}
}
if (it == emulationViewModel.totalShaders.value) {
binding.loadingText.setText(R.string.loading)
binding.loadingProgressIndicator.isIndeterminate = true
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
emulationViewModel.totalShaders.collectLatest {
binding.loadingProgressIndicator.max = it
}
}
}
emulationViewModel.totalShaders.collect(viewLifecycleOwner) {
binding.loadingProgressIndicator.max = it
}
emulationViewModel.shaderMessage.collect(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.loadingText.text = it
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
emulationViewModel.shaderMessage.collectLatest {
if (it.isNotEmpty()) {
binding.loadingText.text = it
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
driverViewModel.isInteractionAllowed.collect {
if (it) {
startEmulation()
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
emulationViewModel.emulationStarted.collectLatest {
if (it) {
binding.drawerLayout.setDrawerLockMode(IntSetting.LOCK_DRAWER.getInt())
ViewUtils.showView(binding.surfaceInputOverlay)
ViewUtils.hideView(binding.loadingIndicator)
}
emulationState.updateSurface()
emulationViewModel.emulationStarted.collect(viewLifecycleOwner) {
if (it) {
binding.drawerLayout.setDrawerLockMode(IntSetting.LOCK_DRAWER.getInt())
ViewUtils.showView(binding.surfaceInputOverlay)
ViewUtils.hideView(binding.loadingIndicator)
// Setup overlays
updateShowFpsOverlay()
updateThermalOverlay()
}
}
}
emulationState.updateSurface()
// Setup overlays
updateShowFpsOverlay()
updateThermalOverlay()
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
emulationViewModel.isEmulationStopping.collectLatest {
if (it) {
binding.loadingText.setText(R.string.shutting_down)
ViewUtils.showView(binding.loadingIndicator)
ViewUtils.hideView(binding.inputContainer)
ViewUtils.hideView(binding.showFpsText)
}
}
}
}
emulationViewModel.isEmulationStopping.collect(viewLifecycleOwner) {
if (it) {
binding.loadingText.setText(R.string.shutting_down)
ViewUtils.showView(binding.loadingIndicator)
ViewUtils.hideView(binding.inputContainer)
ViewUtils.hideView(binding.showFpsText)
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
emulationViewModel.drawerOpen.collect {
if (it) {
binding.drawerLayout.open()
binding.inGameMenu.requestFocus()
} else {
binding.drawerLayout.close()
}
}
}
}
emulationViewModel.drawerOpen.collect(viewLifecycleOwner) {
if (it) {
binding.drawerLayout.open()
binding.inGameMenu.requestFocus()
} else {
binding.drawerLayout.close()
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
emulationViewModel.programChanged.collect {
if (it != 0) {
emulationViewModel.setEmulationStarted(false)
binding.drawerLayout.close()
binding.drawerLayout
.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
ViewUtils.hideView(binding.surfaceInputOverlay)
ViewUtils.showView(binding.loadingIndicator)
}
}
}
}
emulationViewModel.programChanged.collect(viewLifecycleOwner) {
if (it != 0) {
emulationViewModel.setEmulationStarted(false)
binding.drawerLayout.close()
binding.drawerLayout
.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
ViewUtils.hideView(binding.surfaceInputOverlay)
ViewUtils.showView(binding.loadingIndicator)
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
emulationViewModel.emulationStopped.collect {
if (it && emulationViewModel.programChanged.value != -1) {
if (perfStatsUpdater != null) {
perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!)
}
emulationState.changeProgram(emulationViewModel.programChanged.value)
emulationViewModel.setProgramChanged(-1)
emulationViewModel.setEmulationStopped(false)
}
}
}
emulationViewModel.emulationStopped.collect(viewLifecycleOwner) {
if (it && emulationViewModel.programChanged.value != -1) {
if (perfStatsUpdater != null) {
perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!)
}
emulationState.changeProgram(emulationViewModel.programChanged.value)
emulationViewModel.setProgramChanged(-1)
emulationViewModel.setEmulationStopped(false)
}
}
driverViewModel.isInteractionAllowed.collect(viewLifecycleOwner) {
if (it) startEmulation()
}
}
private fun startEmulation(programIndex: Int = 0) {
@ -491,14 +442,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
binding.drawerLayout.close()
}
if (showInputOverlay) {
binding.surfaceInputOverlay.visibility = View.INVISIBLE
binding.surfaceInputOverlay.setVisible(visible = false, gone = false)
}
} else {
if (showInputOverlay && emulationViewModel.emulationStarted.value) {
binding.surfaceInputOverlay.visibility = View.VISIBLE
} else {
binding.surfaceInputOverlay.visibility = View.INVISIBLE
}
binding.surfaceInputOverlay.setVisible(
showInputOverlay && emulationViewModel.emulationStarted.value
)
if (!isInFoldableLayout) {
if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
binding.surfaceInputOverlay.layout = OverlayLayout.Portrait
@ -535,7 +484,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
private fun updateShowFpsOverlay() {
if (BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean()) {
val showOverlay = BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean()
binding.showFpsText.setVisible(showOverlay)
if (showOverlay) {
val SYSTEM_FPS = 0
val FPS = 1
val FRAMETIME = 2
@ -555,17 +506,17 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
}
perfStatsUpdateHandler.post(perfStatsUpdater!!)
binding.showFpsText.visibility = View.VISIBLE
} else {
if (perfStatsUpdater != null) {
perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!)
}
binding.showFpsText.visibility = View.GONE
}
}
private fun updateThermalOverlay() {
if (BooleanSetting.SHOW_THERMAL_OVERLAY.getBoolean()) {
val showOverlay = BooleanSetting.SHOW_THERMAL_OVERLAY.getBoolean()
binding.showThermalsText.setVisible(showOverlay)
if (showOverlay) {
thermalStatsUpdater = {
if (emulationViewModel.emulationStarted.value &&
!emulationViewModel.isEmulationStopping.value
@ -587,12 +538,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
}
thermalStatsUpdateHandler.post(thermalStatsUpdater!!)
binding.showThermalsText.visibility = View.VISIBLE
} else {
if (thermalStatsUpdater != null) {
thermalStatsUpdateHandler.removeCallbacks(thermalStatsUpdater!!)
}
binding.showThermalsText.visibility = View.GONE
}
}
@ -861,12 +810,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
}
}
binding.doneControlConfig.visibility = View.VISIBLE
binding.doneControlConfig.setVisible(false)
binding.surfaceInputOverlay.setIsInEditMode(true)
}
private fun stopConfiguringControls() {
binding.doneControlConfig.visibility = View.GONE
binding.doneControlConfig.setVisible(false)
binding.surfaceInputOverlay.setIsInEditMode(false)
// Unlock the orientation if it was locked for editing
if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == EmulationOrientation.Unspecified.int) {

View File

@ -13,9 +13,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
@ -27,6 +24,7 @@ import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import org.yuzu.yuzu_emu.utils.collect
class GameFoldersFragment : Fragment() {
private var _binding: FragmentFoldersBinding? = null
@ -70,12 +68,8 @@ class GameFoldersFragment : Fragment() {
adapter = FolderAdapter(requireActivity(), gamesViewModel)
}
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
gamesViewModel.folders.collect {
(binding.listFolders.adapter as FolderAdapter).submitList(it)
}
}
gamesViewModel.folders.collect(viewLifecycleOwner) {
(binding.listFolders.adapter as FolderAdapter).submitList(it)
}
val mainActivity = requireActivity() as MainActivity

View File

@ -27,6 +27,7 @@ import org.yuzu.yuzu_emu.databinding.FragmentGameInfoBinding
import org.yuzu.yuzu_emu.model.GameVerificationResult
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.utils.GameMetadata
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
class GameInfoFragment : Fragment() {
@ -85,7 +86,7 @@ class GameInfoFragment : Fragment() {
copyToClipboard(getString(R.string.developer), args.game.developer)
}
} else {
developer.visibility = View.GONE
developer.setVisible(false)
}
version.setHint(R.string.version)

View File

@ -3,11 +3,9 @@
package org.yuzu.yuzu_emu.fragments
import android.annotation.SuppressLint
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.os.Bundle
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -18,9 +16,7 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.GridLayoutManager
@ -46,7 +42,9 @@ import org.yuzu.yuzu_emu.utils.FileUtil
import org.yuzu.yuzu_emu.utils.GameIconUtils
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import org.yuzu.yuzu_emu.utils.MemoryUtil
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import org.yuzu.yuzu_emu.utils.collect
import java.io.BufferedOutputStream
import java.io.File
@ -76,8 +74,6 @@ class GamePropertiesFragment : Fragment() {
return binding.root
}
// This is using the correct scope, lint is just acting up
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = false, animated = true)
@ -107,13 +103,7 @@ class GamePropertiesFragment : Fragment() {
GameIconUtils.loadGameIcon(args.game, binding.imageGameScreen)
binding.title.text = args.game.title
binding.title.postDelayed(
{
binding.title.ellipsize = TextUtils.TruncateAt.MARQUEE
binding.title.isSelected = true
},
3000
)
binding.title.marquee()
binding.buttonStart.setOnClickListener {
LaunchGameDialogFragment.newInstance(args.game)
@ -122,28 +112,14 @@ class GamePropertiesFragment : Fragment() {
reloadList()
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
homeViewModel.openImportSaves.collect {
if (it) {
importSaves.launch(arrayOf("application/zip"))
homeViewModel.setOpenImportSaves(false)
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
homeViewModel.reloadPropertiesList.collect {
if (it) {
reloadList()
homeViewModel.reloadPropertiesList(false)
}
}
}
}
}
homeViewModel.openImportSaves.collect(
viewLifecycleOwner,
resetState = { homeViewModel.setOpenImportSaves(false) }
) { if (it) importSaves.launch(arrayOf("application/zip")) }
homeViewModel.reloadPropertiesList.collect(
viewLifecycleOwner,
resetState = { homeViewModel.reloadPropertiesList(false) }
) { if (it) reloadList() }
setInsets()
}

View File

@ -89,6 +89,20 @@ class HomeSettingsFragment : Fragment() {
}
)
)
add(
HomeSetting(
R.string.preferences_controls,
R.string.preferences_controls_description,
R.drawable.ic_controller,
{
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
null,
Settings.MenuTag.SECTION_INPUT
)
binding.root.findNavController().navigate(action)
}
)
)
add(
HomeSetting(
R.string.gpu_driver_manager,

View File

@ -14,9 +14,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
@ -35,6 +32,7 @@ import org.yuzu.yuzu_emu.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.FileUtil
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import org.yuzu.yuzu_emu.utils.collect
import java.io.BufferedOutputStream
import java.io.File
import java.math.BigInteger
@ -75,14 +73,10 @@ class InstallableFragment : Fragment() {
binding.root.findNavController().popBackStack()
}
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
homeViewModel.openImportSaves.collect {
if (it) {
importSaves.launch(arrayOf("application/zip"))
homeViewModel.setOpenImportSaves(false)
}
}
homeViewModel.openImportSaves.collect(viewLifecycleOwner) {
if (it) {
importSaves.launch(arrayOf("application/zip"))
homeViewModel.setOpenImportSaves(false)
}
}

View File

@ -13,15 +13,13 @@ import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
import org.yuzu.yuzu_emu.model.TaskViewModel
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.utils.collect
class ProgressDialogFragment : DialogFragment() {
private val taskViewModel: TaskViewModel by activityViewModels()
@ -64,72 +62,50 @@ class ProgressDialogFragment : DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.message.isSelected = true
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
taskViewModel.isComplete.collect {
if (it) {
dismiss()
when (val result = taskViewModel.result.value) {
is String -> Toast.makeText(
requireContext(),
result,
Toast.LENGTH_LONG
).show()
taskViewModel.isComplete.collect(viewLifecycleOwner) {
if (it) {
dismiss()
when (val result = taskViewModel.result.value) {
is String -> Toast.makeText(
requireContext(),
result,
Toast.LENGTH_LONG
).show()
is MessageDialogFragment -> result.show(
requireActivity().supportFragmentManager,
MessageDialogFragment.TAG
)
is MessageDialogFragment -> result.show(
requireActivity().supportFragmentManager,
MessageDialogFragment.TAG
)
else -> {
// Do nothing
}
}
taskViewModel.clear()
}
else -> {
// Do nothing
}
}
taskViewModel.clear()
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
taskViewModel.cancelled.collect {
if (it) {
dialog?.setTitle(R.string.cancelling)
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
taskViewModel.progress.collect {
if (it != 0.0) {
binding.progressBar.apply {
isIndeterminate = false
progress = (
(it / taskViewModel.maxProgress.value) *
PROGRESS_BAR_RESOLUTION
).toInt()
min = 0
max = PROGRESS_BAR_RESOLUTION
}
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
taskViewModel.message.collect {
if (it.isEmpty()) {
binding.message.visibility = View.GONE
} else {
binding.message.visibility = View.VISIBLE
binding.message.text = it
}
}
}
taskViewModel.cancelled.collect(viewLifecycleOwner) {
if (it) {
dialog?.setTitle(R.string.cancelling)
}
}
taskViewModel.progress.collect(viewLifecycleOwner) {
if (it != 0.0) {
binding.progressBar.apply {
isIndeterminate = false
progress = (
(it / taskViewModel.maxProgress.value) *
PROGRESS_BAR_RESOLUTION
).toInt()
min = 0
max = PROGRESS_BAR_RESOLUTION
}
}
}
taskViewModel.message.collect(viewLifecycleOwner) {
binding.message.setVisible(it.isNotEmpty())
binding.message.text = it
}
}
// By default, the ProgressDialog will immediately dismiss itself upon a button being pressed.

View File

@ -3,7 +3,6 @@
package org.yuzu.yuzu_emu.fragments
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
@ -18,14 +17,9 @@ import androidx.core.view.updatePadding
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.PreferenceManager
import info.debatty.java.stringsimilarity.Jaccard
import info.debatty.java.stringsimilarity.JaroWinkler
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.util.Locale
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
@ -35,6 +29,8 @@ import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager
import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.utils.collect
class SearchFragment : Fragment() {
private var _binding: FragmentSearchBinding? = null
@ -58,8 +54,6 @@ class SearchFragment : Fragment() {
return binding.root
}
// This is using the correct scope, lint is just acting up
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = true, animated = true)
@ -81,42 +75,18 @@ class SearchFragment : Fragment() {
binding.chipGroup.setOnCheckedStateChangeListener { _, _ -> filterAndSearch() }
binding.searchText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int ->
if (text.toString().isNotEmpty()) {
binding.clearButton.visibility = View.VISIBLE
} else {
binding.clearButton.visibility = View.INVISIBLE
}
binding.clearButton.setVisible(text.toString().isNotEmpty())
filterAndSearch()
}
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
gamesViewModel.searchFocused.collect {
if (it) {
focusSearch()
gamesViewModel.setSearchFocused(false)
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
gamesViewModel.games.collectLatest { filterAndSearch() }
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
gamesViewModel.searchedGames.collect {
(binding.gridGamesSearch.adapter as GameAdapter).submitList(it)
if (it.isEmpty()) {
binding.noResultsView.visibility = View.VISIBLE
} else {
binding.noResultsView.visibility = View.GONE
}
}
}
}
gamesViewModel.searchFocused.collect(
viewLifecycleOwner,
resetState = { gamesViewModel.setSearchFocused(false) }
) { if (it) focusSearch() }
gamesViewModel.games.collect(viewLifecycleOwner) { filterAndSearch() }
gamesViewModel.searchedGames.collect(viewLifecycleOwner) {
(binding.gridGamesSearch.adapter as GameAdapter).submitList(it)
binding.noResultsView.setVisible(it.isNotEmpty())
}
binding.clearButton.setOnClickListener { binding.searchText.setText("") }

View File

@ -4,7 +4,6 @@
package org.yuzu.yuzu_emu.fragments
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.os.Bundle
@ -23,9 +22,6 @@ import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.preference.PreferenceManager
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
@ -46,6 +42,8 @@ import org.yuzu.yuzu_emu.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.ViewUtils
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.utils.collect
class SetupFragment : Fragment() {
private var _binding: FragmentSetupBinding? = null
@ -77,8 +75,6 @@ class SetupFragment : Fragment() {
return binding.root
}
// This is using the correct scope, lint is just acting up
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
mainActivity = requireActivity() as MainActivity
@ -210,28 +206,14 @@ class SetupFragment : Fragment() {
)
}
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
homeViewModel.shouldPageForward.collect {
if (it) {
pageForward()
homeViewModel.setShouldPageForward(false)
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
homeViewModel.gamesDirSelected.collect {
if (it) {
gamesDirCallback.onStepCompleted()
homeViewModel.setGamesDirSelected(false)
}
}
}
}
}
homeViewModel.shouldPageForward.collect(
viewLifecycleOwner,
resetState = { homeViewModel.setShouldPageForward(false) }
) { if (it) pageForward() }
homeViewModel.gamesDirSelected.collect(
viewLifecycleOwner,
resetState = { homeViewModel.setGamesDirSelected(false) }
) { if (it) gamesDirCallback.onStepCompleted() }
binding.viewPager2.apply {
adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages)
@ -292,12 +274,8 @@ class SetupFragment : Fragment() {
val backIsVisible = savedInstanceState.getBoolean(KEY_BACK_VISIBILITY)
hasBeenWarned = savedInstanceState.getBooleanArray(KEY_HAS_BEEN_WARNED)!!
if (nextIsVisible) {
binding.buttonNext.visibility = View.VISIBLE
}
if (backIsVisible) {
binding.buttonBack.visibility = View.VISIBLE
}
binding.buttonNext.setVisible(nextIsVisible)
binding.buttonBack.setVisible(backIsVisible)
} else {
hasBeenWarned = BooleanArray(pages.size)
}

View File

@ -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
)

View File

@ -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

View File

@ -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
}

View File

@ -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) {

View File

@ -3,7 +3,6 @@
package org.yuzu.yuzu_emu.ui
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -14,19 +13,16 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.color.MaterialColors
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.adapters.GameAdapter
import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding
import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import org.yuzu.yuzu_emu.utils.collect
class GamesFragment : Fragment() {
private var _binding: FragmentGamesBinding? = null
@ -44,8 +40,6 @@ class GamesFragment : Fragment() {
return binding.root
}
// This is using the correct scope, lint is just acting up
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = true, animated = true)
@ -88,49 +82,28 @@ class GamesFragment : Fragment() {
}
}
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
gamesViewModel.isReloading.collect {
binding.swipeRefresh.isRefreshing = it
if (gamesViewModel.games.value.isEmpty() && !it) {
binding.noticeText.visibility = View.VISIBLE
} else {
binding.noticeText.visibility = View.INVISIBLE
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
gamesViewModel.games.collectLatest {
(binding.gridGames.adapter as GameAdapter).submitList(it)
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
gamesViewModel.shouldSwapData.collect {
if (it) {
(binding.gridGames.adapter as GameAdapter).submitList(
gamesViewModel.games.value
)
gamesViewModel.setShouldSwapData(false)
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
gamesViewModel.shouldScrollToTop.collect {
if (it) {
scrollToTop()
gamesViewModel.setShouldScrollToTop(false)
}
}
}
gamesViewModel.isReloading.collect(viewLifecycleOwner) {
binding.swipeRefresh.isRefreshing = it
binding.noticeText.setVisible(
visible = gamesViewModel.games.value.isEmpty() && !it,
gone = false
)
}
gamesViewModel.games.collect(viewLifecycleOwner) {
(binding.gridGames.adapter as GameAdapter).submitList(it)
}
gamesViewModel.shouldSwapData.collect(
viewLifecycleOwner,
resetState = { gamesViewModel.setShouldSwapData(false) }
) {
if (it) {
(binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value)
}
}
gamesViewModel.shouldScrollToTop.collect(
viewLifecycleOwner,
resetState = { gamesViewModel.setShouldScrollToTop(false) }
) { if (it) scrollToTop() }
setInsets()
}

View File

@ -19,9 +19,6 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
@ -30,7 +27,6 @@ import com.google.android.material.color.MaterialColors
import com.google.android.material.navigation.NavigationBarView
import java.io.File
import java.io.FilenameFilter
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
@ -47,6 +43,7 @@ import org.yuzu.yuzu_emu.model.InstallResult
import org.yuzu.yuzu_emu.model.TaskState
import org.yuzu.yuzu_emu.model.TaskViewModel
import org.yuzu.yuzu_emu.utils.*
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.util.zip.ZipEntry
@ -139,42 +136,23 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
// Prevents navigation from being drawn for a short time on recreation if set to hidden
if (!homeViewModel.navigationVisible.value.first) {
binding.navigationView.visibility = View.INVISIBLE
binding.statusBarShade.visibility = View.INVISIBLE
binding.navigationView.setVisible(visible = false, gone = false)
binding.statusBarShade.setVisible(visible = false, gone = false)
}
lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
homeViewModel.navigationVisible.collect { showNavigation(it.first, it.second) }
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) }
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
homeViewModel.contentToInstall.collect {
if (it != null) {
installContent(it)
homeViewModel.setContentToInstall(null)
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
homeViewModel.checkKeys.collect {
if (it) {
checkKeys()
homeViewModel.setCheckKeys(false)
}
}
}
homeViewModel.navigationVisible.collect(this) { showNavigation(it.first, it.second) }
homeViewModel.statusBarShadeVisible.collect(this) { showStatusBarShade(it) }
homeViewModel.contentToInstall.collect(
this,
resetState = { homeViewModel.setContentToInstall(null) }
) {
if (it != null) {
installContent(it)
}
}
homeViewModel.checkKeys.collect(this, resetState = { homeViewModel.setCheckKeys(false) }) {
if (it) checkKeys()
}
setInsets()
}
@ -214,18 +192,14 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
private fun showNavigation(visible: Boolean, animated: Boolean) {
if (!animated) {
if (visible) {
binding.navigationView.visibility = View.VISIBLE
} else {
binding.navigationView.visibility = View.INVISIBLE
}
binding.navigationView.setVisible(visible)
return
}
val smallLayout = resources.getBoolean(R.bool.small_layout)
binding.navigationView.animate().apply {
if (visible) {
binding.navigationView.visibility = View.VISIBLE
binding.navigationView.setVisible(true)
duration = 300
interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f)
@ -264,7 +238,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
}.withEndAction {
if (!visible) {
binding.navigationView.visibility = View.INVISIBLE
binding.navigationView.setVisible(visible = false, gone = false)
}
}.start()
}
@ -272,7 +246,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
private fun showStatusBarShade(visible: Boolean) {
binding.statusBarShade.animate().apply {
if (visible) {
binding.statusBarShade.visibility = View.VISIBLE
binding.statusBarShade.setVisible(true)
binding.statusBarShade.translationY = binding.statusBarShade.height.toFloat() * -2
duration = 300
translationY(0f)
@ -284,7 +258,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
}.withEndAction {
if (!visible) {
binding.statusBarShade.visibility = View.INVISIBLE
binding.statusBarShade.setVisible(visible = false, gone = false)
}
}.start()
}

View File

@ -6,439 +6,89 @@ package org.yuzu.yuzu_emu.utils
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import kotlin.math.sqrt
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.input.YuzuInputOverlayDevice
import org.yuzu.yuzu_emu.features.input.YuzuPhysicalDevice
object InputHandler {
private var controllerIds = getGameControllerIds()
fun initialize() {
// Connect first controller
NativeLibrary.onGamePadConnectEvent(getPlayerNumber(NativeLibrary.Player1Device))
}
fun updateControllerIds() {
controllerIds = getGameControllerIds()
}
var androidControllers = mapOf<Int, YuzuPhysicalDevice>()
var registeredControllers = mutableListOf<ParamPackage>()
fun dispatchKeyEvent(event: KeyEvent): Boolean {
val button: Int = when (event.device.vendorId) {
0x045E -> getInputXboxButtonKey(event.keyCode)
0x054C -> getInputDS5ButtonKey(event.keyCode)
0x057E -> getInputJoyconButtonKey(event.keyCode)
0x1532 -> getInputRazerButtonKey(event.keyCode)
0x3537 -> getInputRedmagicButtonKey(event.keyCode)
0x358A -> getInputBackboneLabsButtonKey(event.keyCode)
else -> getInputGenericButtonKey(event.keyCode)
}
val action = when (event.action) {
KeyEvent.ACTION_DOWN -> NativeLibrary.ButtonState.PRESSED
KeyEvent.ACTION_UP -> NativeLibrary.ButtonState.RELEASED
KeyEvent.ACTION_DOWN -> NativeInput.ButtonState.PRESSED
KeyEvent.ACTION_UP -> NativeInput.ButtonState.RELEASED
else -> return false
}
// Ignore invalid buttons
if (button < 0) {
return false
var controllerData = androidControllers[event.device.controllerNumber]
if (controllerData == null) {
updateControllerData()
controllerData = androidControllers[event.device.controllerNumber] ?: return false
}
return NativeLibrary.onGamePadButtonEvent(
getPlayerNumber(event.device.controllerNumber, event.deviceId),
button,
NativeInput.onGamePadButtonEvent(
controllerData.getGUID(),
controllerData.getPort(),
event.keyCode,
action
)
}
fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
val device = event.device
// Check every axis input available on the controller
for (range in device.motionRanges) {
val axis = range.axis
when (device.vendorId) {
0x045E -> setGenericAxisInput(event, axis)
0x054C -> setGenericAxisInput(event, axis)
0x057E -> setJoyconAxisInput(event, axis)
0x1532 -> setRazerAxisInput(event, axis)
else -> setGenericAxisInput(event, axis)
}
}
return true
}
private fun getPlayerNumber(index: Int, deviceId: Int = -1): Int {
var deviceIndex = index
if (deviceId != -1) {
deviceIndex = controllerIds[deviceId] ?: 0
}
// TODO: Joycons are handled as different controllers. Find a way to merge them.
return when (deviceIndex) {
2 -> NativeLibrary.Player2Device
3 -> NativeLibrary.Player3Device
4 -> NativeLibrary.Player4Device
5 -> NativeLibrary.Player5Device
6 -> NativeLibrary.Player6Device
7 -> NativeLibrary.Player7Device
8 -> NativeLibrary.Player8Device
else -> if (NativeLibrary.isHandheldOnly()) {
NativeLibrary.ConsoleDevice
} else {
NativeLibrary.Player1Device
}
fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
val controllerData =
androidControllers[event.device.controllerNumber] ?: return false
event.device.motionRanges.forEach {
NativeInput.onGamePadAxisEvent(
controllerData.getGUID(),
controllerData.getPort(),
it.axis,
event.getAxisValue(it.axis)
)
}
return true
}
private fun setStickState(playerNumber: Int, index: Int, xAxis: Float, yAxis: Float) {
// Calculate vector size
val r2 = xAxis * xAxis + yAxis * yAxis
var r = sqrt(r2.toDouble()).toFloat()
// Adjust range of joystick
val deadzone = 0.15f
var x = xAxis
var y = yAxis
if (r > deadzone) {
val deadzoneFactor = 1.0f / r * (r - deadzone) / (1.0f - deadzone)
x *= deadzoneFactor
y *= deadzoneFactor
r *= deadzoneFactor
} else {
x = 0.0f
y = 0.0f
}
// Normalize joystick
if (r > 1.0f) {
x /= r
y /= r
}
NativeLibrary.onGamePadJoystickEvent(
playerNumber,
index,
x,
-y
)
}
private fun getAxisToButton(axis: Float): Int {
return if (axis > 0.5f) {
NativeLibrary.ButtonState.PRESSED
} else {
NativeLibrary.ButtonState.RELEASED
}
}
private fun setAxisDpadState(playerNumber: Int, xAxis: Float, yAxis: Float) {
NativeLibrary.onGamePadButtonEvent(
playerNumber,
NativeLibrary.ButtonType.DPAD_UP,
getAxisToButton(-yAxis)
)
NativeLibrary.onGamePadButtonEvent(
playerNumber,
NativeLibrary.ButtonType.DPAD_DOWN,
getAxisToButton(yAxis)
)
NativeLibrary.onGamePadButtonEvent(
playerNumber,
NativeLibrary.ButtonType.DPAD_LEFT,
getAxisToButton(-xAxis)
)
NativeLibrary.onGamePadButtonEvent(
playerNumber,
NativeLibrary.ButtonType.DPAD_RIGHT,
getAxisToButton(xAxis)
)
}
private fun getInputDS5ButtonKey(key: Int): Int {
// The missing ds5 buttons are axis
return when (key) {
KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
else -> -1
}
}
private fun getInputJoyconButtonKey(key: Int): Int {
// Joycon support is half dead. A lot of buttons can't be mapped
return when (key) {
KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
KeyEvent.KEYCODE_DPAD_UP -> NativeLibrary.ButtonType.DPAD_UP
KeyEvent.KEYCODE_DPAD_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN
KeyEvent.KEYCODE_DPAD_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT
KeyEvent.KEYCODE_DPAD_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT
KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
else -> -1
}
}
private fun getInputXboxButtonKey(key: Int): Int {
// The missing xbox buttons are axis
return when (key) {
KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A
KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B
KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
else -> -1
}
}
private fun getInputRazerButtonKey(key: Int): Int {
// The missing xbox buttons are axis
return when (key) {
KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
else -> -1
}
}
private fun getInputRedmagicButtonKey(key: Int): Int {
return when (key) {
KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
else -> -1
}
}
private fun getInputBackboneLabsButtonKey(key: Int): Int {
return when (key) {
KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
else -> -1
}
}
private fun getInputGenericButtonKey(key: Int): Int {
return when (key) {
KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A
KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B
KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
KeyEvent.KEYCODE_DPAD_UP -> NativeLibrary.ButtonType.DPAD_UP
KeyEvent.KEYCODE_DPAD_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN
KeyEvent.KEYCODE_DPAD_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT
KeyEvent.KEYCODE_DPAD_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT
KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
else -> -1
}
}
private fun setGenericAxisInput(event: MotionEvent, axis: Int) {
val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId)
when (axis) {
MotionEvent.AXIS_X, MotionEvent.AXIS_Y ->
setStickState(
playerNumber,
NativeLibrary.StickType.STICK_L,
event.getAxisValue(MotionEvent.AXIS_X),
event.getAxisValue(MotionEvent.AXIS_Y)
)
MotionEvent.AXIS_RX, MotionEvent.AXIS_RY ->
setStickState(
playerNumber,
NativeLibrary.StickType.STICK_R,
event.getAxisValue(MotionEvent.AXIS_RX),
event.getAxisValue(MotionEvent.AXIS_RY)
)
MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ ->
setStickState(
playerNumber,
NativeLibrary.StickType.STICK_R,
event.getAxisValue(MotionEvent.AXIS_Z),
event.getAxisValue(MotionEvent.AXIS_RZ)
)
MotionEvent.AXIS_LTRIGGER ->
NativeLibrary.onGamePadButtonEvent(
playerNumber,
NativeLibrary.ButtonType.TRIGGER_ZL,
getAxisToButton(event.getAxisValue(MotionEvent.AXIS_LTRIGGER))
)
MotionEvent.AXIS_BRAKE ->
NativeLibrary.onGamePadButtonEvent(
playerNumber,
NativeLibrary.ButtonType.TRIGGER_ZL,
getAxisToButton(event.getAxisValue(MotionEvent.AXIS_BRAKE))
)
MotionEvent.AXIS_RTRIGGER ->
NativeLibrary.onGamePadButtonEvent(
playerNumber,
NativeLibrary.ButtonType.TRIGGER_ZR,
getAxisToButton(event.getAxisValue(MotionEvent.AXIS_RTRIGGER))
)
MotionEvent.AXIS_GAS ->
NativeLibrary.onGamePadButtonEvent(
playerNumber,
NativeLibrary.ButtonType.TRIGGER_ZR,
getAxisToButton(event.getAxisValue(MotionEvent.AXIS_GAS))
)
MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y ->
setAxisDpadState(
playerNumber,
event.getAxisValue(MotionEvent.AXIS_HAT_X),
event.getAxisValue(MotionEvent.AXIS_HAT_Y)
)
}
}
private fun setJoyconAxisInput(event: MotionEvent, axis: Int) {
// Joycon support is half dead. Right joystick doesn't work
val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId)
when (axis) {
MotionEvent.AXIS_X, MotionEvent.AXIS_Y ->
setStickState(
playerNumber,
NativeLibrary.StickType.STICK_L,
event.getAxisValue(MotionEvent.AXIS_X),
event.getAxisValue(MotionEvent.AXIS_Y)
)
MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ ->
setStickState(
playerNumber,
NativeLibrary.StickType.STICK_R,
event.getAxisValue(MotionEvent.AXIS_Z),
event.getAxisValue(MotionEvent.AXIS_RZ)
)
MotionEvent.AXIS_RX, MotionEvent.AXIS_RY ->
setStickState(
playerNumber,
NativeLibrary.StickType.STICK_R,
event.getAxisValue(MotionEvent.AXIS_RX),
event.getAxisValue(MotionEvent.AXIS_RY)
)
}
}
private fun setRazerAxisInput(event: MotionEvent, axis: Int) {
val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId)
when (axis) {
MotionEvent.AXIS_X, MotionEvent.AXIS_Y ->
setStickState(
playerNumber,
NativeLibrary.StickType.STICK_L,
event.getAxisValue(MotionEvent.AXIS_X),
event.getAxisValue(MotionEvent.AXIS_Y)
)
MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ ->
setStickState(
playerNumber,
NativeLibrary.StickType.STICK_R,
event.getAxisValue(MotionEvent.AXIS_Z),
event.getAxisValue(MotionEvent.AXIS_RZ)
)
MotionEvent.AXIS_BRAKE ->
NativeLibrary.onGamePadButtonEvent(
playerNumber,
NativeLibrary.ButtonType.TRIGGER_ZL,
getAxisToButton(event.getAxisValue(MotionEvent.AXIS_BRAKE))
)
MotionEvent.AXIS_GAS ->
NativeLibrary.onGamePadButtonEvent(
playerNumber,
NativeLibrary.ButtonType.TRIGGER_ZR,
getAxisToButton(event.getAxisValue(MotionEvent.AXIS_GAS))
)
MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y ->
setAxisDpadState(
playerNumber,
event.getAxisValue(MotionEvent.AXIS_HAT_X),
event.getAxisValue(MotionEvent.AXIS_HAT_Y)
)
}
}
fun getGameControllerIds(): Map<Int, Int> {
val gameControllerDeviceIds = mutableMapOf<Int, Int>()
fun getDevices(): Map<Int, YuzuPhysicalDevice> {
val gameControllerDeviceIds = mutableMapOf<Int, YuzuPhysicalDevice>()
val deviceIds = InputDevice.getDeviceIds()
var controllerSlot = 1
var port = 0
val inputSettings = NativeConfig.getInputSettings(true)
deviceIds.forEach { deviceId ->
InputDevice.getDevice(deviceId)?.apply {
// Don't over-assign controllers
if (controllerSlot >= 8) {
return gameControllerDeviceIds
}
// Verify that the device has gamepad buttons, control sticks, or both.
if (sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD ||
sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK
) {
// This device is a game controller. Store its device ID.
if (deviceId and id and vendorId and productId != 0) {
// Additionally filter out devices that have no ID
gameControllerDeviceIds
.takeIf { !it.contains(deviceId) }
?.put(deviceId, controllerSlot)
controllerSlot++
if (!gameControllerDeviceIds.contains(controllerNumber)) {
gameControllerDeviceIds[controllerNumber] = YuzuPhysicalDevice(
this,
port,
inputSettings[port].useSystemVibrator
)
}
port++
}
}
}
return gameControllerDeviceIds
}
fun updateControllerData() {
androidControllers = getDevices()
androidControllers.forEach {
NativeInput.registerController(it.value)
}
// Register the input overlay on a dedicated port for all player 1 vibrations
NativeInput.registerController(YuzuInputOverlayDevice(androidControllers.isEmpty(), 100))
registeredControllers.clear()
NativeInput.getInputDevices().forEach {
registeredControllers.add(ParamPackage(it))
}
registeredControllers.sortBy { it.get("port", 0) }
}
fun InputDevice.getGUID(): String = String.format("%016x%016x", productId, vendorId)
}

View File

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.utils
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
/**
* Collects this [Flow] with a given [LifecycleOwner].
* @param scope [LifecycleOwner] that this [Flow] will be collected with.
* @param repeatState When to repeat collection on this [Flow].
* @param resetState Optional lambda to reset state of an underlying [MutableStateFlow] after
* [stateCollector] has been run.
* @param stateCollector Lambda that receives new state.
*/
inline fun <reified T> Flow<T>.collect(
scope: LifecycleOwner,
repeatState: Lifecycle.State = Lifecycle.State.CREATED,
crossinline resetState: () -> Unit = {},
crossinline stateCollector: (state: T) -> Unit
) {
scope.apply {
lifecycleScope.launch {
repeatOnLifecycle(repeatState) {
this@collect.collect {
stateCollector(it)
resetState()
}
}
}
}
}

View File

@ -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()
}

View File

@ -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())
)
}

View File

@ -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()
}

View File

@ -3,8 +3,10 @@
package org.yuzu.yuzu_emu.utils
import android.text.TextUtils
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
object ViewUtils {
fun showView(view: View, length: Long = 300) {
@ -57,4 +59,35 @@ object ViewUtils {
}
this.layoutParams = layoutParams
}
/**
* Shows or hides a view.
* @param visible Whether a view will be made View.VISIBLE or View.INVISIBLE/GONE.
* @param gone Optional parameter for hiding a view. Uses View.GONE if true and View.INVISIBLE otherwise.
*/
fun View.setVisible(visible: Boolean, gone: Boolean = true) {
visibility = if (visible) {
View.VISIBLE
} else {
if (gone) {
View.GONE
} else {
View.INVISIBLE
}
}
}
/**
* Starts a marquee on some text.
* @param delay Optional parameter for changing the start delay. 3 seconds of delay by default.
*/
fun TextView.marquee(delay: Long = 3000) {
ellipsize = null
marqueeRepeatLimit = -1
isSingleLine = true
postDelayed({
ellipsize = TextUtils.TruncateAt.MARQUEE
isSelected = true
}, delay)
}
}

View File

@ -12,6 +12,7 @@ add_library(yuzu-android SHARED
native_log.cpp
android_config.cpp
android_config.h
native_input.cpp
)
set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR})

View File

@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <common/logging/log.h>
#include <input_common/main.h>
#include "android_config.h"
#include "android_settings.h"
#include "common/settings_setting.h"
@ -32,6 +34,7 @@ void AndroidConfig::ReadAndroidValues() {
ReadOverlayValues();
}
ReadDriverValues();
ReadAndroidControlValues();
}
void AndroidConfig::ReadAndroidUIValues() {
@ -107,6 +110,76 @@ void AndroidConfig::ReadOverlayValues() {
EndGroup();
}
void AndroidConfig::ReadAndroidPlayerValues(std::size_t player_index) {
std::string player_prefix;
if (type != ConfigType::InputProfile) {
player_prefix.append("player_").append(ToString(player_index)).append("_");
}
auto& player = Settings::values.players.GetValue()[player_index];
if (IsCustomConfig()) {
const auto profile_name =
ReadStringSetting(std::string(player_prefix).append("profile_name"));
if (profile_name.empty()) {
// Use the global input config
player = Settings::values.players.GetValue(true)[player_index];
player.profile_name = "";
return;
}
}
// Android doesn't have default options for controllers. We have the input overlay for that.
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
const std::string default_param;
auto& player_buttons = player.buttons[i];
player_buttons = ReadStringSetting(
std::string(player_prefix).append(Settings::NativeButton::mapping[i]), default_param);
if (player_buttons.empty()) {
player_buttons = default_param;
}
}
for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
const std::string default_param;
auto& player_analogs = player.analogs[i];
player_analogs = ReadStringSetting(
std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]), default_param);
if (player_analogs.empty()) {
player_analogs = default_param;
}
}
for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
const std::string default_param;
auto& player_motions = player.motions[i];
player_motions = ReadStringSetting(
std::string(player_prefix).append(Settings::NativeMotion::mapping[i]), default_param);
if (player_motions.empty()) {
player_motions = default_param;
}
}
player.use_system_vibrator = ReadBooleanSetting(
std::string(player_prefix).append("use_system_vibrator"), player_index == 0);
}
void AndroidConfig::ReadAndroidControlValues() {
BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
Settings::values.players.SetGlobal(!IsCustomConfig());
for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) {
ReadAndroidPlayerValues(p);
}
if (IsCustomConfig()) {
EndGroup();
return;
}
// ReadDebugControlValues();
// ReadHidbusValues();
EndGroup();
}
void AndroidConfig::SaveAndroidValues() {
if (global) {
SaveAndroidUIValues();
@ -114,6 +187,7 @@ void AndroidConfig::SaveAndroidValues() {
SaveOverlayValues();
}
SaveDriverValues();
SaveAndroidControlValues();
WriteToIni();
}
@ -187,6 +261,52 @@ void AndroidConfig::SaveOverlayValues() {
EndGroup();
}
void AndroidConfig::SaveAndroidPlayerValues(std::size_t player_index) {
std::string player_prefix;
if (type != ConfigType::InputProfile) {
player_prefix = std::string("player_").append(ToString(player_index)).append("_");
}
const auto& player = Settings::values.players.GetValue()[player_index];
if (IsCustomConfig() && player.profile_name.empty()) {
// No custom profile selected
return;
}
const std::string default_param;
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
WriteStringSetting(std::string(player_prefix).append(Settings::NativeButton::mapping[i]),
player.buttons[i], std::make_optional(default_param));
}
for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
WriteStringSetting(std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]),
player.analogs[i], std::make_optional(default_param));
}
for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
WriteStringSetting(std::string(player_prefix).append(Settings::NativeMotion::mapping[i]),
player.motions[i], std::make_optional(default_param));
}
WriteBooleanSetting(std::string(player_prefix).append("use_system_vibrator"),
player.use_system_vibrator, std::make_optional(player_index == 0));
}
void AndroidConfig::SaveAndroidControlValues() {
BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
Settings::values.players.SetGlobal(!IsCustomConfig());
for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) {
SaveAndroidPlayerValues(p);
}
if (IsCustomConfig()) {
EndGroup();
return;
}
// SaveDebugControlValues();
// SaveHidbusValues();
EndGroup();
}
std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) {
auto& map = Settings::values.linkage.by_category;
if (map.contains(category)) {
@ -194,3 +314,24 @@ std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::
}
return AndroidSettings::values.linkage.by_category[category];
}
void AndroidConfig::ReadAndroidControlPlayerValues(std::size_t player_index) {
BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
ReadPlayerValues(player_index);
ReadAndroidPlayerValues(player_index);
EndGroup();
}
void AndroidConfig::SaveAndroidControlPlayerValues(std::size_t player_index) {
BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
LOG_DEBUG(Config, "Saving players control configuration values");
SavePlayerValues(player_index);
SaveAndroidPlayerValues(player_index);
EndGroup();
WriteToIni();
}

View File

@ -13,7 +13,12 @@ public:
void ReloadAllValues() override;
void SaveAllValues() override;
void ReadAndroidControlPlayerValues(std::size_t player_index);
void SaveAndroidControlPlayerValues(std::size_t player_index);
protected:
void ReadAndroidPlayerValues(std::size_t player_index);
void ReadAndroidControlValues();
void ReadAndroidValues();
void ReadAndroidUIValues();
void ReadDriverValues();
@ -27,6 +32,8 @@ protected:
void ReadUILayoutValues() override {}
void ReadMultiplayerValues() override {}
void SaveAndroidPlayerValues(std::size_t player_index);
void SaveAndroidControlValues();
void SaveAndroidValues();
void SaveAndroidUIValues();
void SaveDriverValues();

View File

@ -5,6 +5,7 @@
#include "common/android/id_cache.h"
#include "common/logging/log.h"
#include "input_common/drivers/android.h"
#include "input_common/drivers/touch_screen.h"
#include "input_common/drivers/virtual_amiibo.h"
#include "input_common/drivers/virtual_gamepad.h"
@ -22,43 +23,6 @@ void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) {
window_info.render_surface = reinterpret_cast<void*>(surface);
}
void EmuWindow_Android::OnTouchPressed(int id, float x, float y) {
const auto [touch_x, touch_y] = MapToTouchScreen(x, y);
m_input_subsystem->GetTouchScreen()->TouchPressed(touch_x, touch_y, id);
}
void EmuWindow_Android::OnTouchMoved(int id, float x, float y) {
const auto [touch_x, touch_y] = MapToTouchScreen(x, y);
m_input_subsystem->GetTouchScreen()->TouchMoved(touch_x, touch_y, id);
}
void EmuWindow_Android::OnTouchReleased(int id) {
m_input_subsystem->GetTouchScreen()->TouchReleased(id);
}
void EmuWindow_Android::OnGamepadButtonEvent(int player_index, int button_id, bool pressed) {
m_input_subsystem->GetVirtualGamepad()->SetButtonState(player_index, button_id, pressed);
}
void EmuWindow_Android::OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y) {
m_input_subsystem->GetVirtualGamepad()->SetStickPosition(player_index, stick_id, x, y);
}
void EmuWindow_Android::OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x,
float gyro_y, float gyro_z, float accel_x,
float accel_y, float accel_z) {
m_input_subsystem->GetVirtualGamepad()->SetMotionState(
player_index, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z);
}
void EmuWindow_Android::OnReadNfcTag(std::span<u8> data) {
m_input_subsystem->GetVirtualAmiibo()->LoadAmiibo(data);
}
void EmuWindow_Android::OnRemoveNfcTag() {
m_input_subsystem->GetVirtualAmiibo()->CloseAmiibo();
}
void EmuWindow_Android::OnFrameDisplayed() {
if (!m_first_frame) {
Common::Android::RunJNIOnFiber<void>(
@ -67,10 +31,9 @@ void EmuWindow_Android::OnFrameDisplayed() {
}
}
EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem,
ANativeWindow* surface,
EmuWindow_Android::EmuWindow_Android(ANativeWindow* surface,
std::shared_ptr<Common::DynamicLibrary> driver_library)
: m_input_subsystem{input_subsystem}, m_driver_library{driver_library} {
: m_driver_library{driver_library} {
LOG_INFO(Frontend, "initializing");
if (!surface) {
@ -80,10 +43,4 @@ EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsyste
OnSurfaceChanged(surface);
window_info.type = Core::Frontend::WindowSystemType::Android;
m_input_subsystem->Initialize();
}
EmuWindow_Android::~EmuWindow_Android() {
m_input_subsystem->Shutdown();
}

View File

@ -30,21 +30,12 @@ private:
class EmuWindow_Android final : public Core::Frontend::EmuWindow {
public:
EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem, ANativeWindow* surface,
EmuWindow_Android(ANativeWindow* surface,
std::shared_ptr<Common::DynamicLibrary> driver_library);
~EmuWindow_Android();
~EmuWindow_Android() = default;
void OnSurfaceChanged(ANativeWindow* surface);
void OnTouchPressed(int id, float x, float y);
void OnTouchMoved(int id, float x, float y);
void OnTouchReleased(int id);
void OnGamepadButtonEvent(int player_index, int button_id, bool pressed);
void OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y);
void OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x, float gyro_y,
float gyro_z, float accel_x, float accel_y, float accel_z);
void OnReadNfcTag(std::span<u8> data);
void OnRemoveNfcTag();
void OnFrameDisplayed() override;
std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override {
@ -55,8 +46,6 @@ public:
};
private:
InputCommon::InputSubsystem* m_input_subsystem{};
float m_window_width{};
float m_window_height{};

View File

@ -88,6 +88,10 @@ FileSys::ManualContentProvider* EmulationSession::GetContentProvider() {
return m_manual_provider.get();
}
InputCommon::InputSubsystem& EmulationSession::GetInputSubsystem() {
return m_input_subsystem;
}
const EmuWindow_Android& EmulationSession::Window() const {
return *m_window;
}
@ -198,6 +202,8 @@ void EmulationSession::InitializeSystem(bool reload) {
Common::Log::Initialize();
Common::Log::SetColorConsoleBackendEnabled(true);
Common::Log::Start();
m_input_subsystem.Initialize();
}
// Initialize filesystem.
@ -222,8 +228,7 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string
std::scoped_lock lock(m_mutex);
// Create the render window.
m_window =
std::make_unique<EmuWindow_Android>(&m_input_subsystem, m_native_window, m_vulkan_library);
m_window = std::make_unique<EmuWindow_Android>(m_native_window, m_vulkan_library);
// Initialize system.
jauto android_keyboard = std::make_unique<Common::Android::SoftwareKeyboard::AndroidKeyboard>();
@ -355,60 +360,6 @@ void EmulationSession::RunEmulation() {
m_applet_id = static_cast<int>(Service::AM::AppletId::Application);
}
bool EmulationSession::IsHandheldOnly() {
jconst npad_style_set = m_system.HIDCore().GetSupportedStyleTag();
if (npad_style_set.fullkey == 1) {
return false;
}
if (npad_style_set.handheld == 0) {
return false;
}
return !Settings::IsDockedMode();
}
void EmulationSession::SetDeviceType([[maybe_unused]] int index, int type) {
jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
controller->SetNpadStyleIndex(static_cast<Core::HID::NpadStyleIndex>(type));
}
void EmulationSession::OnGamepadConnectEvent([[maybe_unused]] int index) {
jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
// Ensure that player1 is configured correctly and handheld disconnected
if (controller->GetNpadIdType() == Core::HID::NpadIdType::Player1) {
jauto handheld = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld);
if (controller->GetNpadStyleIndex() == Core::HID::NpadStyleIndex::Handheld) {
handheld->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey);
controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey);
handheld->Disconnect();
}
}
// Ensure that handheld is configured correctly and player 1 disconnected
if (controller->GetNpadIdType() == Core::HID::NpadIdType::Handheld) {
jauto player1 = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1);
if (controller->GetNpadStyleIndex() != Core::HID::NpadStyleIndex::Handheld) {
player1->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
player1->Disconnect();
}
}
if (!controller->IsConnected()) {
controller->Connect();
}
}
void EmulationSession::OnGamepadDisconnectEvent([[maybe_unused]] int index) {
jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
controller->Disconnect();
}
Common::Android::SoftwareKeyboard::AndroidKeyboard* EmulationSession::SoftwareKeyboard() {
return m_software_keyboard;
}
@ -574,14 +525,14 @@ jobjectArray Java_org_yuzu_yuzu_1emu_utils_GpuDriverHelper_getSystemDriverInfo(
nullptr, nullptr, file_redirect_dir_, nullptr);
auto driver_library = std::make_shared<Common::DynamicLibrary>(handle);
InputCommon::InputSubsystem input_subsystem;
auto m_window = std::make_unique<EmuWindow_Android>(
&input_subsystem, ANativeWindow_fromSurface(env, j_surf), driver_library);
auto window =
std::make_unique<EmuWindow_Android>(ANativeWindow_fromSurface(env, j_surf), driver_library);
Vulkan::vk::InstanceDispatch dld;
Vulkan::vk::Instance vk_instance = Vulkan::CreateInstance(
*driver_library, dld, VK_API_VERSION_1_1, Core::Frontend::WindowSystemType::Android);
auto surface = Vulkan::CreateSurface(vk_instance, m_window->GetWindowInfo());
auto surface = Vulkan::CreateSurface(vk_instance, window->GetWindowInfo());
auto device = Vulkan::CreateDevice(vk_instance, dld, *surface);
@ -622,103 +573,6 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused(JNIEnv* env, jclass claz
return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused());
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly(JNIEnv* env, jclass clazz) {
return EmulationSession::GetInstance().IsHandheldOnly();
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_setDeviceType(JNIEnv* env, jclass clazz,
jint j_device, jint j_type) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().SetDeviceType(j_device, j_type);
}
return static_cast<jboolean>(true);
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadConnectEvent(JNIEnv* env, jclass clazz,
jint j_device) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().OnGamepadConnectEvent(j_device);
}
return static_cast<jboolean>(true);
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadDisconnectEvent(JNIEnv* env, jclass clazz,
jint j_device) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().OnGamepadDisconnectEvent(j_device);
}
return static_cast<jboolean>(true);
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadButtonEvent(JNIEnv* env, jclass clazz,
jint j_device, jint j_button,
jint action) {
if (EmulationSession::GetInstance().IsRunning()) {
// Ensure gamepad is connected
EmulationSession::GetInstance().OnGamepadConnectEvent(j_device);
EmulationSession::GetInstance().Window().OnGamepadButtonEvent(j_device, j_button,
action != 0);
}
return static_cast<jboolean>(true);
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadJoystickEvent(JNIEnv* env, jclass clazz,
jint j_device, jint stick_id,
jfloat x, jfloat y) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().Window().OnGamepadJoystickEvent(j_device, stick_id, x, y);
}
return static_cast<jboolean>(true);
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadMotionEvent(
JNIEnv* env, jclass clazz, jint j_device, jlong delta_timestamp, jfloat gyro_x, jfloat gyro_y,
jfloat gyro_z, jfloat accel_x, jfloat accel_y, jfloat accel_z) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().Window().OnGamepadMotionEvent(
j_device, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z);
}
return static_cast<jboolean>(true);
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onReadNfcTag(JNIEnv* env, jclass clazz,
jbyteArray j_data) {
jboolean isCopy{false};
std::span<u8> data(reinterpret_cast<u8*>(env->GetByteArrayElements(j_data, &isCopy)),
static_cast<size_t>(env->GetArrayLength(j_data)));
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().Window().OnReadNfcTag(data);
}
return static_cast<jboolean>(true);
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onRemoveNfcTag(JNIEnv* env, jclass clazz) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().Window().OnRemoveNfcTag();
}
return static_cast<jboolean>(true);
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchPressed(JNIEnv* env, jclass clazz, jint id,
jfloat x, jfloat y) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().Window().OnTouchPressed(id, x, y);
}
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, jclass clazz, jint id,
jfloat x, jfloat y) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().Window().OnTouchMoved(id, x, y);
}
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchReleased(JNIEnv* env, jclass clazz, jint id) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().Window().OnTouchReleased(id);
}
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass clazz,
jboolean reload) {
// Initialize the emulated system.
@ -759,6 +613,7 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGpuDriver(JNIEnv* env, jobject
void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) {
EmulationSession::GetInstance().System().ApplySettings();
EmulationSession::GetInstance().System().HIDCore().ReloadInputDevices();
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_logSettings(JNIEnv* env, jobject jobj) {

View File

@ -23,6 +23,7 @@ public:
const Core::System& System() const;
Core::System& System();
FileSys::ManualContentProvider* GetContentProvider();
InputCommon::InputSubsystem& GetInputSubsystem();
const EmuWindow_Android& Window() const;
EmuWindow_Android& Window();
@ -50,10 +51,6 @@ public:
const std::size_t program_index,
const bool frontend_initiated);
bool IsHandheldOnly();
void SetDeviceType([[maybe_unused]] int index, int type);
void OnGamepadConnectEvent([[maybe_unused]] int index);
void OnGamepadDisconnectEvent([[maybe_unused]] int index);
Common::Android::SoftwareKeyboard::AndroidKeyboard* SoftwareKeyboard();
static void OnEmulationStarted();

View File

@ -3,7 +3,6 @@
#include <string>
#include <common/fs/fs_util.h>
#include <jni.h>
#include "android_config.h"
@ -425,4 +424,120 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setOverlayControlData(
}
}
jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInputSettings(JNIEnv* env, jobject obj,
jboolean j_global) {
Settings::values.players.SetGlobal(static_cast<bool>(j_global));
auto& players = Settings::values.players.GetValue();
jobjectArray j_input_settings =
env->NewObjectArray(players.size(), Common::Android::GetPlayerInputClass(), nullptr);
for (size_t i = 0; i < players.size(); ++i) {
auto j_connected = static_cast<jboolean>(players[i].connected);
jobjectArray j_buttons = env->NewObjectArray(
players[i].buttons.size(), Common::Android::GetStringClass(), env->NewStringUTF(""));
for (size_t j = 0; j < players[i].buttons.size(); ++j) {
env->SetObjectArrayElement(j_buttons, j,
Common::Android::ToJString(env, players[i].buttons[j]));
}
jobjectArray j_analogs = env->NewObjectArray(
players[i].analogs.size(), Common::Android::GetStringClass(), env->NewStringUTF(""));
for (size_t j = 0; j < players[i].analogs.size(); ++j) {
env->SetObjectArrayElement(j_analogs, j,
Common::Android::ToJString(env, players[i].analogs[j]));
}
jobjectArray j_motions = env->NewObjectArray(
players[i].motions.size(), Common::Android::GetStringClass(), env->NewStringUTF(""));
for (size_t j = 0; j < players[i].motions.size(); ++j) {
env->SetObjectArrayElement(j_motions, j,
Common::Android::ToJString(env, players[i].motions[j]));
}
auto j_vibration_enabled = static_cast<jboolean>(players[i].vibration_enabled);
auto j_vibration_strength = static_cast<jint>(players[i].vibration_strength);
auto j_body_color_left = static_cast<jlong>(players[i].body_color_left);
auto j_body_color_right = static_cast<jlong>(players[i].body_color_right);
auto j_button_color_left = static_cast<jlong>(players[i].button_color_left);
auto j_button_color_right = static_cast<jlong>(players[i].button_color_right);
auto j_profile_name = Common::Android::ToJString(env, players[i].profile_name);
auto j_use_system_vibrator = players[i].use_system_vibrator;
jobject playerInput = env->NewObject(
Common::Android::GetPlayerInputClass(), Common::Android::GetPlayerInputConstructor(),
j_connected, j_buttons, j_analogs, j_motions, j_vibration_enabled, j_vibration_strength,
j_body_color_left, j_body_color_right, j_button_color_left, j_button_color_right,
j_profile_name, j_use_system_vibrator);
env->SetObjectArrayElement(j_input_settings, i, playerInput);
}
return j_input_settings;
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInputSettings(JNIEnv* env, jobject obj,
jobjectArray j_value,
jboolean j_global) {
auto& players = Settings::values.players.GetValue(static_cast<bool>(j_global));
int playersSize = env->GetArrayLength(j_value);
for (int i = 0; i < playersSize; ++i) {
jobject jplayer = env->GetObjectArrayElement(j_value, i);
players[i].connected = static_cast<bool>(
env->GetBooleanField(jplayer, Common::Android::GetPlayerInputConnectedField()));
auto j_buttons_array = static_cast<jobjectArray>(
env->GetObjectField(jplayer, Common::Android::GetPlayerInputButtonsField()));
int buttons_size = env->GetArrayLength(j_buttons_array);
for (int j = 0; j < buttons_size; ++j) {
auto button = static_cast<jstring>(env->GetObjectArrayElement(j_buttons_array, j));
players[i].buttons[j] = Common::Android::GetJString(env, button);
}
auto j_analogs_array = static_cast<jobjectArray>(
env->GetObjectField(jplayer, Common::Android::GetPlayerInputAnalogsField()));
int analogs_size = env->GetArrayLength(j_analogs_array);
for (int j = 0; j < analogs_size; ++j) {
auto analog = static_cast<jstring>(env->GetObjectArrayElement(j_analogs_array, j));
players[i].analogs[j] = Common::Android::GetJString(env, analog);
}
auto j_motions_array = static_cast<jobjectArray>(
env->GetObjectField(jplayer, Common::Android::GetPlayerInputMotionsField()));
int motions_size = env->GetArrayLength(j_motions_array);
for (int j = 0; j < motions_size; ++j) {
auto motion = static_cast<jstring>(env->GetObjectArrayElement(j_motions_array, j));
players[i].motions[j] = Common::Android::GetJString(env, motion);
}
players[i].vibration_enabled = static_cast<bool>(
env->GetBooleanField(jplayer, Common::Android::GetPlayerInputVibrationEnabledField()));
players[i].vibration_strength = static_cast<int>(
env->GetIntField(jplayer, Common::Android::GetPlayerInputVibrationStrengthField()));
players[i].body_color_left = static_cast<u32>(
env->GetLongField(jplayer, Common::Android::GetPlayerInputBodyColorLeftField()));
players[i].body_color_right = static_cast<u32>(
env->GetLongField(jplayer, Common::Android::GetPlayerInputBodyColorRightField()));
players[i].button_color_left = static_cast<u32>(
env->GetLongField(jplayer, Common::Android::GetPlayerInputButtonColorLeftField()));
players[i].button_color_right = static_cast<u32>(
env->GetLongField(jplayer, Common::Android::GetPlayerInputButtonColorRightField()));
auto profileName = static_cast<jstring>(
env->GetObjectField(jplayer, Common::Android::GetPlayerInputProfileNameField()));
players[i].profile_name = Common::Android::GetJString(env, profileName);
players[i].use_system_vibrator =
env->GetBooleanField(jplayer, Common::Android::GetPlayerInputUseSystemVibratorField());
}
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveControlPlayerValues(JNIEnv* env, jobject obj) {
Settings::values.players.SetGlobal(false);
// Clear all controls from the config in case the user reverted back to globals
per_game_config->ClearControlPlayerValues();
for (size_t index = 0; index < Settings::values.players.GetValue().size(); ++index) {
per_game_config->SaveAndroidControlPlayerValues(index);
}
}
} // extern "C"

View File

@ -0,0 +1,631 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <common/fs/fs.h>
#include <common/fs/path_util.h>
#include <common/settings.h>
#include <hid_core/hid_types.h>
#include <jni.h>
#include "android_config.h"
#include "common/android/android_common.h"
#include "common/android/id_cache.h"
#include "hid_core/frontend/emulated_controller.h"
#include "hid_core/hid_core.h"
#include "input_common/drivers/android.h"
#include "input_common/drivers/touch_screen.h"
#include "input_common/drivers/virtual_amiibo.h"
#include "input_common/drivers/virtual_gamepad.h"
#include "native.h"
std::unordered_map<std::string, std::unique_ptr<AndroidConfig>> map_profiles;
bool IsHandheldOnly() {
const auto npad_style_set =
EmulationSession::GetInstance().System().HIDCore().GetSupportedStyleTag();
if (npad_style_set.fullkey == 1) {
return false;
}
if (npad_style_set.handheld == 0) {
return false;
}
return !Settings::IsDockedMode();
}
std::filesystem::path GetNameWithoutExtension(std::filesystem::path filename) {
return filename.replace_extension();
}
bool IsProfileNameValid(std::string_view profile_name) {
return profile_name.find_first_of("<>:;\"/\\|,.!?*") == std::string::npos;
}
bool ProfileExistsInFilesystem(std::string_view profile_name) {
return Common::FS::Exists(Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "input" /
fmt::format("{}.ini", profile_name));
}
bool ProfileExistsInMap(const std::string& profile_name) {
return map_profiles.find(profile_name) != map_profiles.end();
}
bool SaveProfile(const std::string& profile_name, std::size_t player_index) {
if (!ProfileExistsInMap(profile_name)) {
return false;
}
Settings::values.players.GetValue()[player_index].profile_name = profile_name;
map_profiles[profile_name]->SaveAndroidControlPlayerValues(player_index);
return true;
}
bool LoadProfile(std::string& profile_name, std::size_t player_index) {
if (!ProfileExistsInMap(profile_name)) {
return false;
}
if (!ProfileExistsInFilesystem(profile_name)) {
map_profiles.erase(profile_name);
return false;
}
LOG_INFO(Config, "Loading input profile `{}`", profile_name);
Settings::values.players.GetValue()[player_index].profile_name = profile_name;
map_profiles[profile_name]->ReadAndroidControlPlayerValues(player_index);
return true;
}
void ApplyControllerConfig(size_t player_index,
const std::function<void(Core::HID::EmulatedController*)>& apply) {
auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
if (player_index == 0) {
auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
handheld->EnableConfiguration();
player_one->EnableConfiguration();
apply(handheld);
apply(player_one);
handheld->DisableConfiguration();
player_one->DisableConfiguration();
handheld->SaveCurrentConfig();
player_one->SaveCurrentConfig();
} else {
auto* controller = hid_core.GetEmulatedControllerByIndex(player_index);
controller->EnableConfiguration();
apply(controller);
controller->DisableConfiguration();
controller->SaveCurrentConfig();
}
}
void ConnectController(size_t player_index, bool connected) {
auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
if (player_index == 0) {
auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
handheld->EnableConfiguration();
player_one->EnableConfiguration();
if (player_one->GetNpadStyleIndex(true) == Core::HID::NpadStyleIndex::Handheld) {
if (connected) {
handheld->Connect();
} else {
handheld->Disconnect();
}
player_one->Disconnect();
} else {
if (connected) {
player_one->Connect();
} else {
player_one->Disconnect();
}
handheld->Disconnect();
}
handheld->DisableConfiguration();
player_one->DisableConfiguration();
handheld->SaveCurrentConfig();
player_one->SaveCurrentConfig();
} else {
auto* controller = hid_core.GetEmulatedControllerByIndex(player_index);
controller->EnableConfiguration();
if (connected) {
controller->Connect();
} else {
controller->Disconnect();
}
controller->DisableConfiguration();
controller->SaveCurrentConfig();
}
}
extern "C" {
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isHandheldOnly(JNIEnv* env,
jobject j_obj) {
return IsHandheldOnly();
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadButtonEvent(
JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jint j_button_id, jint j_action) {
EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetButtonState(
Common::Android::GetJString(env, j_guid), j_port, j_button_id, j_action != 0);
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadAxisEvent(
JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jint j_stick_id, jfloat j_value) {
EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetAxisPosition(
Common::Android::GetJString(env, j_guid), j_port, j_stick_id, j_value);
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadMotionEvent(
JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jlong j_delta_timestamp,
jfloat j_x_gyro, jfloat j_y_gyro, jfloat j_z_gyro, jfloat j_x_accel, jfloat j_y_accel,
jfloat j_z_accel) {
EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetMotionState(
Common::Android::GetJString(env, j_guid), j_port, j_delta_timestamp, j_x_gyro, j_y_gyro,
j_z_gyro, j_x_accel, j_y_accel, j_z_accel);
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onReadNfcTag(JNIEnv* env, jobject j_obj,
jbyteArray j_data) {
jboolean isCopy{false};
std::span<u8> data(reinterpret_cast<u8*>(env->GetByteArrayElements(j_data, &isCopy)),
static_cast<size_t>(env->GetArrayLength(j_data)));
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo()->LoadAmiibo(data);
}
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onRemoveNfcTag(JNIEnv* env, jobject j_obj) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo()->CloseAmiibo();
}
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchPressed(JNIEnv* env, jobject j_obj,
jint j_id, jfloat j_x_axis,
jfloat j_y_axis) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchPressed(
j_id, j_x_axis, j_y_axis);
}
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchMoved(JNIEnv* env, jobject j_obj,
jint j_id, jfloat j_x_axis,
jfloat j_y_axis) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchMoved(
j_id, j_x_axis, j_y_axis);
}
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchReleased(JNIEnv* env, jobject j_obj,
jint j_id) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchReleased(j_id);
}
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onOverlayButtonEventImpl(
JNIEnv* env, jobject j_obj, jint j_port, jint j_button_id, jint j_action) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetButtonState(
j_port, j_button_id, j_action == 1);
}
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onOverlayJoystickEventImpl(
JNIEnv* env, jobject j_obj, jint j_port, jint j_stick_id, jfloat j_x_axis, jfloat j_y_axis) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetStickPosition(
j_port, j_stick_id, j_x_axis, j_y_axis);
}
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onDeviceMotionEvent(
JNIEnv* env, jobject j_obj, jint j_port, jlong j_delta_timestamp, jfloat j_x_gyro,
jfloat j_y_gyro, jfloat j_z_gyro, jfloat j_x_accel, jfloat j_y_accel, jfloat j_z_accel) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetMotionState(
j_port, j_delta_timestamp, j_x_gyro, j_y_gyro, j_z_gyro, j_x_accel, j_y_accel,
j_z_accel);
}
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_reloadInputDevices(JNIEnv* env,
jobject j_obj) {
EmulationSession::GetInstance().System().HIDCore().ReloadInputDevices();
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_registerController(JNIEnv* env,
jobject j_obj,
jobject j_device) {
EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->RegisterController(j_device);
}
jobjectArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getInputDevices(JNIEnv* env,
jobject j_obj) {
auto devices = EmulationSession::GetInstance().GetInputSubsystem().GetInputDevices();
jobjectArray jdevices = env->NewObjectArray(devices.size(), Common::Android::GetStringClass(),
Common::Android::ToJString(env, ""));
for (size_t i = 0; i < devices.size(); ++i) {
env->SetObjectArrayElement(jdevices, i,
Common::Android::ToJString(env, devices[i].Serialize()));
}
return jdevices;
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadInputProfiles(JNIEnv* env,
jobject j_obj) {
map_profiles.clear();
const auto input_profile_loc =
Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "input";
if (Common::FS::IsDir(input_profile_loc)) {
Common::FS::IterateDirEntries(
input_profile_loc,
[&](const std::filesystem::path& full_path) {
const auto filename = full_path.filename();
const auto name_without_ext =
Common::FS::PathToUTF8String(GetNameWithoutExtension(filename));
if (filename.extension() == ".ini" && IsProfileNameValid(name_without_ext)) {
map_profiles.insert_or_assign(
name_without_ext, std::make_unique<AndroidConfig>(
name_without_ext, Config::ConfigType::InputProfile));
}
return true;
},
Common::FS::DirEntryFilter::File);
}
}
jobjectArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getInputProfileNames(
JNIEnv* env, jobject j_obj) {
std::vector<std::string> profile_names;
profile_names.reserve(map_profiles.size());
auto it = map_profiles.cbegin();
while (it != map_profiles.cend()) {
const auto& [profile_name, config] = *it;
if (!ProfileExistsInFilesystem(profile_name)) {
it = map_profiles.erase(it);
continue;
}
profile_names.push_back(profile_name);
++it;
}
std::stable_sort(profile_names.begin(), profile_names.end());
jobjectArray j_profile_names =
env->NewObjectArray(profile_names.size(), Common::Android::GetStringClass(),
Common::Android::ToJString(env, ""));
for (size_t i = 0; i < profile_names.size(); ++i) {
env->SetObjectArrayElement(j_profile_names, i,
Common::Android::ToJString(env, profile_names[i]));
}
return j_profile_names;
}
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isProfileNameValid(JNIEnv* env,
jobject j_obj,
jstring j_name) {
return Common::Android::GetJString(env, j_name).find_first_of("<>:;\"/\\|,.!?*") ==
std::string::npos;
}
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_createProfile(JNIEnv* env,
jobject j_obj,
jstring j_name,
jint j_player_index) {
auto profile_name = Common::Android::GetJString(env, j_name);
if (ProfileExistsInMap(profile_name)) {
return false;
}
map_profiles.insert_or_assign(
profile_name,
std::make_unique<AndroidConfig>(profile_name, Config::ConfigType::InputProfile));
return SaveProfile(profile_name, j_player_index);
}
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_deleteProfile(JNIEnv* env,
jobject j_obj,
jstring j_name,
jint j_player_index) {
auto profile_name = Common::Android::GetJString(env, j_name);
if (!ProfileExistsInMap(profile_name)) {
return false;
}
if (!ProfileExistsInFilesystem(profile_name) ||
Common::FS::RemoveFile(map_profiles[profile_name]->GetConfigFilePath())) {
map_profiles.erase(profile_name);
}
Settings::values.players.GetValue()[j_player_index].profile_name = "";
return !ProfileExistsInMap(profile_name) && !ProfileExistsInFilesystem(profile_name);
}
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadProfile(JNIEnv* env, jobject j_obj,
jstring j_name,
jint j_player_index) {
auto profile_name = Common::Android::GetJString(env, j_name);
return LoadProfile(profile_name, j_player_index);
}
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_saveProfile(JNIEnv* env, jobject j_obj,
jstring j_name,
jint j_player_index) {
auto profile_name = Common::Android::GetJString(env, j_name);
return SaveProfile(profile_name, j_player_index);
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadPerGameConfiguration(
JNIEnv* env, jobject j_obj, jint j_player_index, jint j_selected_index,
jstring j_selected_profile_name) {
static constexpr size_t HANDHELD_INDEX = 8;
auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
Settings::values.players.SetGlobal(false);
auto profile_name = Common::Android::GetJString(env, j_selected_profile_name);
auto* emulated_controller = hid_core.GetEmulatedControllerByIndex(j_player_index);
if (j_selected_index == 0) {
Settings::values.players.GetValue()[j_player_index].profile_name = "";
if (j_player_index == 0) {
Settings::values.players.GetValue()[HANDHELD_INDEX] = {};
}
Settings::values.players.SetGlobal(true);
emulated_controller->ReloadFromSettings();
return;
}
if (profile_name.empty()) {
return;
}
auto& player = Settings::values.players.GetValue()[j_player_index];
auto& global_player = Settings::values.players.GetValue(true)[j_player_index];
player.profile_name = profile_name;
global_player.profile_name = profile_name;
// Read from the profile into the custom player settings
LoadProfile(profile_name, j_player_index);
// Make sure the controller is connected
player.connected = true;
emulated_controller->ReloadFromSettings();
if (j_player_index > 0) {
return;
}
// Handle Handheld cases
auto& handheld_player = Settings::values.players.GetValue()[HANDHELD_INDEX];
auto* handheld_controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
if (player.controller_type == Settings::ControllerType::Handheld) {
handheld_player = player;
} else {
handheld_player = {};
}
handheld_controller->ReloadFromSettings();
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_beginMapping(JNIEnv* env, jobject j_obj,
jint jtype) {
EmulationSession::GetInstance().GetInputSubsystem().BeginMapping(
static_cast<InputCommon::Polling::InputType>(jtype));
}
jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getNextInput(JNIEnv* env,
jobject j_obj) {
return Common::Android::ToJString(
env, EmulationSession::GetInstance().GetInputSubsystem().GetNextInput().Serialize());
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_stopMapping(JNIEnv* env, jobject j_obj) {
EmulationSession::GetInstance().GetInputSubsystem().StopMapping();
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_updateMappingsWithDefaultImpl(
JNIEnv* env, jobject j_obj, jint j_player_index, jstring j_device_params,
jstring j_display_name) {
auto& input_subsystem = EmulationSession::GetInstance().GetInputSubsystem();
// Clear all previous mappings
for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) {
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
controller->SetButtonParam(button_id, {});
});
}
for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) {
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
controller->SetStickParam(analog_id, {});
});
}
// Apply new mappings
auto device = Common::ParamPackage(Common::Android::GetJString(env, j_device_params));
auto button_mappings = input_subsystem.GetButtonMappingForDevice(device);
auto analog_mappings = input_subsystem.GetAnalogMappingForDevice(device);
auto display_name = Common::Android::GetJString(env, j_display_name);
for (const auto& button_mapping : button_mappings) {
const std::size_t index = button_mapping.first;
auto named_mapping = button_mapping.second;
named_mapping.Set("display", display_name);
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
controller->SetButtonParam(index, named_mapping);
});
}
for (const auto& analog_mapping : analog_mappings) {
const std::size_t index = analog_mapping.first;
auto named_mapping = analog_mapping.second;
named_mapping.Set("display", display_name);
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
controller->SetStickParam(index, named_mapping);
});
}
}
jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getButtonParamImpl(JNIEnv* env,
jobject j_obj,
jint j_player_index,
jint j_button) {
return Common::Android::ToJString(env, EmulationSession::GetInstance()
.System()
.HIDCore()
.GetEmulatedControllerByIndex(j_player_index)
->GetButtonParam(j_button)
.Serialize());
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setButtonParamImpl(
JNIEnv* env, jobject j_obj, jint j_player_index, jint j_button_id, jstring j_param) {
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
controller->SetButtonParam(j_button_id,
Common::ParamPackage(Common::Android::GetJString(env, j_param)));
});
}
jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getStickParamImpl(JNIEnv* env,
jobject j_obj,
jint j_player_index,
jint j_stick) {
return Common::Android::ToJString(env, EmulationSession::GetInstance()
.System()
.HIDCore()
.GetEmulatedControllerByIndex(j_player_index)
->GetStickParam(j_stick)
.Serialize());
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setStickParamImpl(
JNIEnv* env, jobject j_obj, jint j_player_index, jint j_stick_id, jstring j_param) {
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
controller->SetStickParam(j_stick_id,
Common::ParamPackage(Common::Android::GetJString(env, j_param)));
});
}
jint Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getButtonNameImpl(JNIEnv* env,
jobject j_obj,
jstring j_param) {
return static_cast<jint>(EmulationSession::GetInstance().GetInputSubsystem().GetButtonName(
Common::ParamPackage(Common::Android::GetJString(env, j_param))));
}
jintArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getSupportedStyleTagsImpl(
JNIEnv* env, jobject j_obj, jint j_player_index) {
auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
const auto npad_style_set = hid_core.GetSupportedStyleTag();
std::vector<s32> supported_indexes;
if (npad_style_set.fullkey == 1) {
supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::Fullkey));
}
if (npad_style_set.joycon_dual == 1) {
supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconDual));
}
if (npad_style_set.joycon_left == 1) {
supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconLeft));
}
if (npad_style_set.joycon_right == 1) {
supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconRight));
}
if (j_player_index == 0 && npad_style_set.handheld == 1) {
supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::Handheld));
}
if (npad_style_set.gamecube == 1) {
supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::GameCube));
}
jintArray j_supported_indexes = env->NewIntArray(supported_indexes.size());
env->SetIntArrayRegion(j_supported_indexes, 0, supported_indexes.size(),
supported_indexes.data());
return j_supported_indexes;
}
jint Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getStyleIndexImpl(JNIEnv* env,
jobject j_obj,
jint j_player_index) {
return static_cast<s32>(EmulationSession::GetInstance()
.System()
.HIDCore()
.GetEmulatedControllerByIndex(j_player_index)
->GetNpadStyleIndex(true));
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setStyleIndexImpl(JNIEnv* env,
jobject j_obj,
jint j_player_index,
jint j_style_index) {
auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
auto type = static_cast<Core::HID::NpadStyleIndex>(j_style_index);
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
controller->SetNpadStyleIndex(type);
});
if (j_player_index == 0) {
auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
ConnectController(j_player_index,
player_one->IsConnected(true) || handheld->IsConnected(true));
}
}
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isControllerImpl(JNIEnv* env,
jobject j_obj,
jstring jparams) {
return static_cast<jint>(EmulationSession::GetInstance().GetInputSubsystem().IsController(
Common::ParamPackage(Common::Android::GetJString(env, jparams))));
}
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getIsConnected(JNIEnv* env,
jobject j_obj,
jint j_player_index) {
auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
auto* controller = hid_core.GetEmulatedControllerByIndex(static_cast<size_t>(j_player_index));
if (j_player_index == 0 &&
controller->GetNpadStyleIndex(true) == Core::HID::NpadStyleIndex::Handheld) {
return hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld)->IsConnected(true);
}
return controller->IsConnected(true);
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_connectControllersImpl(
JNIEnv* env, jobject j_obj, jbooleanArray j_connected) {
jboolean isCopy = false;
auto j_connected_array_size = env->GetArrayLength(j_connected);
jboolean* j_connected_array = env->GetBooleanArrayElements(j_connected, &isCopy);
for (int i = 0; i < j_connected_array_size; ++i) {
ConnectController(i, j_connected_array[i]);
}
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_resetControllerMappings(
JNIEnv* env, jobject j_obj, jint j_player_index) {
// Clear all previous mappings
for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) {
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
controller->SetButtonParam(button_id, {});
});
}
for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) {
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
controller->SetStickParam(analog_id, {});
});
}
}
} // extern "C"

View File

@ -0,0 +1,142 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:width="1000dp"
android:height="1000dp"
android:viewportWidth="1000"
android:viewportHeight="1000">
<group android:name="_R_G">
<group
android:name="_R_G_L_0_G"
android:pivotX="100"
android:pivotY="100"
android:scaleX="4.5"
android:scaleY="4.5"
android:translateX="400"
android:translateY="400">
<path
android:name="_R_G_L_0_G_D_0_P_0"
android:fillAlpha="1"
android:fillColor="?attr/colorSecondaryContainer"
android:fillType="nonZero"
android:pathData=" M198.56 100 C198.56,154.43 154.43,198.56 100,198.56 C45.57,198.56 1.44,154.43 1.44,100 C1.44,45.57 45.57,1.44 100,1.44 C154.43,1.44 198.56,45.57 198.56,100c " />
<path
android:name="_R_G_L_0_G_D_2_P_0"
android:fillAlpha="0.8"
android:fillColor="?attr/colorOnSecondaryContainer"
android:fillType="nonZero"
android:pathData=" M50.14 151.21 C50.53,150.18 89.6,49.87 90.1,48.63 C90.1,48.63 90.67,47.2 90.67,47.2 C90.67,47.2 101.67,47.2 101.67,47.2 C101.67,47.2 112.67,47.2 112.67,47.2 C112.67,47.2 133.47,99.12 133.47,99.12 C144.91,127.68 154.32,151.17 154.38,151.33 C154.47,151.56 152.2,151.6 143.14,151.55 C143.14,151.55 131.79,151.48 131.79,151.48 C131.79,151.48 127.22,139.57 127.22,139.57 C127.22,139.57 122.65,127.66 122.65,127.66 C122.65,127.66 101.68,127.73 101.68,127.73 C101.68,127.73 80.71,127.8 80.71,127.8 C80.71,127.8 76.38,139.71 76.38,139.71 C76.38,139.71 72.06,151.62 72.06,151.62 C72.06,151.62 61.02,151.62 61.02,151.62 C50.61,151.62 50,151.55 50.14,151.22 C50.14,151.22 50.14,151.21 50.14,151.21c M115.86 110.06 C115.8,109.91 112.55,101.13 108.62,90.56 C104.7,80 101.42,71.43 101.34,71.53 C101.22,71.66 92.84,94.61 87.25,110.06 C87.17,110.29 90.13,110.34 101.56,110.34 C113,110.34 115.95,110.28 115.86,110.06c " />
</group>
</group>
<group android:name="time_group" />
</vector>
</aapt:attr>
<target android:name="_R_G_L_0_G">
<aapt:attr name="android:animation">
<set android:ordering="together">
<objectAnimator
android:duration="100"
android:propertyName="scaleX"
android:startOffset="0"
android:valueFrom="4.5"
android:valueTo="3.75"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="100"
android:propertyName="scaleY"
android:startOffset="0"
android:valueFrom="4.5"
android:valueTo="3.75"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="234"
android:propertyName="scaleX"
android:startOffset="100"
android:valueFrom="3.75"
android:valueTo="3.75"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="234"
android:propertyName="scaleY"
android:startOffset="100"
android:valueFrom="3.75"
android:valueTo="3.75"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="167"
android:propertyName="scaleX"
android:startOffset="334"
android:valueFrom="3.75"
android:valueTo="4.75"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="167"
android:propertyName="scaleY"
android:startOffset="334"
android:valueFrom="3.75"
android:valueTo="4.75"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="67"
android:propertyName="scaleX"
android:startOffset="501"
android:valueFrom="4.75"
android:valueTo="4.5"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="67"
android:propertyName="scaleY"
android:startOffset="501"
android:valueFrom="4.75"
android:valueTo="4.5"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
</set>
</aapt:attr>
</target>
<target android:name="time_group">
<aapt:attr name="android:animation">
<set android:ordering="together">
<objectAnimator
android:duration="1034"
android:propertyName="translateX"
android:startOffset="0"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
</animated-vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M700,480q-25,0 -42.5,-17.5T640,420q0,-25 17.5,-42.5T700,360q25,0 42.5,17.5T760,420q0,25 -17.5,42.5T700,480ZM366,480ZM280,600v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80ZM160,720q-33,0 -56.5,-23.5T80,640v-320q0,-34 24,-57.5t58,-23.5h77l81,81L160,320v320h366L55,169l57,-57 736,736 -57,57 -185,-185L160,720ZM880,640q0,26 -14,46t-37,29l-29,-29v-366L434,320l-80,-80h446q33,0 56.5,23.5T880,320v320ZM617,503Z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M21,12l-4.37,6.16C16.26,18.68 15.65,19 15,19h-3l0,-6H9v-3H3V7c0,-1.1 0.9,-2 2,-2h10c0.65,0 1.26,0.31 1.63,0.84L21,12zM10,15H7v-3H5v3H2v2h3v3h2v-3h3V15z" />
</vector>

View File

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M21,5H3C1.9,5 1,5.9 1,7v10c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V7C23,5.9 22.1,5 21,5zM18,17H6V7h12V17z" />
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M15,11.25h1.5v1.5h-1.5z" />
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M12.5,11.25h1.5v1.5h-1.5z" />
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M10,11.25h1.5v1.5h-1.5z" />
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M7.5,11.25h1.5v1.5h-1.5z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
</vector>

Some files were not shown because too many files have changed in this diff Show More