Merge pull request #13674 from JosJuice/android-visualize-input

Android: Show input indicators in controller settings
This commit is contained in:
Jordan Woyak
2025-07-20 17:46:47 -05:00
committed by GitHub
26 changed files with 542 additions and 78 deletions

View File

@ -37,7 +37,8 @@ class GameAdapter : RecyclerView.Adapter<GameViewHolder>(),
* @return The created ViewHolder with references to all the child view's members. * @return The created ViewHolder with references to all the child view's members.
*/ */
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context)) val inflater = LayoutInflater.from(parent.context)
val binding = CardGameBinding.inflate(inflater, parent, false)
binding.root.apply { binding.root.apply {
setOnClickListener(this@GameAdapter) setOnClickListener(this@GameAdapter)
setOnLongClickListener(this@GameAdapter) setOnLongClickListener(this@GameAdapter)

View File

@ -43,17 +43,17 @@ class CheatsAdapter(
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
return when (viewType) { return when (viewType) {
CheatItem.TYPE_CHEAT -> { CheatItem.TYPE_CHEAT -> {
val listItemCheatBinding = ListItemCheatBinding.inflate(inflater) val listItemCheatBinding = ListItemCheatBinding.inflate(inflater, parent, false)
addViewListeners(listItemCheatBinding.getRoot()) addViewListeners(listItemCheatBinding.getRoot())
CheatViewHolder(listItemCheatBinding) CheatViewHolder(listItemCheatBinding)
} }
CheatItem.TYPE_HEADER -> { CheatItem.TYPE_HEADER -> {
val listItemHeaderBinding = ListItemHeaderBinding.inflate(inflater) val listItemHeaderBinding = ListItemHeaderBinding.inflate(inflater, parent, false)
addViewListeners(listItemHeaderBinding.root) addViewListeners(listItemHeaderBinding.root)
HeaderViewHolder(listItemHeaderBinding) HeaderViewHolder(listItemHeaderBinding)
} }
CheatItem.TYPE_ACTION -> { CheatItem.TYPE_ACTION -> {
val listItemSubmenuBinding = ListItemSubmenuBinding.inflate(inflater) val listItemSubmenuBinding = ListItemSubmenuBinding.inflate(inflater, parent, false)
addViewListeners(listItemSubmenuBinding.root) addViewListeners(listItemSubmenuBinding.root)
ActionViewHolder(listItemSubmenuBinding) ActionViewHolder(listItemSubmenuBinding)
} }

View File

@ -6,6 +6,7 @@ import android.content.Context
import android.hardware.input.InputManager import android.hardware.input.InputManager
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper
import android.os.VibrationEffect import android.os.VibrationEffect
import android.os.Vibrator import android.os.Vibrator
import android.os.VibratorManager import android.os.VibratorManager
@ -13,8 +14,11 @@ import android.view.InputDevice
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import org.dolphinemu.dolphinemu.DolphinApplication import org.dolphinemu.dolphinemu.DolphinApplication
import org.dolphinemu.dolphinemu.utils.LooperThread import org.dolphinemu.dolphinemu.utils.LooperThread
import java.util.concurrent.atomic.AtomicBoolean
/** /**
* This class interfaces with the native ControllerInterface, * This class interfaces with the native ControllerInterface,
@ -24,6 +28,16 @@ object ControllerInterface {
private var inputDeviceListener: InputDeviceListener? = null private var inputDeviceListener: InputDeviceListener? = null
private lateinit var looperThread: LooperThread private lateinit var looperThread: LooperThread
private var inputStateUpdatePending = AtomicBoolean(false)
private val inputStateVersion = MutableLiveData(0)
private val devicesVersion = MutableLiveData(0)
val inputStateChanged: LiveData<Int>
get() = inputStateVersion
val devicesChanged: LiveData<Int>
get() = devicesVersion
/** /**
* Activities which want to pass on inputs to native code * Activities which want to pass on inputs to native code
* should call this in their own dispatchKeyEvent method. * should call this in their own dispatchKeyEvent method.
@ -31,7 +45,13 @@ object ControllerInterface {
* @return true if the emulator core seems to be interested in this event. * @return true if the emulator core seems to be interested in this event.
* false if the event should be passed on to the default dispatchKeyEvent. * false if the event should be passed on to the default dispatchKeyEvent.
*/ */
external fun dispatchKeyEvent(event: KeyEvent): Boolean fun dispatchKeyEvent(event: KeyEvent): Boolean {
val result = dispatchKeyEventNative(event)
onInputStateChanged()
return result
}
private external fun dispatchKeyEventNative(event: KeyEvent): Boolean
/** /**
* Activities which want to pass on inputs to native code * Activities which want to pass on inputs to native code
@ -40,7 +60,13 @@ object ControllerInterface {
* @return true if the emulator core seems to be interested in this event. * @return true if the emulator core seems to be interested in this event.
* false if the event should be passed on to the default dispatchGenericMotionEvent. * false if the event should be passed on to the default dispatchGenericMotionEvent.
*/ */
external fun dispatchGenericMotionEvent(event: MotionEvent): Boolean fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
val result = dispatchGenericMotionEventNative(event)
onInputStateChanged()
return result
}
private external fun dispatchGenericMotionEventNative(event: MotionEvent): Boolean
/** /**
* [DolphinSensorEventListener] calls this for each axis of a received SensorEvent. * [DolphinSensorEventListener] calls this for each axis of a received SensorEvent.
@ -48,7 +74,13 @@ object ControllerInterface {
* @return true if the emulator core seems to be interested in this event. * @return true if the emulator core seems to be interested in this event.
* false if the sensor can be suspended to save battery. * false if the sensor can be suspended to save battery.
*/ */
external fun dispatchSensorEvent( fun dispatchSensorEvent(deviceQualifier: String, axisName: String, value: Float): Boolean {
val result = dispatchSensorEventNative(deviceQualifier, axisName, value)
onInputStateChanged()
return result
}
private external fun dispatchSensorEventNative(
deviceQualifier: String, deviceQualifier: String,
axisName: String, axisName: String,
value: Float value: Float
@ -76,6 +108,27 @@ object ControllerInterface {
external fun getDevice(deviceString: String): CoreDevice? external fun getDevice(deviceString: String): CoreDevice?
private fun onInputStateChanged() {
// When a single SensorEvent is dispatched, this method is likely to get called many times.
// For the sake of performance, let's batch input state updates so that observers only have
// to process one update.
if (!inputStateUpdatePending.getAndSet(true)) {
Handler(Looper.getMainLooper()).post {
if (inputStateUpdatePending.getAndSet(false)) {
inputStateVersion.value = inputStateVersion.value?.plus(1)
}
}
}
}
@Keep
@JvmStatic
private fun onDevicesChanged() {
Handler(Looper.getMainLooper()).post {
devicesVersion.value = devicesVersion.value?.plus(1)
}
}
@Keep @Keep
@JvmStatic @JvmStatic
private fun registerInputDeviceListener() { private fun registerInputDeviceListener() {

View File

@ -18,6 +18,8 @@ class CoreDevice private constructor(private val pointer: Long) {
@Keep @Keep
inner class Control private constructor(private val pointer: Long) { inner class Control private constructor(private val pointer: Long) {
external fun getName(): String external fun getName(): String
external fun getState(): Double
} }
protected external fun finalize() protected external fun finalize()

View File

@ -18,6 +18,9 @@ class InputMappingControlSetting(var control: Control, val controller: EmulatedC
controller.updateSingleControlReference(controlReference) controller.updateSingleControlReference(controlReference)
} }
val state: Double
get() = controlReference.getState()
fun clearValue() { fun clearValue() {
value = "" value = ""
} }

View File

@ -4,21 +4,27 @@ package org.dolphinemu.dolphinemu.features.input.ui
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.dolphinemu.dolphinemu.databinding.ListItemAdvancedMappingControlBinding import org.dolphinemu.dolphinemu.databinding.ListItemAdvancedMappingControlBinding
import org.dolphinemu.dolphinemu.features.input.model.CoreDevice
import java.util.function.Consumer import java.util.function.Consumer
class AdvancedMappingControlAdapter(private val onClickCallback: Consumer<String>) : class AdvancedMappingControlAdapter(
RecyclerView.Adapter<AdvancedMappingControlViewHolder>() { private val parentLifecycle: Lifecycle,
private var controls = emptyArray<String>() private val isInput: Boolean,
private val onClickCallback: Consumer<String>
) : RecyclerView.Adapter<AdvancedMappingControlViewHolder>() {
private var controls = emptyArray<CoreDevice.Control>()
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int
): AdvancedMappingControlViewHolder { ): AdvancedMappingControlViewHolder {
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
val binding = ListItemAdvancedMappingControlBinding.inflate(inflater) val binding = ListItemAdvancedMappingControlBinding.inflate(inflater, parent, false)
return AdvancedMappingControlViewHolder(binding, onClickCallback) return AdvancedMappingControlViewHolder(binding, parentLifecycle, isInput, onClickCallback)
} }
override fun onBindViewHolder(holder: AdvancedMappingControlViewHolder, position: Int) = override fun onBindViewHolder(holder: AdvancedMappingControlViewHolder, position: Int) =
@ -26,8 +32,23 @@ class AdvancedMappingControlAdapter(private val onClickCallback: Consumer<String
override fun getItemCount(): Int = controls.size override fun getItemCount(): Int = controls.size
fun setControls(controls: Array<String>) { fun setControls(controls: Array<CoreDevice.Control>) {
this.controls = controls this.controls = controls
notifyDataSetChanged() notifyDataSetChanged()
} }
override fun onViewRecycled(holder: AdvancedMappingControlViewHolder) {
super.onViewRecycled(holder)
holder.onViewRecycled()
}
override fun onViewAttachedToWindow(holder: AdvancedMappingControlViewHolder) {
super.onViewAttachedToWindow(holder)
holder.onViewAttachedToWindow()
}
override fun onViewDetachedFromWindow(holder: AdvancedMappingControlViewHolder) {
super.onViewDetachedFromWindow(holder)
holder.onViewDetachedFromWindow()
}
} }

View File

@ -2,22 +2,68 @@
package org.dolphinemu.dolphinemu.features.input.ui package org.dolphinemu.dolphinemu.features.input.ui
import androidx.recyclerview.widget.RecyclerView import android.view.View
import androidx.lifecycle.Lifecycle
import org.dolphinemu.dolphinemu.databinding.ListItemAdvancedMappingControlBinding import org.dolphinemu.dolphinemu.databinding.ListItemAdvancedMappingControlBinding
import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface
import org.dolphinemu.dolphinemu.features.input.model.CoreDevice
import org.dolphinemu.dolphinemu.utils.LifecycleViewHolder
import java.text.DecimalFormat
import java.util.function.Consumer import java.util.function.Consumer
import kotlin.math.abs
class AdvancedMappingControlViewHolder( class AdvancedMappingControlViewHolder(
private val binding: ListItemAdvancedMappingControlBinding, private val binding: ListItemAdvancedMappingControlBinding,
private val parentLifecycle: Lifecycle,
private val isInput: Boolean,
onClickCallback: Consumer<String> onClickCallback: Consumer<String>
) : RecyclerView.ViewHolder(binding.root) { ) : LifecycleViewHolder(binding.root, parentLifecycle) {
private lateinit var name: String
private lateinit var control: CoreDevice.Control
private var previousState = Float.POSITIVE_INFINITY
init { init {
binding.root.setOnClickListener { onClickCallback.accept(name) } binding.root.setOnClickListener { onClickCallback.accept(control.getName()) }
if (isInput) {
ControllerInterface.inputStateChanged.observe(this) {
updateInputValue()
}
} else {
binding.layoutState.visibility = View.GONE
}
} }
fun bind(name: String) { fun bind(control: CoreDevice.Control) {
this.name = name this.control = control
binding.textName.text = name binding.textName.text = control.getName()
if (isInput) {
updateInputValue()
}
}
private fun updateInputValue() {
if (parentLifecycle.currentState == Lifecycle.State.DESTROYED) {
throw IllegalStateException("AdvancedMappingControlViewHolder leak")
}
var state = control.getState().toFloat()
if (abs(state - previousState) >= stateUpdateThreshold) {
previousState = state
// Don't print a minus sign for signed zeroes
if (state == -0.0f)
state = 0.0f
binding.textState.setText(stateFormat.format(state))
binding.controlStateBar.state = state
}
}
companion object {
private val stateFormat = DecimalFormat("#.####")
// For performance, require state to change by a certain threshold before we update the UI
private val stateUpdateThreshold = 0.00005f
} }
} }

View File

@ -3,6 +3,8 @@
package org.dolphinemu.dolphinemu.features.input.ui package org.dolphinemu.dolphinemu.features.input.ui
import android.content.Context import android.content.Context
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View import android.view.View
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.AdapterView.OnItemClickListener import android.widget.AdapterView.OnItemClickListener
@ -23,7 +25,7 @@ class AdvancedMappingDialog(
private val controlReference: ControlReference, private val controlReference: ControlReference,
private val controller: EmulatedController private val controller: EmulatedController
) : AlertDialog(context), OnItemClickListener { ) : AlertDialog(context), OnItemClickListener {
private val devices: Array<String> = ControllerInterface.getAllDeviceStrings() private lateinit var devices: Array<String>
private val controlAdapter: AdvancedMappingControlAdapter private val controlAdapter: AdvancedMappingControlAdapter
private lateinit var selectedDevice: String private lateinit var selectedDevice: String
@ -34,12 +36,9 @@ class AdvancedMappingDialog(
binding.dropdownDevice.onItemClickListener = this binding.dropdownDevice.onItemClickListener = this
val deviceAdapter = controlAdapter = AdvancedMappingControlAdapter(lifecycle, controlReference.isInput()) {
ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item, devices) control: String -> onControlClicked(control)
binding.dropdownDevice.setAdapter(deviceAdapter) }
controlAdapter =
AdvancedMappingControlAdapter { control: String -> onControlClicked(control) }
binding.listControl.adapter = controlAdapter binding.listControl.adapter = controlAdapter
binding.listControl.layoutManager = LinearLayoutManager(context) binding.listControl.layoutManager = LinearLayoutManager(context)
@ -49,6 +48,12 @@ class AdvancedMappingDialog(
binding.editExpression.setText(controlReference.getExpression()) binding.editExpression.setText(controlReference.getExpression())
ControllerInterface.devicesChanged.observe(this) {
onDevicesChanged()
setSelectedDevice(selectedDevice)
}
onDevicesChanged()
selectDefaultDevice() selectDefaultDevice()
} }
@ -59,6 +64,23 @@ class AdvancedMappingDialog(
override fun onItemClick(adapterView: AdapterView<*>?, view: View, position: Int, id: Long) = override fun onItemClick(adapterView: AdapterView<*>?, view: View, position: Int, id: Long) =
setSelectedDevice(devices[position]) setSelectedDevice(devices[position])
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
ControllerInterface.dispatchKeyEvent(event)
return super.dispatchKeyEvent(event)
}
override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
ControllerInterface.dispatchGenericMotionEvent(event)
return super.dispatchGenericMotionEvent(event)
}
private fun onDevicesChanged() {
devices = ControllerInterface.getAllDeviceStrings()
binding.dropdownDevice.setAdapter(
ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item, devices)
)
}
private fun setSelectedDevice(deviceString: String) { private fun setSelectedDevice(deviceString: String) {
selectedDevice = deviceString selectedDevice = deviceString
@ -72,7 +94,7 @@ class AdvancedMappingDialog(
} }
private fun setControls(controls: Array<CoreDevice.Control>) = private fun setControls(controls: Array<CoreDevice.Control>) =
controlAdapter.setControls(controls.map { it.getName() }.toTypedArray()) controlAdapter.setControls(controls)
private fun onControlClicked(control: String) { private fun onControlClicked(control: String) {
val expression = val expression =

View File

@ -0,0 +1,25 @@
package org.dolphinemu.dolphinemu.features.input.ui
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import org.dolphinemu.dolphinemu.databinding.ViewControlStateBarHorizontalBinding
class ControlStateBarHorizontal @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
private val binding =
ViewControlStateBarHorizontalBinding.inflate(LayoutInflater.from(context), this)
private val impl = ControlStateBarImpl(binding.viewFilled, binding.viewUnfilled)
var state: Float
get() = impl.state
set(value) {
impl.state = value
}
}

View File

@ -0,0 +1,28 @@
package org.dolphinemu.dolphinemu.features.input.ui
import android.view.View
import android.widget.LinearLayout
import androidx.core.math.MathUtils
class ControlStateBarImpl(private val filledView: View, private val unfilledView: View) {
private var _state = 0.0f
var state: Float
get() = _state
set(value) {
val clampedState = MathUtils.clamp(value, 0.0f, 1.0f)
if (clampedState != _state) {
setLinearLayoutWeight(filledView, clampedState)
setLinearLayoutWeight(unfilledView, 1.0f - clampedState)
}
_state = clampedState
}
companion object {
private fun setLinearLayoutWeight(view: View, weight: Float) {
val layoutParams = view.layoutParams as LinearLayout.LayoutParams
layoutParams.weight = weight
view.layoutParams = layoutParams
}
}
}

View File

@ -0,0 +1,30 @@
package org.dolphinemu.dolphinemu.features.input.ui
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import org.dolphinemu.dolphinemu.databinding.ViewControlStateBarVerticalBinding
class ControlStateBarVertical @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
init {
// Setting this in the XML file has no effect for some reason
orientation = VERTICAL
}
private val binding =
ViewControlStateBarVerticalBinding.inflate(LayoutInflater.from(context), this)
private val impl = ControlStateBarImpl(binding.viewFilled, binding.viewUnfilled)
var state: Float
get() = impl.state
set(value) {
impl.state = value
}
}

View File

@ -3,18 +3,30 @@
package org.dolphinemu.dolphinemu.features.input.ui.viewholder package org.dolphinemu.dolphinemu.features.input.ui.viewholder
import android.view.View import android.view.View
import androidx.lifecycle.Lifecycle
import org.dolphinemu.dolphinemu.databinding.ListItemMappingBinding import org.dolphinemu.dolphinemu.databinding.ListItemMappingBinding
import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface
import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHolder import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHolder
import kotlin.math.abs
class InputMappingControlSettingViewHolder( class InputMappingControlSettingViewHolder(
private val binding: ListItemMappingBinding, private val binding: ListItemMappingBinding,
adapter: SettingsAdapter adapter: SettingsAdapter
) : SettingViewHolder(binding.getRoot(), adapter) { ) : SettingViewHolder(binding.getRoot(), adapter) {
lateinit var setting: InputMappingControlSetting lateinit var setting: InputMappingControlSetting
private var previousState = Float.POSITIVE_INFINITY
init {
ControllerInterface.inputStateChanged.observe(this) {
updateInputValue()
}
}
override val item: SettingsItem override val item: SettingsItem
get() = setting get() = setting
@ -25,7 +37,12 @@ class InputMappingControlSettingViewHolder(
binding.textSettingDescription.text = setting.value binding.textSettingDescription.text = setting.value
binding.buttonAdvancedSettings.setOnClickListener { clicked: View -> onLongClick(clicked) } binding.buttonAdvancedSettings.setOnClickListener { clicked: View -> onLongClick(clicked) }
if (!setting.isInput) {
binding.controlStateBar.visibility = View.INVISIBLE
}
setStyle(binding.textSettingName, setting) setStyle(binding.textSettingName, setting)
updateInputValue()
} }
override fun onClick(clicked: View) { override fun onClick(clicked: View) {
@ -52,4 +69,23 @@ class InputMappingControlSettingViewHolder(
return true return true
} }
private fun updateInputValue() {
if (adapter.getFragmentLifecycle().currentState == Lifecycle.State.DESTROYED) {
throw IllegalStateException("InputMappingControlSettingViewHolder leak")
}
if (setting.isInput) {
val state = setting.state.toFloat()
if (abs(state - previousState) >= stateUpdateThreshold) {
previousState = state
binding.controlStateBar.state = state
}
}
}
companion object {
// For performance, require state to change by a certain threshold before we update the UI
private val stateUpdateThreshold = 0.01f
}
} }

View File

@ -32,7 +32,7 @@ class RiivolutionAdapter(private val context: Context, private val patches: Riiv
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RiivolutionViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RiivolutionViewHolder {
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
val binding = ListItemRiivolutionBinding.inflate(inflater) val binding = ListItemRiivolutionBinding.inflate(inflater, parent, false)
return RiivolutionViewHolder(binding.root, binding) return RiivolutionViewHolder(binding.root, binding)
} }

View File

@ -11,7 +11,9 @@ import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent
import android.view.Menu import android.view.Menu
import android.view.MotionEvent
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
@ -22,11 +24,11 @@ import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.google.android.material.appbar.CollapsingToolbarLayout import com.google.android.material.appbar.CollapsingToolbarLayout
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.dolphinemu.dolphinemu.NativeLibrary import org.dolphinemu.dolphinemu.NativeLibrary
import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.ActivitySettingsBinding import org.dolphinemu.dolphinemu.databinding.ActivitySettingsBinding
import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface
import org.dolphinemu.dolphinemu.features.settings.model.Settings import org.dolphinemu.dolphinemu.features.settings.model.Settings
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsFragment.Companion.newInstance import org.dolphinemu.dolphinemu.features.settings.ui.SettingsFragment.Companion.newInstance
import org.dolphinemu.dolphinemu.ui.main.MainPresenter import org.dolphinemu.dolphinemu.ui.main.MainPresenter
@ -197,6 +199,16 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
} }
} }
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
ControllerInterface.dispatchKeyEvent(event)
return super.dispatchKeyEvent(event)
}
override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
ControllerInterface.dispatchGenericMotionEvent(event)
return super.dispatchGenericMotionEvent(event)
}
private fun canonicalizeIfPossible(uri: Uri): Uri { private fun canonicalizeIfPossible(uri: Uri): Uri {
val canonicalizedUri = contentResolver.canonicalize(uri) val canonicalizedUri = contentResolver.canonicalize(uri)
return canonicalizedUri ?: uri return canonicalizedUri ?: uri

View File

@ -16,6 +16,7 @@ import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.datepicker.CalendarConstraints import com.google.android.material.datepicker.CalendarConstraints
@ -67,57 +68,48 @@ class SettingsAdapter(
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
return when (viewType) { return when (viewType) {
SettingsItem.TYPE_HEADER -> HeaderViewHolder( SettingsItem.TYPE_HEADER -> HeaderViewHolder(
ListItemHeaderBinding.inflate(inflater), ListItemHeaderBinding.inflate(inflater, parent, false),
this this
) )
SettingsItem.TYPE_SWITCH -> SwitchSettingViewHolder( SettingsItem.TYPE_SWITCH -> SwitchSettingViewHolder(
ListItemSettingSwitchBinding.inflate(inflater), ListItemSettingSwitchBinding.inflate(inflater, parent, false),
this this
) )
SettingsItem.TYPE_STRING_SINGLE_CHOICE, SettingsItem.TYPE_STRING_SINGLE_CHOICE,
SettingsItem.TYPE_SINGLE_CHOICE_DYNAMIC_DESCRIPTIONS, SettingsItem.TYPE_SINGLE_CHOICE_DYNAMIC_DESCRIPTIONS,
SettingsItem.TYPE_SINGLE_CHOICE -> SingleChoiceViewHolder( SettingsItem.TYPE_SINGLE_CHOICE -> SingleChoiceViewHolder(
ListItemSettingBinding.inflate(inflater), ListItemSettingBinding.inflate(inflater, parent, false),
this this
) )
SettingsItem.TYPE_SLIDER -> SliderViewHolder( SettingsItem.TYPE_SLIDER -> SliderViewHolder(
ListItemSettingBinding.inflate( ListItemSettingBinding.inflate(inflater, parent, false),
inflater this,
), this, context context
) )
SettingsItem.TYPE_SUBMENU -> SubmenuViewHolder( SettingsItem.TYPE_SUBMENU -> SubmenuViewHolder(
ListItemSubmenuBinding.inflate( ListItemSubmenuBinding.inflate(inflater, parent, false),
inflater this
), this
) )
SettingsItem.TYPE_INPUT_MAPPING_CONTROL -> InputMappingControlSettingViewHolder( SettingsItem.TYPE_INPUT_MAPPING_CONTROL -> InputMappingControlSettingViewHolder(
ListItemMappingBinding.inflate(inflater), ListItemMappingBinding.inflate(inflater, parent, false),
this this
) )
SettingsItem.TYPE_FILE_PICKER -> FilePickerViewHolder( SettingsItem.TYPE_FILE_PICKER -> FilePickerViewHolder(
ListItemSettingBinding.inflate( ListItemSettingBinding.inflate(inflater, parent, false),
inflater this
), this
) )
SettingsItem.TYPE_RUN_RUNNABLE -> RunRunnableViewHolder( SettingsItem.TYPE_RUN_RUNNABLE -> RunRunnableViewHolder(
ListItemSettingBinding.inflate( ListItemSettingBinding.inflate(inflater, parent, false),
inflater this, context
), this, context
) )
SettingsItem.TYPE_STRING -> InputStringSettingViewHolder( SettingsItem.TYPE_STRING -> InputStringSettingViewHolder(
ListItemSettingBinding.inflate( ListItemSettingBinding.inflate(inflater, parent, false), this
inflater
), this
) )
SettingsItem.TYPE_HYPERLINK_HEADER -> HeaderHyperLinkViewHolder( SettingsItem.TYPE_HYPERLINK_HEADER -> HeaderHyperLinkViewHolder(
ListItemHeaderBinding.inflate( ListItemHeaderBinding.inflate(inflater, parent, false), this
inflater
), this
) )
SettingsItem.TYPE_DATETIME_CHOICE -> DateTimeSettingViewHolder( SettingsItem.TYPE_DATETIME_CHOICE -> DateTimeSettingViewHolder(
ListItemSettingBinding.inflate( ListItemSettingBinding.inflate(inflater, parent, false), this
inflater
), this
) )
else -> throw IllegalArgumentException("Invalid view type: $viewType") else -> throw IllegalArgumentException("Invalid view type: $viewType")
} }
@ -543,6 +535,25 @@ class SettingsAdapter(
} }
} }
override fun onViewRecycled(holder: SettingViewHolder) {
super.onViewRecycled(holder)
holder.onViewRecycled()
}
override fun onViewAttachedToWindow(holder: SettingViewHolder) {
super.onViewAttachedToWindow(holder)
holder.onViewAttachedToWindow()
}
override fun onViewDetachedFromWindow(holder: SettingViewHolder) {
super.onViewDetachedFromWindow(holder)
holder.onViewDetachedFromWindow()
}
fun getFragmentLifecycle(): Lifecycle {
return fragmentView.getFragmentLifecycle()
}
private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int { private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int {
val valuesId = item.valuesId val valuesId = item.valuesId

View File

@ -18,6 +18,7 @@ import androidx.core.view.updatePadding
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -25,7 +26,6 @@ import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.FragmentSettingsBinding import org.dolphinemu.dolphinemu.databinding.FragmentSettingsBinding
import org.dolphinemu.dolphinemu.features.settings.model.Settings import org.dolphinemu.dolphinemu.features.settings.model.Settings
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
import org.dolphinemu.dolphinemu.ui.main.MainActivity
import org.dolphinemu.dolphinemu.ui.main.MainPresenter import org.dolphinemu.dolphinemu.ui.main.MainPresenter
import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult
import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable
@ -200,6 +200,10 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
.show() .show()
} }
override fun getFragmentLifecycle(): Lifecycle {
return lifecycle
}
private fun askForDriverFile() { private fun askForDriverFile() {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply { val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)

View File

@ -4,6 +4,7 @@ package org.dolphinemu.dolphinemu.features.settings.ui
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
import org.dolphinemu.dolphinemu.features.settings.model.Settings import org.dolphinemu.dolphinemu.features.settings.model.Settings
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult
@ -119,4 +120,9 @@ interface SettingsFragmentView {
* Shows a dialog asking the user to install or uninstall a GPU driver * Shows a dialog asking the user to install or uninstall a GPU driver
*/ */
fun showGpuDriverDialog() fun showGpuDriverDialog()
/**
* Returns the Lifecycle for the Fragment.
*/
fun getFragmentLifecycle(): Lifecycle
} }

View File

@ -9,15 +9,17 @@ import android.view.View
import android.view.View.OnLongClickListener import android.view.View.OnLongClickListener
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView import androidx.lifecycle.LifecycleOwner
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.dolphinemu.dolphinemu.DolphinApplication import org.dolphinemu.dolphinemu.DolphinApplication
import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter
import org.dolphinemu.dolphinemu.utils.LifecycleViewHolder
abstract class SettingViewHolder(itemView: View, protected val adapter: SettingsAdapter) : abstract class SettingViewHolder(itemView: View, protected val adapter: SettingsAdapter) :
RecyclerView.ViewHolder(itemView), View.OnClickListener, OnLongClickListener { LifecycleViewHolder(itemView, adapter.getFragmentLifecycle()),
LifecycleOwner, View.OnClickListener, OnLongClickListener {
init { init {
itemView.setOnClickListener(this) itemView.setOnClickListener(this)

View File

@ -0,0 +1,59 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.utils
import android.view.View
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.recyclerview.widget.RecyclerView
/**
* A ViewHolder with an attached Lifecycle.
*
* @param itemView The view held by the ViewHolder.
* @param parentLifecycle If the passed-in Lifecycle changes state to DESTROYED, this ViewHolder
* will also change state to DESTROYED. This should normally be set to the
* Lifecycle of the containing Fragment or Activity to prevent instances
* of this class from leaking.
*/
abstract class LifecycleViewHolder(itemView: View, parentLifecycle: Lifecycle) :
RecyclerView.ViewHolder(itemView), LifecycleOwner {
private val lifecycleRegistry = LifecycleRegistry(this)
override val lifecycle: Lifecycle
get() = lifecycleRegistry
init {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
parentLifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
// Make sure this ViewHolder doesn't leak if its lifecycle has observers
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
})
}
/**
* To be called from a function overriding [RecyclerView.Adapter.onViewRecycled].
*/
fun onViewRecycled() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
}
/**
* To be called from a function overriding [RecyclerView.Adapter.onViewAttachedToWindow].
*/
fun onViewAttachedToWindow() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}
/**
* To be called from a function overriding [RecyclerView.Adapter.onViewDetachedFromWindow].
*/
fun onViewDetachedFromWindow() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
}
}

View File

@ -22,7 +22,7 @@
android:layout_marginTop="@dimen/spacing_large" android:layout_marginTop="@dimen/spacing_large"
android:textSize="16sp" android:textSize="16sp"
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:layout_toStartOf="@+id/button_advanced_settings" android:layout_toStartOf="@id/control_state_bar"
tools:text="Setting Name" /> tools:text="Setting Name" />
<TextView <TextView
@ -30,16 +30,23 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_alignStart="@+id/text_setting_name" android:layout_alignStart="@id/text_setting_name"
android:layout_below="@+id/text_setting_name" android:layout_below="@id/text_setting_name"
android:layout_marginBottom="@dimen/spacing_large" android:layout_marginBottom="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_large" android:layout_marginEnd="@dimen/spacing_large"
android:layout_marginStart="@dimen/spacing_large" android:layout_marginStart="@dimen/spacing_large"
android:layout_marginTop="@dimen/spacing_small" android:layout_marginTop="@dimen/spacing_small"
android:layout_toStartOf="@+id/button_advanced_settings" android:layout_toStartOf="@id/control_state_bar"
android:textAlignment="viewStart" android:textAlignment="viewStart"
tools:text="@string/overclock_enable_description" /> tools:text="@string/overclock_enable_description" />
<org.dolphinemu.dolphinemu.features.input.ui.ControlStateBarVertical
android:id="@+id/control_state_bar"
android:layout_width="4dp"
android:layout_height="16dp"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/button_advanced_settings" />
<Button <Button
android:id="@+id/button_advanced_settings" android:id="@+id/button_advanced_settings"
style="?attr/materialIconButtonStyle" style="?attr/materialIconButtonStyle"

View File

@ -1,27 +1,48 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"
android:minHeight="54dp" android:minHeight="54dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:focusable="true" android:focusable="true"
android:clickable="true"> android:clickable="true">
<TextView <TextView
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="2"
style="@style/TextAppearance.MaterialComponents.Headline5" style="@style/TextAppearance.MaterialComponents.Headline5"
tools:text="Button A" tools:text="Button A"
android:layout_alignParentEnd="true" android:padding="@dimen/spacing_large"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginStart="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_large"
android:layout_marginTop="@dimen/spacing_large"
android:id="@+id/text_name" android:id="@+id/text_name"
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:textSize="16sp" /> android:textSize="16sp" />
</RelativeLayout> <FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:id="@+id/layout_state">
<org.dolphinemu.dolphinemu.features.input.ui.ControlStateBarHorizontal
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/control_state_bar" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|center_vertical"
style="@style/TextAppearance.MaterialComponents.Headline5"
tools:text="0.1234"
android:padding="@dimen/spacing_large"
android:id="@+id/text_state"
android:textAlignment="center"
android:textSize="16sp" />
</FrameLayout>
</LinearLayout>

View File

@ -22,7 +22,7 @@
android:layout_marginTop="@dimen/spacing_large" android:layout_marginTop="@dimen/spacing_large"
android:textSize="16sp" android:textSize="16sp"
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:layout_toStartOf="@+id/button_advanced_settings" android:layout_toStartOf="@id/control_state_bar"
tools:text="Setting Name" /> tools:text="Setting Name" />
<TextView <TextView
@ -30,16 +30,23 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_alignStart="@+id/text_setting_name" android:layout_alignStart="@id/text_setting_name"
android:layout_below="@+id/text_setting_name" android:layout_below="@id/text_setting_name"
android:layout_marginBottom="@dimen/spacing_large" android:layout_marginBottom="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_large" android:layout_marginEnd="@dimen/spacing_large"
android:layout_marginStart="@dimen/spacing_large" android:layout_marginStart="@dimen/spacing_large"
android:layout_marginTop="@dimen/spacing_small" android:layout_marginTop="@dimen/spacing_small"
android:layout_toStartOf="@+id/button_advanced_settings" android:layout_toStartOf="@id/control_state_bar"
android:textAlignment="viewStart" android:textAlignment="viewStart"
tools:text="@string/overclock_enable_description" /> tools:text="@string/overclock_enable_description" />
<org.dolphinemu.dolphinemu.features.input.ui.ControlStateBarVertical
android:id="@+id/control_state_bar"
android:layout_width="4dp"
android:layout_height="16dp"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/button_advanced_settings" />
<Button <Button
android:id="@+id/button_advanced_settings" android:id="@+id/button_advanced_settings"
style="?attr/materialIconButtonStyle" style="?attr/materialIconButtonStyle"

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="LinearLayout">
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0"
android:id="@+id/view_filled"
android:background="@color/dolphin_errorContainer" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:id="@+id/view_unfilled" />
</merge>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="LinearLayout">
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:id="@+id/view_unfilled" />
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0"
android:id="@+id/view_filled"
android:background="@color/dolphin_error" />
</merge>

View File

@ -64,6 +64,13 @@ Java_org_dolphinemu_dolphinemu_features_input_model_CoreDevice_00024Control_getN
return ToJString(env, GetControlPointer(env, obj)->GetName()); return ToJString(env, GetControlPointer(env, obj)->GetName());
} }
JNIEXPORT jdouble JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_CoreDevice_00024Control_getState(JNIEnv* env,
jobject obj)
{
return env, GetControlPointer(env, obj)->ToInput()->GetState();
}
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_CoreDevice_finalize(JNIEnv* env, jobject obj) Java_org_dolphinemu_dolphinemu_features_input_model_CoreDevice_finalize(JNIEnv* env, jobject obj)
{ {

View File

@ -442,6 +442,25 @@ std::shared_ptr<ciface::Core::Device> FindDevice(jint device_id)
return device; return device;
} }
void RegisterDevicesChangedCallbackIfNeeded(JNIEnv* env, jclass controller_interface_class)
{
static bool registered = false;
if (registered)
return;
registered = true;
const jclass global_controller_interface_class =
reinterpret_cast<jclass>(env->NewGlobalRef(controller_interface_class));
const jmethodID controller_interface_on_devices_changed =
env->GetStaticMethodID(global_controller_interface_class, "onDevicesChanged", "()V");
g_controller_interface.RegisterDevicesChangedCallback(
[global_controller_interface_class, controller_interface_on_devices_changed] {
IDCache::GetEnvForThread()->CallStaticVoidMethod(global_controller_interface_class,
controller_interface_on_devices_changed);
});
}
} // namespace } // namespace
namespace ciface::Android namespace ciface::Android
@ -903,6 +922,8 @@ InputBackend::InputBackend(ControllerInterface* controller_interface)
env->CallStaticVoidMethod(s_controller_interface_class, env->CallStaticVoidMethod(s_controller_interface_class,
s_controller_interface_register_input_device_listener); s_controller_interface_register_input_device_listener);
RegisterDevicesChangedCallbackIfNeeded(env, s_controller_interface_class);
} }
InputBackend::~InputBackend() InputBackend::~InputBackend()
@ -1002,7 +1023,7 @@ void InputBackend::PopulateDevices()
extern "C" { extern "C" {
JNIEXPORT jboolean JNICALL JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatchKeyEvent( Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatchKeyEventNative(
JNIEnv* env, jclass, jobject key_event) JNIEnv* env, jclass, jobject key_event)
{ {
const jint action = env->CallIntMethod(key_event, s_key_event_get_action); const jint action = env->CallIntMethod(key_event, s_key_event_get_action);
@ -1046,7 +1067,7 @@ Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatch
} }
JNIEXPORT jboolean JNICALL JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatchGenericMotionEvent( Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatchGenericMotionEventNative(
JNIEnv* env, jclass, jobject motion_event) JNIEnv* env, jclass, jobject motion_event)
{ {
const jint device_id = env->CallIntMethod(motion_event, s_input_event_get_device_id); const jint device_id = env->CallIntMethod(motion_event, s_input_event_get_device_id);
@ -1090,7 +1111,7 @@ Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatch
} }
JNIEXPORT jboolean JNICALL JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatchSensorEvent( Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatchSensorEventNative(
JNIEnv* env, jclass, jstring j_device_qualifier, jstring j_axis_name, jfloat value) JNIEnv* env, jclass, jstring j_device_qualifier, jstring j_axis_name, jfloat value)
{ {
ciface::Core::DeviceQualifier device_qualifier; ciface::Core::DeviceQualifier device_qualifier;