@@ -0,0 +1,150 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.model

import android.content.Context
import android.hardware.input.InputManager
import android.os.Build
import android.os.Handler
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import androidx.annotation.Keep
import org.dolphinemu.dolphinemu.DolphinApplication
import org.dolphinemu.dolphinemu.utils.LooperThread

/**
* This class interfaces with the native ControllerInterface,
* which is where the emulator core gets inputs from.
*/
object ControllerInterface {
private var inputDeviceListener: InputDeviceListener? = null
private lateinit var looperThread: LooperThread

/**
* Activities which want to pass on inputs to native code
* should call this in their own dispatchKeyEvent method.
*
* @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.
*/
external fun dispatchKeyEvent(event: KeyEvent): Boolean

/**
* Activities which want to pass on inputs to native code
* should call this in their own dispatchGenericMotionEvent method.
*
* @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.
*/
external fun dispatchGenericMotionEvent(event: MotionEvent): Boolean

/**
* [DolphinSensorEventListener] calls this for each axis of a received SensorEvent.
*
* @return true if the emulator core seems to be interested in this event.
* false if the sensor can be suspended to save battery.
*/
external fun dispatchSensorEvent(
deviceQualifier: String,
axisName: String,
value: Float
): Boolean

/**
* Called when a sensor is suspended or unsuspended.
*
* @param deviceQualifier A string used by native code for uniquely identifying devices.
* @param axisNames The name of all axes for the sensor.
* @param suspended Whether the sensor is now suspended.
*/
external fun notifySensorSuspendedState(
deviceQualifier: String,
axisNames: Array<String>,
suspended: Boolean
)

/**
* Rescans for input devices.
*/
external fun refreshDevices()

external fun getAllDeviceStrings(): Array<String>

external fun getDevice(deviceString: String): CoreDevice?

@Keep
@JvmStatic
private fun registerInputDeviceListener() {
looperThread = LooperThread("Hotplug thread")
looperThread.start()

if (inputDeviceListener == null) {
val im = DolphinApplication.getAppContext()
.getSystemService(Context.INPUT_SERVICE) as InputManager?

inputDeviceListener = InputDeviceListener()
im!!.registerInputDeviceListener(inputDeviceListener, Handler(looperThread.looper))
}
}

@Keep
@JvmStatic
private fun unregisterInputDeviceListener() {
if (inputDeviceListener != null) {
val im = DolphinApplication.getAppContext()
.getSystemService(Context.INPUT_SERVICE) as InputManager?

im!!.unregisterInputDeviceListener(inputDeviceListener)
inputDeviceListener = null
}
}

@Keep
@JvmStatic
private fun getVibratorManager(device: InputDevice): DolphinVibratorManager {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
DolphinVibratorManagerPassthrough(device.vibratorManager)
} else {
DolphinVibratorManagerCompat(device.vibrator)
}
}

@Keep
@JvmStatic
private fun getSystemVibratorManager(): DolphinVibratorManager {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager = DolphinApplication.getAppContext()
.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager?
if (vibratorManager != null)
return DolphinVibratorManagerPassthrough(vibratorManager)
}
val vibrator = DolphinApplication.getAppContext()
.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
return DolphinVibratorManagerCompat(vibrator)
}

@Keep
@JvmStatic
private fun vibrate(vibrator: Vibrator) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE))
} else {
vibrator.vibrate(100)
}
}

private class InputDeviceListener : InputManager.InputDeviceListener {
// Simple implementation for now. We could do something fancier if we wanted to.
override fun onInputDeviceAdded(deviceId: Int) = refreshDevices()

// Simple implementation for now. We could do something fancier if we wanted to.
override fun onInputDeviceRemoved(deviceId: Int) = refreshDevices()

// Simple implementation for now. We could do something fancier if we wanted to.
override fun onInputDeviceChanged(deviceId: Int) = refreshDevices()
}
}

This file was deleted.

@@ -0,0 +1,28 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.model

import androidx.annotation.Keep

/**
* Represents a C++ ciface::Core::Device.
*/
@Keep
class CoreDevice private constructor(private val pointer: Long) {
/**
* Represents a C++ ciface::Core::Device::Control.
*
* This class is marked inner to ensure that the CoreDevice parent does not get garbage collected
* while a Control is still accessible. (CoreDevice's finalizer may delete the native controls.)
*/
@Keep
inner class Control private constructor(private val pointer: Long) {
external fun getName(): String
}

protected external fun finalize()

external fun getInputs(): Array<Control>

external fun getOutputs(): Array<Control>
}

This file was deleted.

Large diffs are not rendered by default.

This file was deleted.

@@ -0,0 +1,16 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.model

import android.os.Vibrator
import androidx.annotation.Keep

/**
* A wrapper around [android.os.VibratorManager], for backwards compatibility.
*/
@Keep
interface DolphinVibratorManager {
fun getVibrator(vibratorId: Int): Vibrator

fun getVibratorIds(): IntArray
}

This file was deleted.

@@ -0,0 +1,24 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.model

import android.os.Vibrator

class DolphinVibratorManagerCompat(vibrator: Vibrator) : DolphinVibratorManager {
private val vibrator: Vibrator
private val vibratorIds: IntArray

init {
this.vibrator = vibrator
vibratorIds = if (vibrator.hasVibrator()) intArrayOf(0) else intArrayOf()
}

override fun getVibrator(vibratorId: Int): Vibrator {
if (vibratorId > vibratorIds.size)
throw IndexOutOfBoundsException()

return vibrator
}

override fun getVibratorIds(): IntArray = vibratorIds
}

This file was deleted.

@@ -0,0 +1,16 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.model

import android.os.Build
import android.os.Vibrator
import android.os.VibratorManager
import androidx.annotation.RequiresApi

@RequiresApi(api = Build.VERSION_CODES.S)
class DolphinVibratorManagerPassthrough(private val vibratorManager: VibratorManager) :
DolphinVibratorManager {
override fun getVibrator(vibratorId: Int): Vibrator = vibratorManager.getVibrator(vibratorId)

override fun getVibratorIds(): IntArray = vibratorManager.vibratorIds
}

This file was deleted.

@@ -0,0 +1,25 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.model

import org.dolphinemu.dolphinemu.features.input.model.controlleremu.NumericSetting
import org.dolphinemu.dolphinemu.features.settings.model.AbstractBooleanSetting
import org.dolphinemu.dolphinemu.features.settings.model.Settings

class InputMappingBooleanSetting(private val numericSetting: NumericSetting) :
AbstractBooleanSetting {
override val boolean: Boolean
get() = numericSetting.getBooleanValue()

override fun setBoolean(settings: Settings, newValue: Boolean) =
numericSetting.setBooleanValue(newValue)

override val isOverridden: Boolean = false

override val isRuntimeEditable: Boolean = true

override fun delete(settings: Settings): Boolean {
numericSetting.setBooleanValue(numericSetting.getBooleanDefaultValue())
return true
}
}

This file was deleted.

@@ -0,0 +1,25 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.model

import org.dolphinemu.dolphinemu.features.input.model.controlleremu.NumericSetting
import org.dolphinemu.dolphinemu.features.settings.model.AbstractFloatSetting
import org.dolphinemu.dolphinemu.features.settings.model.Settings

// Yes, floats are not the same thing as doubles... They're close enough, though
class InputMappingDoubleSetting(private val numericSetting: NumericSetting) : AbstractFloatSetting {
override val float: Float
get() = numericSetting.getDoubleValue().toFloat()

override fun setFloat(settings: Settings, newValue: Float) =
numericSetting.setDoubleValue(newValue.toDouble())

override val isOverridden: Boolean = false

override val isRuntimeEditable: Boolean = true

override fun delete(settings: Settings): Boolean {
numericSetting.setDoubleValue(numericSetting.getDoubleDefaultValue())
return true
}
}

This file was deleted.

@@ -0,0 +1,23 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.model

import org.dolphinemu.dolphinemu.features.input.model.controlleremu.NumericSetting
import org.dolphinemu.dolphinemu.features.settings.model.AbstractIntSetting
import org.dolphinemu.dolphinemu.features.settings.model.Settings

class InputMappingIntSetting(private val numericSetting: NumericSetting) : AbstractIntSetting {
override val int: Int
get() = numericSetting.getIntValue()

override fun setInt(settings: Settings, newValue: Int) = numericSetting.setIntValue(newValue)

override val isOverridden: Boolean = false

override val isRuntimeEditable: Boolean = true

override fun delete(settings: Settings): Boolean {
numericSetting.setIntValue(numericSetting.getIntDefaultValue())
return true
}
}

This file was deleted.

@@ -0,0 +1,82 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.model

object InputOverrider {
external fun registerGameCube(controllerIndex: Int)

external fun registerWii(controllerIndex: Int)

external fun unregisterGameCube(controllerIndex: Int)

external fun unregisterWii(controllerIndex: Int)

external fun setControlState(controllerIndex: Int, control: Int, state: Double)

external fun clearControlState(controllerIndex: Int, control: Int)

// Angle is in radians and should be non-negative
external fun getGateRadiusAtAngle(emuPadId: Int, stick: Int, angle: Double): Double

object ControlId {
const val GCPAD_A_BUTTON = 0
const val GCPAD_B_BUTTON = 1
const val GCPAD_X_BUTTON = 2
const val GCPAD_Y_BUTTON = 3
const val GCPAD_Z_BUTTON = 4
const val GCPAD_START_BUTTON = 5
const val GCPAD_DPAD_UP = 6
const val GCPAD_DPAD_DOWN = 7
const val GCPAD_DPAD_LEFT = 8
const val GCPAD_DPAD_RIGHT = 9
const val GCPAD_L_DIGITAL = 10
const val GCPAD_R_DIGITAL = 11
const val GCPAD_L_ANALOG = 12
const val GCPAD_R_ANALOG = 13
const val GCPAD_MAIN_STICK_X = 14
const val GCPAD_MAIN_STICK_Y = 15
const val GCPAD_C_STICK_X = 16
const val GCPAD_C_STICK_Y = 17

const val WIIMOTE_A_BUTTON = 18
const val WIIMOTE_B_BUTTON = 19
const val WIIMOTE_ONE_BUTTON = 20
const val WIIMOTE_TWO_BUTTON = 21
const val WIIMOTE_PLUS_BUTTON = 22
const val WIIMOTE_MINUS_BUTTON = 23
const val WIIMOTE_HOME_BUTTON = 24
const val WIIMOTE_DPAD_UP = 25
const val WIIMOTE_DPAD_DOWN = 26
const val WIIMOTE_DPAD_LEFT = 27
const val WIIMOTE_DPAD_RIGHT = 28
const val WIIMOTE_IR_X = 29
const val WIIMOTE_IR_Y = 30

const val NUNCHUK_C_BUTTON = 31
const val NUNCHUK_Z_BUTTON = 32
const val NUNCHUK_STICK_X = 33
const val NUNCHUK_STICK_Y = 34

const val CLASSIC_A_BUTTON = 35
const val CLASSIC_B_BUTTON = 36
const val CLASSIC_X_BUTTON = 37
const val CLASSIC_Y_BUTTON = 38
const val CLASSIC_ZL_BUTTON = 39
const val CLASSIC_ZR_BUTTON = 40
const val CLASSIC_PLUS_BUTTON = 41
const val CLASSIC_MINUS_BUTTON = 42
const val CLASSIC_HOME_BUTTON = 43
const val CLASSIC_DPAD_UP = 44
const val CLASSIC_DPAD_DOWN = 45
const val CLASSIC_DPAD_LEFT = 46
const val CLASSIC_DPAD_RIGHT = 47
const val CLASSIC_L_DIGITAL = 48
const val CLASSIC_R_DIGITAL = 49
const val CLASSIC_L_ANALOG = 50
const val CLASSIC_R_ANALOG = 51
const val CLASSIC_LEFT_STICK_X = 52
const val CLASSIC_LEFT_STICK_Y = 53
const val CLASSIC_RIGHT_STICK_X = 54
const val CLASSIC_RIGHT_STICK_Y = 55
}
}

This file was deleted.

@@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.model

import org.dolphinemu.dolphinemu.features.input.model.controlleremu.EmulatedController

object MappingCommon {
/**
* Waits until the user presses one or more inputs or until a timeout,
* then returns the pressed inputs.
*
* When this is being called, a separate thread must be calling ControllerInterface's
* dispatchKeyEvent and dispatchGenericMotionEvent, otherwise no inputs will be registered.
*
* @param controller The device to detect inputs from.
* @param allDevices Whether to also detect inputs from devices other than the specified one.
* @return The input(s) pressed by the user in the form of an InputCommon expression,
* or an empty string if there were no inputs.
*/
external fun detectInput(controller: EmulatedController, allDevices: Boolean): String

external fun getExpressionForControl(
control: String,
device: String,
defaultDevice: String
): String

external fun save()
}
@@ -1,27 +1,18 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.model.controlleremu;
package org.dolphinemu.dolphinemu.features.input.model.controlleremu

import androidx.annotation.Keep;
import androidx.annotation.Keep

/**
* Represents a C++ ControllerEmu::Control.
*
* The lifetime of this class is managed by C++ code. Calling methods on it after it's destroyed
* in C++ is undefined behavior!
*/
public class Control
{
@Keep
private final long mPointer;
@Keep
class Control private constructor(private val pointer: Long) {
external fun getUiName(): String

@Keep
private Control(long pointer)
{
mPointer = pointer;
}

public native String getUiName();

public native ControlReference getControlReference();
external fun getControlReference(): ControlReference
}

This file was deleted.

@@ -0,0 +1,59 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.model.controlleremu

import androidx.annotation.Keep

/**
* Represents a C++ ControllerEmu::ControlGroup.
*
* The lifetime of this class is managed by C++ code. Calling methods on it after it's destroyed
* in C++ is undefined behavior!
*/
@Keep
class ControlGroup private constructor(private val pointer: Long) {
external fun getUiName(): String

external fun getGroupType(): Int

external fun getDefaultEnabledValue(): Int

external fun getEnabled(): Boolean

external fun setEnabled(value: Boolean)

external fun getControlCount(): Int

external fun getControl(i: Int): Control

external fun getNumericSettingCount(): Int

external fun getNumericSetting(i: Int): NumericSetting

/**
* If getGroupType returns TYPE_ATTACHMENTS, this returns the attachment selection setting.
* Otherwise, undefined behavior!
*/
external fun getAttachmentSetting(): NumericSetting

companion object {
const val TYPE_OTHER = 0
const val TYPE_STICK = 1
const val TYPE_MIXED_TRIGGERS = 2
const val TYPE_BUTTONS = 3
const val TYPE_FORCE = 4
const val TYPE_ATTACHMENTS = 5
const val TYPE_TILT = 6
const val TYPE_CURSOR = 7
const val TYPE_TRIGGERS = 8
const val TYPE_SLIDER = 9
const val TYPE_SHAKE = 10
const val TYPE_IMU_ACCELEROMETER = 11
const val TYPE_IMU_GYROSCOPE = 12
const val TYPE_IMU_CURSOR = 13

const val DEFAULT_ENABLED_ALWAYS = 0
const val DEFAULT_ENABLED_YES = 1
const val DEFAULT_ENABLED_NO = 2
}
}

This file was deleted.

@@ -0,0 +1,28 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.model.controlleremu

import androidx.annotation.Keep

/**
* Represents a C++ ControlReference.
*
* The lifetime of this class is managed by C++ code. Calling methods on it after it's destroyed
* in C++ is undefined behavior!
*/
@Keep
class ControlReference private constructor(private val pointer: Long) {
external fun getState(): Double

external fun getExpression(): String

/**
* Sets the expression for this control reference.
*
* @param expr The new expression
* @return null on success, a human-readable error on failure
*/
external fun setExpression(expr: String): String?

external fun isInput(): Boolean
}

This file was deleted.

@@ -0,0 +1,52 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.model.controlleremu

import androidx.annotation.Keep

/**
* Represents a C++ ControllerEmu::EmulatedController.
*
* The lifetime of this class is managed by C++ code. Calling methods on it after it's destroyed
* in C++ is undefined behavior!
*/
@Keep
class EmulatedController private constructor(private val pointer: Long) {
external fun getDefaultDevice(): String

external fun setDefaultDevice(device: String)

external fun getGroupCount(): Int

external fun getGroup(index: Int): ControlGroup

external fun updateSingleControlReference(controlReference: ControlReference)

external fun loadDefaultSettings()

external fun clearSettings()

external fun loadProfile(path: String)

external fun saveProfile(path: String)

companion object {
@JvmStatic
external fun getGcPad(controllerIndex: Int): EmulatedController

@JvmStatic
external fun getWiimote(controllerIndex: Int): EmulatedController

@JvmStatic
external fun getWiimoteAttachment(
controllerIndex: Int,
attachmentIndex: Int
): EmulatedController

@JvmStatic
external fun getSelectedWiimoteAttachment(controllerIndex: Int): Int

@JvmStatic
external fun getSidewaysWiimoteSetting(controllerIndex: Int): NumericSetting
}
}

This file was deleted.

@@ -0,0 +1,97 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.model.controlleremu

import androidx.annotation.Keep

/**
* Represents a C++ ControllerEmu::NumericSetting.
*
* The lifetime of this class is managed by C++ code. Calling methods on it after it's destroyed
* in C++ is undefined behavior!
*/
@Keep
class NumericSetting private constructor(private val pointer: Long) {
/**
* @return The name used in the UI.
*/
external fun getUiName(): String

/**
* @return A string applied to the number in the UI (unit of measure).
*/
external fun getUiSuffix(): String

/**
* @return Detailed description of the setting.
*/
external fun getUiDescription(): String

/**
* @return TYPE_INT, TYPE_DOUBLE or TYPE_BOOLEAN
*/
external fun getType(): Int

external fun getControlReference(): ControlReference

/**
* If the type is TYPE_INT, gets the current value. Otherwise, undefined behavior!
*/
external fun getIntValue(): Int

/**
* If the type is TYPE_INT, sets the current value. Otherwise, undefined behavior!
*/
external fun setIntValue(value: Int)

/**
* If the type is TYPE_INT, gets the default value. Otherwise, undefined behavior!
*/
external fun getIntDefaultValue(): Int

/**
* If the type is TYPE_DOUBLE, gets the current value. Otherwise, undefined behavior!
*/
external fun getDoubleValue(): Double

/**
* If the type is TYPE_DOUBLE, sets the current value. Otherwise, undefined behavior!
*/
external fun setDoubleValue(value: Double)

/**
* If the type is TYPE_DOUBLE, gets the default value. Otherwise, undefined behavior!
*/
external fun getDoubleDefaultValue(): Double

/**
* If the type is TYPE_DOUBLE, returns the minimum valid value. Otherwise, undefined behavior!
*/
external fun getDoubleMin(): Double

/**
* If the type is TYPE_DOUBLE, returns the maximum valid value. Otherwise, undefined behavior!
*/
external fun getDoubleMax(): Double

/**
* If the type is TYPE_BOOLEAN, gets the current value. Otherwise, undefined behavior!
*/
external fun getBooleanValue(): Boolean

/**
* If the type is TYPE_BOOLEAN, sets the current value. Otherwise, undefined behavior!
*/
external fun setBooleanValue(value: Boolean)

/**
* If the type is TYPE_BOOLEAN, gets the default value. Otherwise, undefined behavior!
*/
external fun getBooleanDefaultValue(): Boolean

companion object {
const val TYPE_INT = 0
const val TYPE_DOUBLE = 1
const val TYPE_BOOLEAN = 2
}
}

This file was deleted.

@@ -0,0 +1,42 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.model.view

import android.content.Context
import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface
import org.dolphinemu.dolphinemu.features.input.model.controlleremu.EmulatedController
import org.dolphinemu.dolphinemu.features.settings.model.Settings
import org.dolphinemu.dolphinemu.features.settings.model.view.StringSingleChoiceSetting

class InputDeviceSetting(
context: Context,
titleId: Int,
descriptionId: Int,
private val controller: EmulatedController
) : StringSingleChoiceSetting(context, null, titleId, descriptionId, null, null, null) {
init {
refreshChoicesAndValues()
}

override val selectedChoice: String
get() = controller.getDefaultDevice()

override val selectedValue: String
get() = controller.getDefaultDevice()

override fun setSelectedValue(settings: Settings, selection: String) =
controller.setDefaultDevice(selection)

override fun refreshChoicesAndValues() {
val devices = ControllerInterface.getAllDeviceStrings()

choices = devices
values = devices
}

override val isEditable: Boolean = true

override fun canClear(): Boolean = true

override fun clear(settings: Settings) = setSelectedValue(settings, "")
}

This file was deleted.

@@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.model.view

import org.dolphinemu.dolphinemu.features.input.model.controlleremu.Control
import org.dolphinemu.dolphinemu.features.input.model.controlleremu.EmulatedController
import org.dolphinemu.dolphinemu.features.settings.model.AbstractSetting
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem

class InputMappingControlSetting(var control: Control, val controller: EmulatedController) :
SettingsItem(control.getUiName(), "") {
val controlReference get() = control.getControlReference()

var value: String
get() = controlReference.getExpression()
set(expr) {
controlReference.setExpression(expr)
controller.updateSingleControlReference(controlReference)
}

fun clearValue() {
value = ""
}

override val type: Int = TYPE_INPUT_MAPPING_CONTROL

override val setting: AbstractSetting? = null

override val isEditable: Boolean = true

val isInput: Boolean
get() = controlReference.isInput()
}

This file was deleted.

@@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.ui

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.dolphinemu.dolphinemu.databinding.ListItemAdvancedMappingControlBinding
import java.util.function.Consumer

class AdvancedMappingControlAdapter(private val onClickCallback: Consumer<String>) :
RecyclerView.Adapter<AdvancedMappingControlViewHolder>() {
private var controls = emptyArray<String>()

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): AdvancedMappingControlViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ListItemAdvancedMappingControlBinding.inflate(inflater)
return AdvancedMappingControlViewHolder(binding, onClickCallback)
}

override fun onBindViewHolder(holder: AdvancedMappingControlViewHolder, position: Int) =
holder.bind(controls[position])

override fun getItemCount(): Int = controls.size

fun setControls(controls: Array<String>) {
this.controls = controls
notifyDataSetChanged()
}
}

This file was deleted.

@@ -0,0 +1,23 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.ui

import androidx.recyclerview.widget.RecyclerView
import org.dolphinemu.dolphinemu.databinding.ListItemAdvancedMappingControlBinding
import java.util.function.Consumer

class AdvancedMappingControlViewHolder(
private val binding: ListItemAdvancedMappingControlBinding,
onClickCallback: Consumer<String>
) : RecyclerView.ViewHolder(binding.root) {
private lateinit var name: String

init {
binding.root.setOnClickListener { onClickCallback.accept(name) }
}

fun bind(name: String) {
this.name = name
binding.textName.text = name
}
}

This file was deleted.

@@ -0,0 +1,122 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.ui

import android.content.Context
import android.view.View
import android.widget.AdapterView
import android.widget.AdapterView.OnItemClickListener
import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.divider.MaterialDividerItemDecoration
import org.dolphinemu.dolphinemu.databinding.DialogAdvancedMappingBinding
import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface
import org.dolphinemu.dolphinemu.features.input.model.CoreDevice
import org.dolphinemu.dolphinemu.features.input.model.MappingCommon
import org.dolphinemu.dolphinemu.features.input.model.controlleremu.ControlReference
import org.dolphinemu.dolphinemu.features.input.model.controlleremu.EmulatedController

class AdvancedMappingDialog(
context: Context,
private val binding: DialogAdvancedMappingBinding,
private val controlReference: ControlReference,
private val controller: EmulatedController
) : AlertDialog(context), OnItemClickListener {
private val devices: Array<String> = ControllerInterface.getAllDeviceStrings()
private val controlAdapter: AdvancedMappingControlAdapter
private lateinit var selectedDevice: String

init {
// TODO: Remove workaround for text filtering issue in material components when fixed
// https://github.com/material-components/material-components-android/issues/1464
binding.dropdownDevice.isSaveEnabled = false

binding.dropdownDevice.onItemClickListener = this

val deviceAdapter =
ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item, devices)
binding.dropdownDevice.setAdapter(deviceAdapter)

controlAdapter =
AdvancedMappingControlAdapter { control: String -> onControlClicked(control) }
binding.listControl.adapter = controlAdapter
binding.listControl.layoutManager = LinearLayoutManager(context)

val divider = MaterialDividerItemDecoration(context, LinearLayoutManager.VERTICAL)
divider.isLastItemDecorated = false
binding.listControl.addItemDecoration(divider)

binding.editExpression.setText(controlReference.getExpression())

selectDefaultDevice()
}

val expression: String
get() = binding.editExpression.text.toString()

override fun onItemClick(adapterView: AdapterView<*>?, view: View, position: Int, id: Long) =
setSelectedDevice(devices[position])

private fun setSelectedDevice(deviceString: String) {
selectedDevice = deviceString

val device = ControllerInterface.getDevice(deviceString)
if (device == null)
setControls(emptyArray())
else if (controlReference.isInput())
setControls(device.getInputs())
else
setControls(device.getOutputs())
}

private fun setControls(controls: Array<CoreDevice.Control>) =
controlAdapter.setControls(controls.map { it.getName() }.toTypedArray())

private fun onControlClicked(control: String) {
val expression =
MappingCommon.getExpressionForControl(control, selectedDevice, controller.getDefaultDevice())

val start = binding.editExpression.selectionStart.coerceAtLeast(0)
val end = binding.editExpression.selectionEnd.coerceAtLeast(0)
binding.editExpression.text?.replace(
start.coerceAtMost(end),
start.coerceAtLeast(end),
expression,
0,
expression.length
)
}

private fun selectDefaultDevice() {
val defaultDevice = controller.getDefaultDevice()
val isInput = controlReference.isInput()

if (listOf(*devices).contains(defaultDevice) &&
(isInput || deviceHasOutputs(defaultDevice))
) {
// The default device is available, and it's an appropriate choice. Pick it
setSelectedDevice(defaultDevice)
binding.dropdownDevice.setText(defaultDevice, false)
return
} else if (!isInput) {
// Find the first device that has an output. (Most built-in devices don't have any)
val deviceWithOutputs = devices.first { deviceHasOutputs(it) }
if (deviceWithOutputs.isNotEmpty()) {
setSelectedDevice(deviceWithOutputs)
binding.dropdownDevice.setText(deviceWithOutputs, false)
return
}
}

// Nothing found
setSelectedDevice("")
}

companion object {
private fun deviceHasOutputs(deviceString: String): Boolean {
val device = ControllerInterface.getDevice(deviceString)
return device != null && device.getOutputs().isNotEmpty()
}
}
}

This file was deleted.

@@ -0,0 +1,68 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.ui

import android.app.Activity
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import androidx.appcompat.app.AlertDialog
import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface
import org.dolphinemu.dolphinemu.features.input.model.MappingCommon
import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting

/**
* [AlertDialog] derivative that listens for
* motion events from controllers and joysticks.
*
* @param activity The current [Activity].
* @param setting The setting to show this dialog for.
* @param allDevices Whether to detect inputs from devices other than the configured one.
*/
class MotionAlertDialog(
private val activity: Activity,
private val setting: InputMappingControlSetting,
private val allDevices: Boolean
) : AlertDialog(activity) {
private var running = false

override fun onStart() {
super.onStart()

running = true
Thread {
val result = MappingCommon.detectInput(setting.controller, allDevices)
activity.runOnUiThread {
if (running) {
setting.value = result
dismiss()
}
}
}.start()
}

override fun onStop() {
super.onStop()
running = false
}

override fun dispatchKeyEvent(event: KeyEvent): Boolean {
ControllerInterface.dispatchKeyEvent(event)
if (event.keyCode == KeyEvent.KEYCODE_BACK && event.isLongPress) {
// Special case: Let the user cancel by long-pressing Back (intended for non-touch devices)
setting.clearValue()
dismiss()
}
return true
}

override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) {
// Special case: Let the user cancel by touching an on-screen button
return super.dispatchGenericMotionEvent(event)
}

ControllerInterface.dispatchGenericMotionEvent(event)
return true
}
}

This file was deleted.

@@ -0,0 +1,42 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.ui

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.dolphinemu.dolphinemu.databinding.ListItemProfileBinding

class ProfileAdapter(
private val context: Context,
private val presenter: ProfileDialogPresenter
) : RecyclerView.Adapter<ProfileViewHolder>() {
private val stockProfileNames: Array<String> = presenter.getProfileNames(true)
private val userProfileNames: Array<String> = presenter.getProfileNames(false)

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ListItemProfileBinding.inflate(inflater, parent, false)
return ProfileViewHolder(presenter, binding)
}

override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) {
var profilePosition = position
if (profilePosition < stockProfileNames.size) {
holder.bind(stockProfileNames[profilePosition], true)
return
}

profilePosition -= stockProfileNames.size

if (profilePosition < userProfileNames.size) {
holder.bind(userProfileNames[profilePosition], false)
return
}

holder.bindAsEmpty(context)
}

override fun getItemCount(): Int = stockProfileNames.size + userProfileNames.size + 1
}
Expand Up @@ -16,13 +16,13 @@ import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag
import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable

