| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,27 +1,18 @@ | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| package org.dolphinemu.dolphinemu.features.input.model.controlleremu | ||
|
|
||
| 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! | ||
| */ | ||
| @Keep | ||
| class Control private constructor(private val pointer: Long) { | ||
| external fun getUiName(): String | ||
|
|
||
| external fun getControlReference(): ControlReference | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, "") | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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!!) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |