mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-07-23 14:19:46 -06:00
Merge pull request #13674 from JosJuice/android-visualize-input
Android: Show input indicators in controller settings
This commit is contained in:
@ -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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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()
|
||||||
|
@ -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 = ""
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 =
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
@ -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>
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
|
Reference in New Issue
Block a user