class ProfileDialog : BottomSheetDialogFragment() {
private var presenter: ProfileDialogPresenter? = null
private lateinit var presenter: ProfileDialogPresenter

private var _binding: DialogInputProfilesBinding? = null
private val binding get() = _binding!!

override fun onCreate(savedInstanceState: Bundle?) {
val menuTag = requireArguments().serializable<MenuTag>(KEY_MENU_TAG)
val menuTag = requireArguments().serializable<MenuTag>(KEY_MENU_TAG)!!

presenter = ProfileDialogPresenter(this, menuTag)

Expand All @@ -39,7 +39,7 @@ class ProfileDialog : BottomSheetDialogFragment() {
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.profileList.adapter = ProfileAdapter(context, presenter)
binding.profileList.adapter = ProfileAdapter(requireContext(), presenter)
binding.profileList.layoutManager = LinearLayoutManager(context)
val divider = MaterialDividerItemDecoration(requireActivity(), LinearLayoutManager.VERTICAL)
divider.isLastItemDecorated = false
Expand Down

This file was deleted.

@@ -0,0 +1,119 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.ui

import android.content.Context
import android.content.DialogInterface
import android.view.LayoutInflater
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.DialogInputStringBinding
import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivityView
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization
import java.io.File
import java.util.Locale

class ProfileDialogPresenter {
private val context: Context?
private val dialog: DialogFragment?
private val menuTag: MenuTag

constructor(menuTag: MenuTag) {
context = null
dialog = null
this.menuTag = menuTag
}

constructor(dialog: DialogFragment, menuTag: MenuTag) {
context = dialog.context
this.dialog = dialog
this.menuTag = menuTag
}

fun getProfileNames(stock: Boolean): Array<String> {
val profiles = File(getProfileDirectoryPath(stock)).listFiles { file: File ->
!file.isDirectory && file.name.endsWith(EXTENSION)
} ?: return emptyArray()

return profiles.map { it.name.substring(0, it.name.length - EXTENSION.length) }
.sortedBy { it.lowercase(Locale.getDefault()) }
.toTypedArray()
}

fun loadProfile(profileName: String, stock: Boolean) {
MaterialAlertDialogBuilder(context!!)
.setMessage(context.getString(R.string.input_profile_confirm_load, profileName))
.setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int ->
menuTag.correspondingEmulatedController
.loadProfile(getProfilePath(profileName, stock))
(dialog!!.requireActivity() as SettingsActivityView).onControllerSettingsChanged()
dialog.dismiss()
}
.setNegativeButton(R.string.no, null)
.show()
}

fun saveProfile(profileName: String) {
// If the user is saving over an existing profile, we should show an overwrite warning.
// If the user is creating a new profile, we normally shouldn't show a warning,
// but if they've entered the name of an existing profile, we should shown an overwrite warning.
val profilePath = getProfilePath(profileName, false)
if (!File(profilePath).exists()) {
menuTag.correspondingEmulatedController.saveProfile(profilePath)
dialog!!.dismiss()
} else {
MaterialAlertDialogBuilder(context!!)
.setMessage(context.getString(R.string.input_profile_confirm_save, profileName))
.setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int ->
menuTag.correspondingEmulatedController.saveProfile(profilePath)
dialog!!.dismiss()
}
.setNegativeButton(R.string.no, null)
.show()
}
}

fun saveProfileAndPromptForName() {
val inflater = LayoutInflater.from(context)
val binding = DialogInputStringBinding.inflate(inflater)
val input = binding.input

MaterialAlertDialogBuilder(context!!)
.setView(binding.root)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
saveProfile(input.text.toString())
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}

fun deleteProfile(profileName: String) {
MaterialAlertDialogBuilder(context!!)
.setMessage(context.getString(R.string.input_profile_confirm_delete, profileName))
.setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int ->
File(getProfilePath(profileName, false)).delete()
dialog!!.dismiss()
}
.setNegativeButton(R.string.no, null)
.show()
}

private val profileDirectoryName: String
get() = if (menuTag.isGCPadMenu) "GCPad" else if (menuTag.isWiimoteMenu) "Wiimote" else throw UnsupportedOperationException()

private fun getProfileDirectoryPath(stock: Boolean): String =
if (stock) {
"${DirectoryInitialization.getSysDirectory()}/Profiles/$profileDirectoryName/"
} else {
"${DirectoryInitialization.getUserDirectory()}/Config/Profiles/$profileDirectoryName/"
}

private fun getProfilePath(profileName: String, stock: Boolean): String =
getProfileDirectoryPath(stock) + profileName + EXTENSION

companion object {
private const val EXTENSION = ".ini"
}
}

This file was deleted.

@@ -0,0 +1,56 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.ui

import android.content.Context
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.ListItemProfileBinding

class ProfileViewHolder(
private val presenter: ProfileDialogPresenter,
private val binding: ListItemProfileBinding
) : RecyclerView.ViewHolder(binding.getRoot()) {
private var profileName: String? = null
private var stock = false

init {
binding.buttonLoad.setOnClickListener { loadProfile() }
binding.buttonSave.setOnClickListener { saveProfile() }
binding.buttonDelete.setOnClickListener { deleteProfile() }
}

fun bind(profileName: String, stock: Boolean) {
this.profileName = profileName
this.stock = stock

binding.textName.text = profileName

binding.buttonLoad.visibility = View.VISIBLE
binding.buttonSave.visibility = if (stock) View.GONE else View.VISIBLE
binding.buttonDelete.visibility = if (stock) View.GONE else View.VISIBLE
}

fun bindAsEmpty(context: Context) {
profileName = null
stock = false

binding.textName.text = context.getText(R.string.input_profile_new)

binding.buttonLoad.visibility = View.GONE
binding.buttonSave.visibility = View.VISIBLE
binding.buttonDelete.visibility = View.GONE
}

private fun loadProfile() = presenter.loadProfile(profileName!!, stock)

private fun saveProfile() {
if (profileName == null)
presenter.saveProfileAndPromptForName()
else
presenter.saveProfile(profileName!!)
}

private fun deleteProfile() = presenter.deleteProfile(profileName!!)
}

This file was deleted.

@@ -0,0 +1,55 @@
// SPDX-License-Identifier: GPL-2.0-or-later

package org.dolphinemu.dolphinemu.features.input.ui.viewholder

import android.view.View
import org.dolphinemu.dolphinemu.databinding.ListItemMappingBinding
import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHolder

class InputMappingControlSettingViewHolder(
private val binding: ListItemMappingBinding,
adapter: SettingsAdapter
) : SettingViewHolder(binding.getRoot(), adapter) {
lateinit var setting: InputMappingControlSetting

override val item: SettingsItem
get() = setting

override fun bind(item: SettingsItem) {
setting = item as InputMappingControlSetting

binding.textSettingName.text = setting.name
binding.textSettingDescription.text = setting.value
binding.buttonAdvancedSettings.setOnClickListener { clicked: View -> onLongClick(clicked) }

setStyle(binding.textSettingName, setting)
}

override fun onClick(clicked: View) {
if (!setting.isEditable) {
showNotRuntimeEditableError()
return
}

if (setting.isInput)
adapter.onInputMappingClick(setting, bindingAdapterPosition)
else
adapter.onAdvancedInputMappingClick(setting, bindingAdapterPosition)

setStyle(binding.textSettingName, setting)
}

override fun onLongClick(clicked: View): Boolean {
if (!setting.isEditable) {
showNotRuntimeEditableError()
return true
}

adapter.onAdvancedInputMappingClick(setting, bindingAdapterPosition)

return true
}
}
Expand Up @@ -4,14 +4,14 @@ package org.dolphinemu.dolphinemu.features.settings.model

object PostProcessing {
@JvmStatic
val shaderList: Array<String?>
val shaderList: Array<String>
external get

@JvmStatic
val anaglyphShaderList: Array<String?>
val anaglyphShaderList: Array<String>
external get

@JvmStatic
val passiveShaderList: Array<String?>
val passiveShaderList: Array<String>
external get
}
Expand Up @@ -17,9 +17,9 @@ open class StringSingleChoiceSetting : SettingsItem {
override val setting: AbstractSetting?
get() = stringSetting

var choices: Array<String?>?
var choices: Array<String>?
protected set
var values: Array<String?>?
var values: Array<String>?
protected set
val menuTag: MenuTag?
var noChoicesAvailableString = 0
Expand All @@ -37,8 +37,8 @@ open class StringSingleChoiceSetting : SettingsItem {
setting: AbstractStringSetting?,
titleId: Int,
descriptionId: Int,
choices: Array<String?>?,
values: Array<String?>?,
choices: Array<String>?,
values: Array<String>?,
menuTag: MenuTag? = null
) : super(context, titleId, descriptionId) {
stringSetting = setting
Expand All @@ -52,8 +52,8 @@ open class StringSingleChoiceSetting : SettingsItem {
setting: AbstractStringSetting,
titleId: Int,
descriptionId: Int,
choices: Array<String?>,
values: Array<String?>,
choices: Array<String>,
values: Array<String>,
noChoicesAvailableString: Int
) : this(context, setting, titleId, descriptionId, choices, values) {
this.noChoicesAvailableString = noChoicesAvailableString
Expand Down Expand Up @@ -102,8 +102,8 @@ open class StringSingleChoiceSetting : SettingsItem {
return -1
}

open fun setSelectedValue(settings: Settings?, selection: String?) {
stringSetting!!.setString(settings!!, selection!!)
open fun setSelectedValue(settings: Settings, selection: String) {
stringSetting!!.setString(settings, selection)
}

open fun refreshChoicesAndValues() {}
Expand Down
Expand Up @@ -258,7 +258,7 @@ class SettingsAdapter(
}

fun onInputMappingClick(item: InputMappingControlSetting, position: Int) {
if (item.controller.defaultDevice.isEmpty() && !fragmentView.isMappingAllDevices) {
if (item.controller.getDefaultDevice().isEmpty() && !fragmentView.isMappingAllDevices) {
MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
.setMessage(R.string.input_binding_no_device)
.setPositiveButton(R.string.ok, this)
Expand Down Expand Up @@ -474,7 +474,7 @@ class SettingsAdapter(
val value = scSetting.getValueAt(which)
if (scSetting.selectedValue != value) fragmentView.onSettingChanged()

scSetting.setSelectedValue(settings, value)
scSetting.setSelectedValue(settings!!, value!!)

closeDialog()
}
Expand Down