diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControlGroupEnabledSetting.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControlGroupEnabledSetting.java deleted file mode 100644 index 2e22ce84aa..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControlGroupEnabledSetting.java +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.features.input.model; - -import androidx.annotation.NonNull; - -import org.dolphinemu.dolphinemu.features.input.model.controlleremu.ControlGroup; -import org.dolphinemu.dolphinemu.features.settings.model.AbstractBooleanSetting; -import org.dolphinemu.dolphinemu.features.settings.model.Settings; - -public class ControlGroupEnabledSetting implements AbstractBooleanSetting -{ - private final ControlGroup mControlGroup; - - public ControlGroupEnabledSetting(ControlGroup controlGroup) - { - mControlGroup = controlGroup; - } - - @Override - public boolean getBoolean() - { - return mControlGroup.getEnabled(); - } - - @Override - public void setBoolean(@NonNull Settings settings, boolean newValue) - { - mControlGroup.setEnabled(newValue); - } - - @Override - public boolean isOverridden() - { - return false; - } - - @Override - public boolean isRuntimeEditable() - { - return true; - } - - @Override - public boolean delete(@NonNull Settings settings) - { - boolean newValue = mControlGroup.getDefaultEnabledValue() != ControlGroup.DEFAULT_ENABLED_NO; - mControlGroup.setEnabled(newValue); - - return true; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControlGroupEnabledSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControlGroupEnabledSetting.kt new file mode 100644 index 0000000000..43c983cc41 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControlGroupEnabledSetting.kt @@ -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.ControlGroup +import org.dolphinemu.dolphinemu.features.settings.model.AbstractBooleanSetting +import org.dolphinemu.dolphinemu.features.settings.model.Settings + +class ControlGroupEnabledSetting(private val controlGroup: ControlGroup) : AbstractBooleanSetting { + override val boolean: Boolean + get() = controlGroup.getEnabled() + + override fun setBoolean(settings: Settings, newValue: Boolean) = + controlGroup.setEnabled(newValue) + + override val isOverridden: Boolean = false + + override val isRuntimeEditable: Boolean = true + + override fun delete(settings: Settings): Boolean { + val newValue = controlGroup.getDefaultEnabledValue() != ControlGroup.DEFAULT_ENABLED_NO + controlGroup.setEnabled(newValue) + return true + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.java deleted file mode 100644 index 732ed97a7c..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.java +++ /dev/null @@ -1,178 +0,0 @@ -// 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 androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -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. - */ -public final class ControllerInterface -{ - private static final class InputDeviceListener implements InputManager.InputDeviceListener - { - @Override - public void onInputDeviceAdded(int deviceId) - { - // Simple implementation for now. We could do something fancier if we wanted to. - refreshDevices(); - } - - @Override - public void onInputDeviceRemoved(int deviceId) - { - // Simple implementation for now. We could do something fancier if we wanted to. - refreshDevices(); - } - - @Override - public void onInputDeviceChanged(int deviceId) - { - // Simple implementation for now. We could do something fancier if we wanted to. - refreshDevices(); - } - } - - private static InputDeviceListener mInputDeviceListener; - private static LooperThread mLooperThread; - - /** - * 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. - */ - public static native boolean dispatchKeyEvent(KeyEvent event); - - /** - * 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. - */ - public static native boolean dispatchGenericMotionEvent(MotionEvent event); - - /** - * {@link 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. - */ - public static native boolean dispatchSensorEvent(String deviceQualifier, String axisName, - float value); - - /** - * 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. - */ - public static native void notifySensorSuspendedState(String deviceQualifier, String[] axisNames, - boolean suspended); - - /** - * Rescans for input devices. - */ - public static native void refreshDevices(); - - public static native String[] getAllDeviceStrings(); - - @Nullable - public static native CoreDevice getDevice(String deviceString); - - @Keep - private static void registerInputDeviceListener() - { - if (mLooperThread == null) - { - mLooperThread = new LooperThread("Hotplug thread"); - mLooperThread.start(); - } - - if (mInputDeviceListener == null) - { - InputManager im = (InputManager) - DolphinApplication.getAppContext().getSystemService(Context.INPUT_SERVICE); - - mInputDeviceListener = new InputDeviceListener(); - im.registerInputDeviceListener(mInputDeviceListener, new Handler(mLooperThread.getLooper())); - } - } - - @Keep - private static void unregisterInputDeviceListener() - { - if (mInputDeviceListener != null) - { - InputManager im = (InputManager) - DolphinApplication.getAppContext().getSystemService(Context.INPUT_SERVICE); - - im.unregisterInputDeviceListener(mInputDeviceListener); - mInputDeviceListener = null; - } - } - - @Keep @NonNull - private static DolphinVibratorManager getVibratorManager(InputDevice device) - { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) - { - return new DolphinVibratorManagerPassthrough(device.getVibratorManager()); - } - else - { - return new DolphinVibratorManagerCompat(device.getVibrator()); - } - } - - @Keep @NonNull - private static DolphinVibratorManager getSystemVibratorManager() - { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) - { - VibratorManager vibratorManager = (VibratorManager) - DolphinApplication.getAppContext().getSystemService(Context.VIBRATOR_MANAGER_SERVICE); - - if (vibratorManager != null) - return new DolphinVibratorManagerPassthrough(vibratorManager); - } - - Vibrator vibrator = (Vibrator) - DolphinApplication.getAppContext().getSystemService(Context.VIBRATOR_SERVICE); - - return new DolphinVibratorManagerCompat(vibrator); - } - - @Keep - private static void vibrate(@NonNull Vibrator vibrator) - { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - { - vibrator.vibrate(VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE)); - } - else - { - vibrator.vibrate(100); - } - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt new file mode 100644 index 0000000000..3bca59f7b7 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt @@ -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, + suspended: Boolean + ) + + /** + * Rescans for input devices. + */ + external fun refreshDevices() + + external fun getAllDeviceStrings(): Array + + 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() + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/CoreDevice.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/CoreDevice.java deleted file mode 100644 index b69e9a0236..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/CoreDevice.java +++ /dev/null @@ -1,48 +0,0 @@ -// 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. - */ -public final class CoreDevice -{ - /** - * Represents a C++ ciface::Core::Device::Control. - * - * This class is non-static 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.) - */ - @SuppressWarnings("InnerClassMayBeStatic") - public final class Control - { - @Keep - private final long mPointer; - - @Keep - private Control(long pointer) - { - mPointer = pointer; - } - - public native String getName(); - } - - @Keep - private final long mPointer; - - @Keep - private CoreDevice(long pointer) - { - mPointer = pointer; - } - - @Override - protected native void finalize(); - - public native Control[] getInputs(); - - public native Control[] getOutputs(); -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/CoreDevice.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/CoreDevice.kt new file mode 100644 index 0000000000..1483422c50 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/CoreDevice.kt @@ -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 + + external fun getOutputs(): Array +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinSensorEventListener.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinSensorEventListener.java deleted file mode 100644 index 78d0e4785d..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinSensorEventListener.java +++ /dev/null @@ -1,440 +0,0 @@ -package org.dolphinemu.dolphinemu.features.input.model; - -import android.content.Context; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.os.Build; -import android.view.InputDevice; -import android.view.Surface; - -import androidx.annotation.Keep; - -import org.dolphinemu.dolphinemu.DolphinApplication; -import org.dolphinemu.dolphinemu.utils.Log; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class DolphinSensorEventListener implements SensorEventListener -{ - // Set of three axes. Creates a negative companion to each axis, and corrects for device rotation. - private static final int AXIS_SET_TYPE_DEVICE_COORDINATES = 0; - // Set of three axes. Creates a negative companion to each axis. - private static final int AXIS_SET_TYPE_OTHER_COORDINATES = 1; - - private static class AxisSetDetails - { - public final int firstAxisOfSet; - public final int axisSetType; - - public AxisSetDetails(int firstAxisOfSet, int axisSetType) - { - this.firstAxisOfSet = firstAxisOfSet; - this.axisSetType = axisSetType; - } - } - - private static class SensorDetails - { - public final int sensorType; - public final String[] axisNames; - public final AxisSetDetails[] axisSetDetails; - public boolean isSuspended = true; - - public SensorDetails(int sensorType, String[] axisNames, AxisSetDetails[] axisSetDetails) - { - this.sensorType = sensorType; - this.axisNames = axisNames; - this.axisSetDetails = axisSetDetails; - } - } - - private static int sDeviceRotation = Surface.ROTATION_0; - - private final SensorManager mSensorManager; - - private final HashMap mSensorDetails = new HashMap<>(); - - private final boolean mRotateCoordinatesForScreenOrientation; - - private String mDeviceQualifier = ""; - - // The fastest sampling rate Android lets us use without declaring the HIGH_SAMPLING_RATE_SENSORS - // permission is 200 Hz. This is also the sampling rate of a Wii Remote, so it fits us perfectly. - private static final int SAMPLING_PERIOD_US = 1000000 / 200; - - @Keep - public DolphinSensorEventListener() - { - mSensorManager = (SensorManager) - DolphinApplication.getAppContext().getSystemService(Context.SENSOR_SERVICE); - mRotateCoordinatesForScreenOrientation = true; - - addSensors(); - } - - @Keep - public DolphinSensorEventListener(InputDevice inputDevice) - { - mRotateCoordinatesForScreenOrientation = false; - - if (Build.VERSION.SDK_INT >= 31) - { - mSensorManager = inputDevice.getSensorManager(); - - // TODO: There is a bug where after suspending sensors, onSensorChanged can get called for - // a sensor that we never registered as a listener for. The way our code is currently written, - // this causes a NullPointerException, but if we checked for null we would instead have the - // problem of being spammed with onSensorChanged calls even though the sensor shouldn't be - // enabled. For now, let's comment out the ability to use InputDevice sensors. - - //addSensors(); - } - else - { - mSensorManager = null; - } - } - - private void addSensors() - { - tryAddSensor(Sensor.TYPE_ACCELEROMETER, new String[]{"Accel Right", "Accel Left", - "Accel Forward", "Accel Backward", "Accel Up", "Accel Down"}, - new AxisSetDetails[]{new AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES)}); - - tryAddSensor(Sensor.TYPE_GYROSCOPE, new String[]{"Gyro Pitch Up", "Gyro Pitch Down", - "Gyro Roll Right", "Gyro Roll Left", "Gyro Yaw Left", "Gyro Yaw Right"}, - new AxisSetDetails[]{new AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES)}); - - tryAddSensor(Sensor.TYPE_LIGHT, "Light"); - - tryAddSensor(Sensor.TYPE_PRESSURE, "Pressure"); - - tryAddSensor(Sensor.TYPE_TEMPERATURE, "Device Temperature"); - - tryAddSensor(Sensor.TYPE_PROXIMITY, "Proximity"); - - tryAddSensor(Sensor.TYPE_GRAVITY, new String[]{"Gravity Right", "Gravity Left", - "Gravity Forward", "Gravity Backward", "Gravity Up", "Gravity Down"}, - new AxisSetDetails[]{new AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES)}); - - tryAddSensor(Sensor.TYPE_LINEAR_ACCELERATION, - new String[]{"Linear Acceleration Right", "Linear Acceleration Left", - "Linear Acceleration Forward", "Linear Acceleration Backward", - "Linear Acceleration Up", "Linear Acceleration Down"}, - new AxisSetDetails[]{new AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES)}); - - // The values provided by this sensor can be interpreted as an Euler vector or a quaternion. - // The directions of X and Y are flipped to match the Wii Remote coordinate system. - tryAddSensor(Sensor.TYPE_ROTATION_VECTOR, - new String[]{"Rotation Vector X-", "Rotation Vector X+", "Rotation Vector Y-", - "Rotation Vector Y+", "Rotation Vector Z+", - "Rotation Vector Z-", "Rotation Vector R", "Rotation Vector Heading Accuracy"}, - new AxisSetDetails[]{new AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES)}); - - tryAddSensor(Sensor.TYPE_RELATIVE_HUMIDITY, "Relative Humidity"); - - tryAddSensor(Sensor.TYPE_AMBIENT_TEMPERATURE, "Ambient Temperature"); - - // The values provided by this sensor can be interpreted as an Euler vector or a quaternion. - // The directions of X and Y are flipped to match the Wii Remote coordinate system. - tryAddSensor(Sensor.TYPE_GAME_ROTATION_VECTOR, - new String[]{"Game Rotation Vector X-", "Game Rotation Vector X+", - "Game Rotation Vector Y-", "Game Rotation Vector Y+", "Game Rotation Vector Z+", - "Game Rotation Vector Z-", "Game Rotation Vector R"}, - new AxisSetDetails[]{new AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES)}); - - tryAddSensor(Sensor.TYPE_GYROSCOPE_UNCALIBRATED, - new String[]{"Gyro Uncalibrated Pitch Up", "Gyro Uncalibrated Pitch Down", - "Gyro Uncalibrated Roll Right", "Gyro Uncalibrated Roll Left", - "Gyro Uncalibrated Yaw Left", "Gyro Uncalibrated Yaw Right", - "Gyro Drift Pitch Up", "Gyro Drift Pitch Down", "Gyro Drift Roll Right", - "Gyro Drift Roll Left", "Gyro Drift Yaw Left", "Gyro Drift Yaw Right"}, - new AxisSetDetails[]{new AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES), - new AxisSetDetails(3, AXIS_SET_TYPE_DEVICE_COORDINATES)}); - - tryAddSensor(Sensor.TYPE_HEART_RATE, "Heart Rate"); - - if (Build.VERSION.SDK_INT >= 24) - { - tryAddSensor(Sensor.TYPE_HEART_BEAT, "Heart Beat"); - } - - if (Build.VERSION.SDK_INT >= 26) - { - tryAddSensor(Sensor.TYPE_ACCELEROMETER_UNCALIBRATED, - new String[]{"Accel Uncalibrated Right", "Accel Uncalibrated Left", - "Accel Uncalibrated Forward", "Accel Uncalibrated Backward", - "Accel Uncalibrated Up", "Accel Uncalibrated Down", - "Accel Bias Right", "Accel Bias Left", "Accel Bias Forward", - "Accel Bias Backward", "Accel Bias Up", "Accel Bias Down"}, - new AxisSetDetails[]{new AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES), - new AxisSetDetails(3, AXIS_SET_TYPE_DEVICE_COORDINATES)}); - } - - if (Build.VERSION.SDK_INT >= 30) - { - tryAddSensor(Sensor.TYPE_HINGE_ANGLE, "Hinge Angle"); - } - - if (Build.VERSION.SDK_INT >= 33) - { - // The values provided by this sensor can be interpreted as an Euler vector. - // The directions of X and Y are flipped to match the Wii Remote coordinate system. - tryAddSensor(Sensor.TYPE_HEAD_TRACKER, - new String[]{"Head Rotation Vector X-", "Head Rotation Vector X+", - "Head Rotation Vector Y-", "Head Rotation Vector Y+", - "Head Rotation Vector Z+", "Head Rotation Vector Z-", - "Head Pitch Up", "Head Pitch Down", "Head Roll Right", "Head Roll Left", - "Head Yaw Left", "Head Yaw Right"}, - new AxisSetDetails[]{new AxisSetDetails(0, AXIS_SET_TYPE_OTHER_COORDINATES), - new AxisSetDetails(3, AXIS_SET_TYPE_OTHER_COORDINATES)}); - - tryAddSensor(Sensor.TYPE_HEADING, new String[]{"Heading", "Heading Accuracy"}, - new AxisSetDetails[]{}); - } - } - - private void tryAddSensor(int sensorType, String axisName) - { - tryAddSensor(sensorType, new String[]{axisName}, new AxisSetDetails[]{}); - } - - private void tryAddSensor(int sensorType, String[] axisNames, AxisSetDetails[] axisSetDetails) - { - Sensor sensor = mSensorManager.getDefaultSensor(sensorType); - if (sensor != null) - { - mSensorDetails.put(sensor, new SensorDetails(sensorType, axisNames, axisSetDetails)); - } - } - - @Override - public void onSensorChanged(SensorEvent sensorEvent) - { - final SensorDetails sensorDetails = mSensorDetails.get(sensorEvent.sensor); - - final float[] values = sensorEvent.values; - final String[] axisNames = sensorDetails.axisNames; - final AxisSetDetails[] axisSetDetails = sensorDetails.axisSetDetails; - - int eventAxisIndex = 0; - int detailsAxisIndex = 0; - int detailsAxisSetIndex = 0; - boolean keepSensorAlive = false; - while (eventAxisIndex < values.length && detailsAxisIndex < axisNames.length) - { - if (detailsAxisSetIndex < axisSetDetails.length && - axisSetDetails[detailsAxisSetIndex].firstAxisOfSet == eventAxisIndex) - { - int rotation = Surface.ROTATION_0; - if (mRotateCoordinatesForScreenOrientation && - axisSetDetails[detailsAxisSetIndex].axisSetType == AXIS_SET_TYPE_DEVICE_COORDINATES) - { - rotation = sDeviceRotation; - } - - float x, y; - switch (rotation) - { - default: - case Surface.ROTATION_0: - x = values[eventAxisIndex]; - y = values[eventAxisIndex + 1]; - break; - case Surface.ROTATION_90: - x = -values[eventAxisIndex + 1]; - y = values[eventAxisIndex]; - break; - case Surface.ROTATION_180: - x = -values[eventAxisIndex]; - y = -values[eventAxisIndex + 1]; - break; - case Surface.ROTATION_270: - x = values[eventAxisIndex + 1]; - y = -values[eventAxisIndex]; - break; - } - - float z = values[eventAxisIndex + 2]; - - keepSensorAlive |= ControllerInterface.dispatchSensorEvent(mDeviceQualifier, - axisNames[detailsAxisIndex], x); - keepSensorAlive |= ControllerInterface.dispatchSensorEvent(mDeviceQualifier, - axisNames[detailsAxisIndex + 1], x); - keepSensorAlive |= ControllerInterface.dispatchSensorEvent(mDeviceQualifier, - axisNames[detailsAxisIndex + 2], y); - keepSensorAlive |= ControllerInterface.dispatchSensorEvent(mDeviceQualifier, - axisNames[detailsAxisIndex + 3], y); - keepSensorAlive |= ControllerInterface.dispatchSensorEvent(mDeviceQualifier, - axisNames[detailsAxisIndex + 4], z); - keepSensorAlive |= ControllerInterface.dispatchSensorEvent(mDeviceQualifier, - axisNames[detailsAxisIndex + 5], z); - - eventAxisIndex += 3; - detailsAxisIndex += 6; - detailsAxisSetIndex++; - } - else - { - keepSensorAlive |= ControllerInterface.dispatchSensorEvent(mDeviceQualifier, - axisNames[detailsAxisIndex], values[eventAxisIndex]); - - eventAxisIndex++; - detailsAxisIndex++; - } - } - - if (!keepSensorAlive) - { - setSensorSuspended(sensorEvent.sensor, sensorDetails, true); - } - } - - @Override - public void onAccuracyChanged(Sensor sensor, int i) - { - // We don't care about this - } - - /** - * The device qualifier set here will be passed on to native code, - * for the purpose of letting native code identify which device this object belongs to. - */ - @Keep - public void setDeviceQualifier(String deviceQualifier) - { - mDeviceQualifier = deviceQualifier; - } - - /** - * If a sensor has been suspended to save battery, this unsuspends it. - * If the sensor isn't currently suspended, nothing happens. - * - * @param axisName The name of any of the sensor's axes. - */ - @Keep - public void requestUnsuspendSensor(String axisName) - { - for (Map.Entry entry : mSensorDetails.entrySet()) - { - if (Arrays.asList(entry.getValue().axisNames).contains(axisName)) - { - setSensorSuspended(entry.getKey(), entry.getValue(), false); - } - } - } - - private void setSensorSuspended(Sensor sensor, SensorDetails sensorDetails, boolean suspend) - { - boolean changeOccurred = false; - - synchronized (sensorDetails) - { - if (sensorDetails.isSuspended != suspend) - { - ControllerInterface.notifySensorSuspendedState(mDeviceQualifier, sensorDetails.axisNames, - suspend); - - if (suspend) - mSensorManager.unregisterListener(this, sensor); - else - mSensorManager.registerListener(this, sensor, SAMPLING_PERIOD_US); - - sensorDetails.isSuspended = suspend; - - changeOccurred = true; - } - } - - if (changeOccurred) - { - Log.info((suspend ? "Suspended sensor " : "Unsuspended sensor ") + sensor.getName()); - } - } - - @Keep - public String[] getAxisNames() - { - ArrayList axisNames = new ArrayList<>(); - - for (SensorDetails sensorDetails : getSensorDetailsSorted()) - { - Collections.addAll(axisNames, sensorDetails.axisNames); - } - - return axisNames.toArray(new String[]{}); - } - - @Keep - public boolean[] getNegativeAxes() - { - ArrayList negativeAxes = new ArrayList<>(); - - for (SensorDetails sensorDetails : getSensorDetailsSorted()) - { - int eventAxisIndex = 0; - int detailsAxisIndex = 0; - int detailsAxisSetIndex = 0; - while (detailsAxisIndex < sensorDetails.axisNames.length) - { - if (detailsAxisSetIndex < sensorDetails.axisSetDetails.length && - sensorDetails.axisSetDetails[detailsAxisSetIndex].firstAxisOfSet == eventAxisIndex) - { - negativeAxes.add(false); - negativeAxes.add(true); - negativeAxes.add(false); - negativeAxes.add(true); - negativeAxes.add(false); - negativeAxes.add(true); - - eventAxisIndex += 3; - detailsAxisIndex += 6; - detailsAxisSetIndex++; - } - else - { - negativeAxes.add(false); - - eventAxisIndex++; - detailsAxisIndex++; - } - } - } - - boolean[] result = new boolean[negativeAxes.size()]; - for (int i = 0; i < result.length; i++) - { - result[i] = negativeAxes.get(i); - } - - return result; - } - - private List getSensorDetailsSorted() - { - ArrayList sensorDetails = new ArrayList<>(mSensorDetails.values()); - Collections.sort(sensorDetails, Comparator.comparingInt(s -> s.sensorType)); - return sensorDetails; - } - - /** - * Should be called when an activity or other component that uses sensor events is resumed. - * - * Sensor events that contain device coordinates will have the coordinates rotated by the value - * passed to this function. - * - * @param deviceRotation The current rotation of the device (i.e. rotation of the default display) - */ - public static void setDeviceRotation(int deviceRotation) - { - sDeviceRotation = deviceRotation; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinSensorEventListener.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinSensorEventListener.kt new file mode 100644 index 0000000000..e89143aa31 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinSensorEventListener.kt @@ -0,0 +1,504 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.input.model + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.os.Build +import android.view.InputDevice +import android.view.Surface +import androidx.annotation.Keep +import org.dolphinemu.dolphinemu.DolphinApplication +import org.dolphinemu.dolphinemu.utils.Log +import java.util.Collections + +class DolphinSensorEventListener : SensorEventListener { + private class AxisSetDetails(val firstAxisOfSet: Int, val axisSetType: Int) + + private class SensorDetails( + val sensorType: Int, + val axisNames: Array, + val axisSetDetails: Array + ) { + var isSuspended = true + } + + private val sensorManager: SensorManager? + + private val sensorDetails = HashMap() + + private val rotateCoordinatesForScreenOrientation: Boolean + + private var deviceQualifier = "" + + @Keep + constructor() { + sensorManager = DolphinApplication.getAppContext() + .getSystemService(Context.SENSOR_SERVICE) as SensorManager? + rotateCoordinatesForScreenOrientation = true + addSensors() + } + + @Keep + constructor(inputDevice: InputDevice) { + rotateCoordinatesForScreenOrientation = false + sensorManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + inputDevice.sensorManager + + // TODO: There is a bug where after suspending sensors, onSensorChanged can get called for + // a sensor that we never registered as a listener for. The way our code is currently written, + // this causes a NullPointerException, but if we checked for null we would instead have the + // problem of being spammed with onSensorChanged calls even though the sensor shouldn't be + // enabled. For now, let's comment out the ability to use InputDevice sensors. + + //addSensors(); + } else { + null + } + } + + private fun addSensors() { + tryAddSensor( + Sensor.TYPE_ACCELEROMETER, + arrayOf( + "Accel Right", + "Accel Left", + "Accel Forward", + "Accel Backward", + "Accel Up", + "Accel Down" + ), + arrayOf(AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES)) + ) + + tryAddSensor( + Sensor.TYPE_GYROSCOPE, + arrayOf( + "Gyro Pitch Up", + "Gyro Pitch Down", + "Gyro Roll Right", + "Gyro Roll Left", + "Gyro Yaw Left", + "Gyro Yaw Right" + ), + arrayOf(AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES)) + ) + + tryAddSensor(Sensor.TYPE_LIGHT, "Light") + + tryAddSensor(Sensor.TYPE_PRESSURE, "Pressure") + + tryAddSensor(Sensor.TYPE_TEMPERATURE, "Device Temperature") + + tryAddSensor(Sensor.TYPE_PROXIMITY, "Proximity") + + tryAddSensor( + Sensor.TYPE_GRAVITY, + arrayOf( + "Gravity Right", + "Gravity Left", + "Gravity Forward", + "Gravity Backward", + "Gravity Up", + "Gravity Down" + ), + arrayOf(AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES)) + ) + + tryAddSensor( + Sensor.TYPE_LINEAR_ACCELERATION, + arrayOf( + "Linear Acceleration Right", + "Linear Acceleration Left", + "Linear Acceleration Forward", + "Linear Acceleration Backward", + "Linear Acceleration Up", + "Linear Acceleration Down" + ), + arrayOf(AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES)) + ) + + // The values provided by this sensor can be interpreted as an Euler vector or a quaternion. + // The directions of X and Y are flipped to match the Wii Remote coordinate system. + tryAddSensor( + Sensor.TYPE_ROTATION_VECTOR, + arrayOf( + "Rotation Vector X-", + "Rotation Vector X+", + "Rotation Vector Y-", + "Rotation Vector Y+", + "Rotation Vector Z+", + "Rotation Vector Z-", + "Rotation Vector R", + "Rotation Vector Heading Accuracy" + ), + arrayOf(AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES)) + ) + + tryAddSensor(Sensor.TYPE_RELATIVE_HUMIDITY, "Relative Humidity") + + tryAddSensor(Sensor.TYPE_AMBIENT_TEMPERATURE, "Ambient Temperature") + + // The values provided by this sensor can be interpreted as an Euler vector or a quaternion. + // The directions of X and Y are flipped to match the Wii Remote coordinate system. + tryAddSensor( + Sensor.TYPE_GAME_ROTATION_VECTOR, + arrayOf( + "Game Rotation Vector X-", + "Game Rotation Vector X+", + "Game Rotation Vector Y-", + "Game Rotation Vector Y+", + "Game Rotation Vector Z+", + "Game Rotation Vector Z-", + "Game Rotation Vector R" + ), + arrayOf(AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES)) + ) + + tryAddSensor( + Sensor.TYPE_GYROSCOPE_UNCALIBRATED, + arrayOf( + "Gyro Uncalibrated Pitch Up", + "Gyro Uncalibrated Pitch Down", + "Gyro Uncalibrated Roll Right", + "Gyro Uncalibrated Roll Left", + "Gyro Uncalibrated Yaw Left", + "Gyro Uncalibrated Yaw Right", + "Gyro Drift Pitch Up", + "Gyro Drift Pitch Down", + "Gyro Drift Roll Right", + "Gyro Drift Roll Left", + "Gyro Drift Yaw Left", + "Gyro Drift Yaw Right" + ), + arrayOf( + AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES), + AxisSetDetails(3, AXIS_SET_TYPE_DEVICE_COORDINATES) + ) + ) + + tryAddSensor(Sensor.TYPE_HEART_RATE, "Heart Rate") + + if (Build.VERSION.SDK_INT >= 24) { + tryAddSensor(Sensor.TYPE_HEART_BEAT, "Heart Beat") + } + + if (Build.VERSION.SDK_INT >= 26) { + tryAddSensor( + Sensor.TYPE_ACCELEROMETER_UNCALIBRATED, + arrayOf( + "Accel Uncalibrated Right", + "Accel Uncalibrated Left", + "Accel Uncalibrated Forward", + "Accel Uncalibrated Backward", + "Accel Uncalibrated Up", + "Accel Uncalibrated Down", + "Accel Bias Right", + "Accel Bias Left", + "Accel Bias Forward", + "Accel Bias Backward", + "Accel Bias Up", + "Accel Bias Down" + ), + arrayOf( + AxisSetDetails(0, AXIS_SET_TYPE_DEVICE_COORDINATES), + AxisSetDetails(3, AXIS_SET_TYPE_DEVICE_COORDINATES) + ) + ) + } + + if (Build.VERSION.SDK_INT >= 30) { + tryAddSensor(Sensor.TYPE_HINGE_ANGLE, "Hinge Angle") + } + + if (Build.VERSION.SDK_INT >= 33) { + // The values provided by this sensor can be interpreted as an Euler vector. + // The directions of X and Y are flipped to match the Wii Remote coordinate system. + tryAddSensor( + Sensor.TYPE_HEAD_TRACKER, + arrayOf( + "Head Rotation Vector X-", + "Head Rotation Vector X+", + "Head Rotation Vector Y-", + "Head Rotation Vector Y+", + "Head Rotation Vector Z+", + "Head Rotation Vector Z-", + "Head Pitch Up", + "Head Pitch Down", + "Head Roll Right", + "Head Roll Left", + "Head Yaw Left", + "Head Yaw Right" + ), + arrayOf( + AxisSetDetails(0, AXIS_SET_TYPE_OTHER_COORDINATES), + AxisSetDetails(3, AXIS_SET_TYPE_OTHER_COORDINATES) + ) + ) + + tryAddSensor(Sensor.TYPE_HEADING, arrayOf("Heading", "Heading Accuracy"), arrayOf()) + } + } + + private fun tryAddSensor(sensorType: Int, axisName: String) { + tryAddSensor(sensorType, arrayOf(axisName), arrayOf()) + } + + private fun tryAddSensor( + sensorType: Int, + axisNames: Array, + axisSetDetails: Array + ) { + val sensor = sensorManager!!.getDefaultSensor(sensorType) + if (sensor != null) { + sensorDetails[sensor] = SensorDetails(sensorType, axisNames, axisSetDetails) + } + } + + override fun onSensorChanged(sensorEvent: SensorEvent) { + val sensorDetails = sensorDetails[sensorEvent.sensor] + + val values = sensorEvent.values + val axisNames = sensorDetails!!.axisNames + val axisSetDetails = sensorDetails.axisSetDetails + + var eventAxisIndex = 0 + var detailsAxisIndex = 0 + var detailsAxisSetIndex = 0 + var keepSensorAlive = false + while (eventAxisIndex < values.size && detailsAxisIndex < axisNames.size) { + if (detailsAxisSetIndex < axisSetDetails.size && + axisSetDetails[detailsAxisSetIndex].firstAxisOfSet == eventAxisIndex + ) { + var rotation = Surface.ROTATION_0 + if (rotateCoordinatesForScreenOrientation && + axisSetDetails[detailsAxisSetIndex].axisSetType == AXIS_SET_TYPE_DEVICE_COORDINATES + ) { + rotation = deviceRotation + } + + var x: Float + var y: Float + when (rotation) { + Surface.ROTATION_0 -> { + x = values[eventAxisIndex] + y = values[eventAxisIndex + 1] + } + + Surface.ROTATION_90 -> { + x = -values[eventAxisIndex + 1] + y = values[eventAxisIndex] + } + + Surface.ROTATION_180 -> { + x = -values[eventAxisIndex] + y = -values[eventAxisIndex + 1] + } + + Surface.ROTATION_270 -> { + x = values[eventAxisIndex + 1] + y = -values[eventAxisIndex] + } + + else -> { + x = values[eventAxisIndex] + y = values[eventAxisIndex + 1] + } + } + + val z = values[eventAxisIndex + 2] + + keepSensorAlive = keepSensorAlive or ControllerInterface.dispatchSensorEvent( + deviceQualifier, + axisNames[detailsAxisIndex], + x + ) + keepSensorAlive = keepSensorAlive or ControllerInterface.dispatchSensorEvent( + deviceQualifier, + axisNames[detailsAxisIndex + 1], + x + ) + keepSensorAlive = keepSensorAlive or ControllerInterface.dispatchSensorEvent( + deviceQualifier, + axisNames[detailsAxisIndex + 2], + y + ) + keepSensorAlive = keepSensorAlive or ControllerInterface.dispatchSensorEvent( + deviceQualifier, + axisNames[detailsAxisIndex + 3], + y + ) + keepSensorAlive = keepSensorAlive or ControllerInterface.dispatchSensorEvent( + deviceQualifier, + axisNames[detailsAxisIndex + 4], + z + ) + keepSensorAlive = keepSensorAlive or ControllerInterface.dispatchSensorEvent( + deviceQualifier, + axisNames[detailsAxisIndex + 5], + z + ) + + eventAxisIndex += 3 + detailsAxisIndex += 6 + detailsAxisSetIndex++ + } else { + keepSensorAlive = keepSensorAlive or ControllerInterface.dispatchSensorEvent( + deviceQualifier, + axisNames[detailsAxisIndex], values[eventAxisIndex] + ) + + eventAxisIndex++ + detailsAxisIndex++ + } + } + if (!keepSensorAlive) { + setSensorSuspended(sensorEvent.sensor, sensorDetails, true) + } + } + + override fun onAccuracyChanged(sensor: Sensor, i: Int) { + // We don't care about this + } + + /** + * The device qualifier set here will be passed on to native code, + * for the purpose of letting native code identify which device this object belongs to. + */ + @Keep + fun setDeviceQualifier(deviceQualifier: String) { + this.deviceQualifier = deviceQualifier + } + + /** + * If a sensor has been suspended to save battery, this unsuspends it. + * If the sensor isn't currently suspended, nothing happens. + * + * @param axisName The name of any of the sensor's axes. + */ + @Keep + fun requestUnsuspendSensor(axisName: String) { + for ((key, value) in sensorDetails) { + if (listOf(*value.axisNames).contains(axisName)) { + setSensorSuspended(key, value, false) + } + } + } + + private fun setSensorSuspended( + sensor: Sensor, + sensorDetails: SensorDetails, + suspend: Boolean + ) { + var changeOccurred = false + + synchronized(sensorDetails) { + if (sensorDetails.isSuspended != suspend) { + ControllerInterface.notifySensorSuspendedState( + deviceQualifier, + sensorDetails.axisNames, + suspend + ) + + if (suspend) + sensorManager!!.unregisterListener(this, sensor) + else + sensorManager!!.registerListener(this, sensor, SAMPLING_PERIOD_US) + + sensorDetails.isSuspended = suspend + + changeOccurred = true + } + } + + if (changeOccurred) { + Log.info((if (suspend) "Suspended sensor " else "Unsuspended sensor ") + sensor.name) + } + } + + @Keep + fun getAxisNames(): Array { + val axisNames = ArrayList() + for (sensorDetails in sensorDetailsSorted) { + sensorDetails.axisNames.forEach { axisNames.add(it) } + } + return axisNames.toArray(arrayOf()) + } + + @Keep + fun getNegativeAxes(): BooleanArray { + val negativeAxes = ArrayList() + + for (sensorDetails in sensorDetailsSorted) { + var eventAxisIndex = 0 + var detailsAxisIndex = 0 + var detailsAxisSetIndex = 0 + while (detailsAxisIndex < sensorDetails.axisNames.size) { + if (detailsAxisSetIndex < sensorDetails.axisSetDetails.size && + sensorDetails.axisSetDetails[detailsAxisSetIndex].firstAxisOfSet == eventAxisIndex + ) { + negativeAxes.add(false) + negativeAxes.add(true) + negativeAxes.add(false) + negativeAxes.add(true) + negativeAxes.add(false) + negativeAxes.add(true) + + eventAxisIndex += 3 + detailsAxisIndex += 6 + detailsAxisSetIndex++ + } else { + negativeAxes.add(false) + + eventAxisIndex++ + detailsAxisIndex++ + } + } + } + + val result = BooleanArray(negativeAxes.size) + for (i in result.indices) { + result[i] = negativeAxes[i] + } + + return result + } + + private val sensorDetailsSorted: List + get() { + val sensorDetails = ArrayList(sensorDetails.values) + Collections.sort( + sensorDetails, + Comparator.comparingInt { s: SensorDetails -> s.sensorType } + ) + return sensorDetails + } + + companion object { + // Set of three axes. Creates a negative companion to each axis, and corrects for device rotation. + private const val AXIS_SET_TYPE_DEVICE_COORDINATES = 0 + + // Set of three axes. Creates a negative companion to each axis. + private const val AXIS_SET_TYPE_OTHER_COORDINATES = 1 + private var deviceRotation = Surface.ROTATION_0 + + // The fastest sampling rate Android lets us use without declaring the HIGH_SAMPLING_RATE_SENSORS + // permission is 200 Hz. This is also the sampling rate of a Wii Remote, so it fits us perfectly. + private const val SAMPLING_PERIOD_US = 1000000 / 200 + + /** + * Should be called when an activity or other component that uses sensor events is resumed. + * + * Sensor events that contain device coordinates will have the coordinates rotated by the value + * passed to this function. + * + * @param deviceRotation The current rotation of the device (i.e. rotation of the default display) + */ + fun setDeviceRotation(deviceRotation: Int) { + this.deviceRotation = deviceRotation + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinVibratorManager.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinVibratorManager.java deleted file mode 100644 index abc04d969d..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinVibratorManager.java +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.features.input.model; - -import android.os.Vibrator; - -import androidx.annotation.Keep; -import androidx.annotation.NonNull; - -/** - * A wrapper around {@link android.os.VibratorManager}, for backwards compatibility. - */ -public interface DolphinVibratorManager -{ - @Keep @NonNull - Vibrator getVibrator(int vibratorId); - - @Keep @NonNull - int[] getVibratorIds(); -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinVibratorManager.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinVibratorManager.kt new file mode 100644 index 0000000000..c3c6ba28da --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinVibratorManager.kt @@ -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 +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinVibratorManagerCompat.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinVibratorManagerCompat.java deleted file mode 100644 index 40c4fa95d1..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinVibratorManagerCompat.java +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.features.input.model; - -import android.os.Vibrator; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -public final class DolphinVibratorManagerCompat implements DolphinVibratorManager -{ - private final Vibrator mVibrator; - private final int[] mIds; - - public DolphinVibratorManagerCompat(@Nullable Vibrator vibrator) - { - mVibrator = vibrator; - mIds = vibrator != null && vibrator.hasVibrator() ? new int[]{0} : new int[]{}; - } - - @Override @NonNull - public Vibrator getVibrator(int vibratorId) - { - if (vibratorId > mIds.length) - throw new IndexOutOfBoundsException(); - - return mVibrator; - } - - @Override @NonNull - public int[] getVibratorIds() - { - return mIds; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinVibratorManagerCompat.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinVibratorManagerCompat.kt new file mode 100644 index 0000000000..039f0ecb6b --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinVibratorManagerCompat.kt @@ -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 +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinVibratorManagerPassthrough.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinVibratorManagerPassthrough.java deleted file mode 100644 index 2ca747f54f..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinVibratorManagerPassthrough.java +++ /dev/null @@ -1,33 +0,0 @@ -// 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.NonNull; -import androidx.annotation.RequiresApi; - -@RequiresApi(api = Build.VERSION_CODES.S) -public final class DolphinVibratorManagerPassthrough implements DolphinVibratorManager -{ - private final VibratorManager mVibratorManager; - - public DolphinVibratorManagerPassthrough(@NonNull VibratorManager vibratorManager) - { - mVibratorManager = vibratorManager; - } - - @Override @NonNull - public Vibrator getVibrator(int vibratorId) - { - return mVibratorManager.getVibrator(vibratorId); - } - - @Override @NonNull - public int[] getVibratorIds() - { - return mVibratorManager.getVibratorIds(); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinVibratorManagerPassthrough.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinVibratorManagerPassthrough.kt new file mode 100644 index 0000000000..0895484314 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/DolphinVibratorManagerPassthrough.kt @@ -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 +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputMappingBooleanSetting.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputMappingBooleanSetting.java deleted file mode 100644 index 6e80fef6dd..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputMappingBooleanSetting.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.features.input.model; - -import androidx.annotation.NonNull; - -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; - -public class InputMappingBooleanSetting implements AbstractBooleanSetting -{ - private final NumericSetting mNumericSetting; - - public InputMappingBooleanSetting(NumericSetting numericSetting) - { - mNumericSetting = numericSetting; - } - - @Override - public boolean getBoolean() - { - return mNumericSetting.getBooleanValue(); - } - - @Override - public void setBoolean(@NonNull Settings settings, boolean newValue) - { - mNumericSetting.setBooleanValue(newValue); - } - - @Override - public boolean isOverridden() - { - return false; - } - - @Override - public boolean isRuntimeEditable() - { - return true; - } - - @Override - public boolean delete(@NonNull Settings settings) - { - mNumericSetting.setBooleanValue(mNumericSetting.getBooleanDefaultValue()); - return true; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputMappingBooleanSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputMappingBooleanSetting.kt new file mode 100644 index 0000000000..5771db54e0 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputMappingBooleanSetting.kt @@ -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 + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputMappingDoubleSetting.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputMappingDoubleSetting.java deleted file mode 100644 index 0311e6d2b3..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputMappingDoubleSetting.java +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.features.input.model; - -import androidx.annotation.NonNull; - -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 -public class InputMappingDoubleSetting implements AbstractFloatSetting -{ - private final NumericSetting mNumericSetting; - - public InputMappingDoubleSetting(NumericSetting numericSetting) - { - mNumericSetting = numericSetting; - } - - @Override - public float getFloat() - { - return (float) mNumericSetting.getDoubleValue(); - } - - @Override - public void setFloat(@NonNull Settings settings, float newValue) - { - mNumericSetting.setDoubleValue(newValue); - } - - @Override - public boolean isOverridden() - { - return false; - } - - @Override - public boolean isRuntimeEditable() - { - return true; - } - - @Override - public boolean delete(@NonNull Settings settings) - { - mNumericSetting.setDoubleValue(mNumericSetting.getDoubleDefaultValue()); - return true; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputMappingDoubleSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputMappingDoubleSetting.kt new file mode 100644 index 0000000000..e2da062250 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputMappingDoubleSetting.kt @@ -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 + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputMappingIntSetting.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputMappingIntSetting.java deleted file mode 100644 index 176346c63a..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputMappingIntSetting.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.features.input.model; - -import androidx.annotation.NonNull; - -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; - -public class InputMappingIntSetting implements AbstractIntSetting -{ - private final NumericSetting mNumericSetting; - - public InputMappingIntSetting(NumericSetting numericSetting) - { - mNumericSetting = numericSetting; - } - - @Override - public int getInt() - { - return mNumericSetting.getIntValue(); - } - - @Override - public void setInt(@NonNull Settings settings, int newValue) - { - mNumericSetting.setIntValue(newValue); - } - - @Override - public boolean isOverridden() - { - return false; - } - - @Override - public boolean isRuntimeEditable() - { - return true; - } - - @Override - public boolean delete(@NonNull Settings settings) - { - mNumericSetting.setIntValue(mNumericSetting.getIntDefaultValue()); - return true; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputMappingIntSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputMappingIntSetting.kt new file mode 100644 index 0000000000..3e8a14afc7 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputMappingIntSetting.kt @@ -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 + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputOverrider.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputOverrider.java deleted file mode 100644 index 6596c8dd91..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputOverrider.java +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.features.input.model; - -public final class InputOverrider -{ - public static final class ControlId - { - public static final int GCPAD_A_BUTTON = 0; - public static final int GCPAD_B_BUTTON = 1; - public static final int GCPAD_X_BUTTON = 2; - public static final int GCPAD_Y_BUTTON = 3; - public static final int GCPAD_Z_BUTTON = 4; - public static final int GCPAD_START_BUTTON = 5; - public static final int GCPAD_DPAD_UP = 6; - public static final int GCPAD_DPAD_DOWN = 7; - public static final int GCPAD_DPAD_LEFT = 8; - public static final int GCPAD_DPAD_RIGHT = 9; - public static final int GCPAD_L_DIGITAL = 10; - public static final int GCPAD_R_DIGITAL = 11; - public static final int GCPAD_L_ANALOG = 12; - public static final int GCPAD_R_ANALOG = 13; - public static final int GCPAD_MAIN_STICK_X = 14; - public static final int GCPAD_MAIN_STICK_Y = 15; - public static final int GCPAD_C_STICK_X = 16; - public static final int GCPAD_C_STICK_Y = 17; - - public static final int WIIMOTE_A_BUTTON = 18; - public static final int WIIMOTE_B_BUTTON = 19; - public static final int WIIMOTE_ONE_BUTTON = 20; - public static final int WIIMOTE_TWO_BUTTON = 21; - public static final int WIIMOTE_PLUS_BUTTON = 22; - public static final int WIIMOTE_MINUS_BUTTON = 23; - public static final int WIIMOTE_HOME_BUTTON = 24; - public static final int WIIMOTE_DPAD_UP = 25; - public static final int WIIMOTE_DPAD_DOWN = 26; - public static final int WIIMOTE_DPAD_LEFT = 27; - public static final int WIIMOTE_DPAD_RIGHT = 28; - public static final int WIIMOTE_IR_X = 29; - public static final int WIIMOTE_IR_Y = 30; - - public static final int NUNCHUK_C_BUTTON = 31; - public static final int NUNCHUK_Z_BUTTON = 32; - public static final int NUNCHUK_STICK_X = 33; - public static final int NUNCHUK_STICK_Y = 34; - - public static final int CLASSIC_A_BUTTON = 35; - public static final int CLASSIC_B_BUTTON = 36; - public static final int CLASSIC_X_BUTTON = 37; - public static final int CLASSIC_Y_BUTTON = 38; - public static final int CLASSIC_ZL_BUTTON = 39; - public static final int CLASSIC_ZR_BUTTON = 40; - public static final int CLASSIC_PLUS_BUTTON = 41; - public static final int CLASSIC_MINUS_BUTTON = 42; - public static final int CLASSIC_HOME_BUTTON = 43; - public static final int CLASSIC_DPAD_UP = 44; - public static final int CLASSIC_DPAD_DOWN = 45; - public static final int CLASSIC_DPAD_LEFT = 46; - public static final int CLASSIC_DPAD_RIGHT = 47; - public static final int CLASSIC_L_DIGITAL = 48; - public static final int CLASSIC_R_DIGITAL = 49; - public static final int CLASSIC_L_ANALOG = 50; - public static final int CLASSIC_R_ANALOG = 51; - public static final int CLASSIC_LEFT_STICK_X = 52; - public static final int CLASSIC_LEFT_STICK_Y = 53; - public static final int CLASSIC_RIGHT_STICK_X = 54; - public static final int CLASSIC_RIGHT_STICK_Y = 55; - } - - public static native void registerGameCube(int controllerIndex); - - public static native void registerWii(int controllerIndex); - - public static native void unregisterGameCube(int controllerIndex); - - public static native void unregisterWii(int controllerIndex); - - public static native void setControlState(int controllerIndex, int control, double state); - - public static native void clearControlState(int controllerIndex, int control); - - // Angle is in radians and should be non-negative - public static native double getGateRadiusAtAngle(int emuPadId, int stick, double angle); -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputOverrider.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputOverrider.kt new file mode 100644 index 0000000000..8224434ee6 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputOverrider.kt @@ -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 + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/MappingCommon.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/MappingCommon.java deleted file mode 100644 index 5e4c0076ea..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/MappingCommon.java +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.features.input.model; - -import androidx.annotation.NonNull; - -import org.dolphinemu.dolphinemu.features.input.model.controlleremu.EmulatedController; - -public final class MappingCommon -{ - private 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. - */ - public static native String detectInput(@NonNull EmulatedController controller, - boolean allDevices); - - public static native String getExpressionForControl(String control, String device, - String defaultDevice); - - public static native void save(); -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/MappingCommon.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/MappingCommon.kt new file mode 100644 index 0000000000..32775a7ebc --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/MappingCommon.kt @@ -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() +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/Control.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/Control.kt similarity index 52% rename from Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/Control.java rename to Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/Control.kt index 6033e17e37..fcc420c740 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/Control.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/Control.kt @@ -1,8 +1,8 @@ // 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. @@ -10,18 +10,9 @@ import androidx.annotation.Keep; * 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 } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/ControlGroup.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/ControlGroup.java deleted file mode 100644 index 1bafd4a1f2..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/ControlGroup.java +++ /dev/null @@ -1,66 +0,0 @@ -// 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! - */ -public class ControlGroup -{ - public static final int TYPE_OTHER = 0; - public static final int TYPE_STICK = 1; - public static final int TYPE_MIXED_TRIGGERS = 2; - public static final int TYPE_BUTTONS = 3; - public static final int TYPE_FORCE = 4; - public static final int TYPE_ATTACHMENTS = 5; - public static final int TYPE_TILT = 6; - public static final int TYPE_CURSOR = 7; - public static final int TYPE_TRIGGERS = 8; - public static final int TYPE_SLIDER = 9; - public static final int TYPE_SHAKE = 10; - public static final int TYPE_IMU_ACCELEROMETER = 11; - public static final int TYPE_IMU_GYROSCOPE = 12; - public static final int TYPE_IMU_CURSOR = 13; - - public static final int DEFAULT_ENABLED_ALWAYS = 0; - public static final int DEFAULT_ENABLED_YES = 1; - public static final int DEFAULT_ENABLED_NO = 2; - - @Keep - private final long mPointer; - - @Keep - private ControlGroup(long pointer) - { - mPointer = pointer; - } - - public native String getUiName(); - - public native int getGroupType(); - - public native int getDefaultEnabledValue(); - - public native boolean getEnabled(); - - public native void setEnabled(boolean value); - - public native int getControlCount(); - - public native Control getControl(int i); - - public native int getNumericSettingCount(); - - public native NumericSetting getNumericSetting(int i); - - /** - * If getGroupType returns TYPE_ATTACHMENTS, this returns the attachment selection setting. - * Otherwise, undefined behavior! - */ - public native NumericSetting getAttachmentSetting(); -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/ControlGroup.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/ControlGroup.kt new file mode 100644 index 0000000000..9c5132b1a2 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/ControlGroup.kt @@ -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 + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/ControlReference.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/ControlReference.java deleted file mode 100644 index 66fd9fa02d..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/ControlReference.java +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.features.input.model.controlleremu; - -import androidx.annotation.Keep; -import androidx.annotation.Nullable; - -/** - * 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! - */ -public class ControlReference -{ - @Keep - private final long mPointer; - - @Keep - private ControlReference(long pointer) - { - mPointer = pointer; - } - - public native double getState(); - - public native String getExpression(); - - /** - * Sets the expression for this control reference. - * - * @param expr The new expression - * @return null on success, a human-readable error on failure - */ - @Nullable - public native String setExpression(String expr); - - public native boolean isInput(); -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/ControlReference.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/ControlReference.kt new file mode 100644 index 0000000000..cd0c7848a6 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/ControlReference.kt @@ -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 +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/EmulatedController.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/EmulatedController.java deleted file mode 100644 index 39645ffbba..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/EmulatedController.java +++ /dev/null @@ -1,52 +0,0 @@ -// 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! - */ -public class EmulatedController -{ - @Keep - private final long mPointer; - - @Keep - private EmulatedController(long pointer) - { - mPointer = pointer; - } - - public native String getDefaultDevice(); - - public native void setDefaultDevice(String device); - - public native int getGroupCount(); - - public native ControlGroup getGroup(int index); - - public native void updateSingleControlReference(ControlReference controlReference); - - public native void loadDefaultSettings(); - - public native void clearSettings(); - - public native void loadProfile(String path); - - public native void saveProfile(String path); - - public static native EmulatedController getGcPad(int controllerIndex); - - public static native EmulatedController getWiimote(int controllerIndex); - - public static native EmulatedController getWiimoteAttachment(int controllerIndex, - int attachmentIndex); - - public static native int getSelectedWiimoteAttachment(int controllerIndex); - - public static native NumericSetting getSidewaysWiimoteSetting(int controllerIndex); -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/EmulatedController.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/EmulatedController.kt new file mode 100644 index 0000000000..348e75b6a0 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/EmulatedController.kt @@ -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 + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/NumericSetting.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/NumericSetting.java deleted file mode 100644 index bcb9dce181..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/NumericSetting.java +++ /dev/null @@ -1,104 +0,0 @@ -// 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! - */ -public class NumericSetting -{ - public static final int TYPE_INT = 0; - public static final int TYPE_DOUBLE = 1; - public static final int TYPE_BOOLEAN = 2; - - @Keep - private final long mPointer; - - @Keep - private NumericSetting(long pointer) - { - mPointer = pointer; - } - - /** - * @return The name used in the UI. - */ - public native String getUiName(); - - /** - * @return A string applied to the number in the UI (unit of measure). - */ - public native String getUiSuffix(); - - /** - * @return Detailed description of the setting. - */ - public native String getUiDescription(); - - /** - * @return TYPE_INT, TYPE_DOUBLE or TYPE_BOOLEAN - */ - public native int getType(); - - public native ControlReference getControlReference(); - - /** - * If the type is TYPE_INT, gets the current value. Otherwise, undefined behavior! - */ - public native int getIntValue(); - - /** - * If the type is TYPE_INT, sets the current value. Otherwise, undefined behavior! - */ - public native void setIntValue(int value); - - /** - * If the type is TYPE_INT, gets the default value. Otherwise, undefined behavior! - */ - public native int getIntDefaultValue(); - - /** - * If the type is TYPE_DOUBLE, gets the current value. Otherwise, undefined behavior! - */ - public native double getDoubleValue(); - - /** - * If the type is TYPE_DOUBLE, sets the current value. Otherwise, undefined behavior! - */ - public native void setDoubleValue(double value); - - /** - * If the type is TYPE_DOUBLE, gets the default value. Otherwise, undefined behavior! - */ - public native double getDoubleDefaultValue(); - - /** - * If the type is TYPE_DOUBLE, returns the minimum valid value. Otherwise, undefined behavior! - */ - public native double getDoubleMin(); - - /** - * If the type is TYPE_DOUBLE, returns the maximum valid value. Otherwise, undefined behavior! - */ - public native double getDoubleMax(); - - /** - * If the type is TYPE_BOOLEAN, gets the current value. Otherwise, undefined behavior! - */ - public native boolean getBooleanValue(); - - /** - * If the type is TYPE_BOOLEAN, sets the current value. Otherwise, undefined behavior! - */ - public native void setBooleanValue(boolean value); - - /** - * If the type is TYPE_BOOLEAN, gets the default value. Otherwise, undefined behavior! - */ - public native boolean getBooleanDefaultValue(); -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/NumericSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/NumericSetting.kt new file mode 100644 index 0000000000..0a2485deab --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/NumericSetting.kt @@ -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 + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputDeviceSetting.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputDeviceSetting.java deleted file mode 100644 index f02de2e3f3..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputDeviceSetting.java +++ /dev/null @@ -1,70 +0,0 @@ -// 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; - -public class InputDeviceSetting extends StringSingleChoiceSetting -{ - private final EmulatedController mController; - - public InputDeviceSetting(Context context, int titleId, int descriptionId, - EmulatedController controller) - { - super(context, null, titleId, descriptionId, null, null, null); - - mController = controller; - - refreshChoicesAndValues(); - } - - @Override - public String getSelectedChoice() - { - return mController.getDefaultDevice(); - } - - @Override - public String getSelectedValue() - { - return mController.getDefaultDevice(); - } - - @Override - public void setSelectedValue(Settings settings, String newValue) - { - mController.setDefaultDevice(newValue); - } - - @Override - public void refreshChoicesAndValues() - { - String[] devices = ControllerInterface.getAllDeviceStrings(); - - setChoices(devices); - setValues(devices); - } - - @Override - public boolean isEditable() - { - return true; - } - - @Override - public boolean canClear() - { - return true; - } - - @Override - public void clear(Settings settings) - { - setSelectedValue(settings, ""); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputDeviceSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputDeviceSetting.kt new file mode 100644 index 0000000000..c98af542c7 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputDeviceSetting.kt @@ -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, "") +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputMappingControlSetting.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputMappingControlSetting.java deleted file mode 100644 index 86379bf5a6..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputMappingControlSetting.java +++ /dev/null @@ -1,71 +0,0 @@ -// 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.ControlReference; -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; - -public final class InputMappingControlSetting extends SettingsItem -{ - private final ControlReference mControlReference; - private final EmulatedController mController; - - public InputMappingControlSetting(Control control, EmulatedController controller) - { - super(control.getUiName(), ""); - mControlReference = control.getControlReference(); - mController = controller; - } - - public String getValue() - { - return mControlReference.getExpression(); - } - - public void setValue(String expr) - { - mControlReference.setExpression(expr); - mController.updateSingleControlReference(mControlReference); - } - - public void clearValue() - { - setValue(""); - } - - @Override - public int getType() - { - return TYPE_INPUT_MAPPING_CONTROL; - } - - @Override - public AbstractSetting getSetting() - { - return null; - } - - @Override - public boolean isEditable() - { - return true; - } - - public EmulatedController getController() - { - return mController; - } - - public ControlReference getControlReference() - { - return mControlReference; - } - - public boolean isInput() - { - return mControlReference.isInput(); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputMappingControlSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputMappingControlSetting.kt new file mode 100644 index 0000000000..26a86a9411 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputMappingControlSetting.kt @@ -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() +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.java deleted file mode 100644 index 0ad589fe40..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.java +++ /dev/null @@ -1,55 +0,0 @@ -// 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.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import org.dolphinemu.dolphinemu.databinding.ListItemAdvancedMappingControlBinding; - -import java.util.function.Consumer; - -public final class AdvancedMappingControlAdapter - extends RecyclerView.Adapter -{ - private final Consumer mOnClickCallback; - - private String[] mControls = new String[0]; - - public AdvancedMappingControlAdapter(Consumer onClickCallback) - { - mOnClickCallback = onClickCallback; - } - - @NonNull @Override - public AdvancedMappingControlViewHolder onCreateViewHolder(@NonNull ViewGroup parent, - int viewType) - { - LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - - ListItemAdvancedMappingControlBinding binding = - ListItemAdvancedMappingControlBinding.inflate(inflater); - return new AdvancedMappingControlViewHolder(binding, mOnClickCallback); - } - - @Override - public void onBindViewHolder(@NonNull AdvancedMappingControlViewHolder holder, int position) - { - holder.bind(mControls[position]); - } - - @Override - public int getItemCount() - { - return mControls.length; - } - - public void setControls(String[] controls) - { - mControls = controls; - notifyDataSetChanged(); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.kt new file mode 100644 index 0000000000..1c82616726 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.kt @@ -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) : + RecyclerView.Adapter() { + private var controls = emptyArray() + + 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) { + this.controls = controls + notifyDataSetChanged() + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.java deleted file mode 100644 index 2ce925235b..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.java +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.features.input.ui; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import org.dolphinemu.dolphinemu.databinding.ListItemAdvancedMappingControlBinding; - -import java.util.function.Consumer; - -public class AdvancedMappingControlViewHolder extends RecyclerView.ViewHolder -{ - private final ListItemAdvancedMappingControlBinding mBinding; - - private String mName; - - public AdvancedMappingControlViewHolder(@NonNull ListItemAdvancedMappingControlBinding binding, - Consumer onClickCallback) - { - super(binding.getRoot()); - - mBinding = binding; - - binding.getRoot().setOnClickListener(view -> onClickCallback.accept(mName)); - } - - public void bind(String name) - { - mName = name; - - mBinding.textName.setText(name); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.kt new file mode 100644 index 0000000000..dbfecfc94d --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.kt @@ -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 +) : 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 + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.java deleted file mode 100644 index de8e8265a6..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.java +++ /dev/null @@ -1,151 +0,0 @@ -// 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.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; - -import java.util.Arrays; -import java.util.Optional; - -public final class AdvancedMappingDialog extends AlertDialog - implements AdapterView.OnItemClickListener -{ - private final DialogAdvancedMappingBinding mBinding; - private final ControlReference mControlReference; - private final EmulatedController mController; - private final String[] mDevices; - private final AdvancedMappingControlAdapter mControlAdapter; - - private String mSelectedDevice; - - public AdvancedMappingDialog(Context context, DialogAdvancedMappingBinding binding, - ControlReference controlReference, EmulatedController controller) - { - super(context); - - mBinding = binding; - mControlReference = controlReference; - mController = controller; - - mDevices = ControllerInterface.getAllDeviceStrings(); - - // TODO: Remove workaround for text filtering issue in material components when fixed - // https://github.com/material-components/material-components-android/issues/1464 - mBinding.dropdownDevice.setSaveEnabled(false); - - binding.dropdownDevice.setOnItemClickListener(this); - - ArrayAdapter deviceAdapter = new ArrayAdapter<>( - context, android.R.layout.simple_spinner_dropdown_item, mDevices); - binding.dropdownDevice.setAdapter(deviceAdapter); - - mControlAdapter = new AdvancedMappingControlAdapter(this::onControlClicked); - mBinding.listControl.setAdapter(mControlAdapter); - mBinding.listControl.setLayoutManager(new LinearLayoutManager(context)); - - MaterialDividerItemDecoration divider = - new MaterialDividerItemDecoration(context, LinearLayoutManager.VERTICAL); - divider.setLastItemDecorated(false); - mBinding.listControl.addItemDecoration(divider); - - binding.editExpression.setText(controlReference.getExpression()); - - selectDefaultDevice(); - } - - public String getExpression() - { - return mBinding.editExpression.getText().toString(); - } - - @Override - public void onItemClick(AdapterView adapterView, View view, int position, long id) - { - setSelectedDevice(mDevices[position]); - } - - private void setSelectedDevice(String deviceString) - { - mSelectedDevice = deviceString; - - CoreDevice device = ControllerInterface.getDevice(deviceString); - if (device == null) - setControls(new CoreDevice.Control[0]); - else if (mControlReference.isInput()) - setControls(device.getInputs()); - else - setControls(device.getOutputs()); - } - - private void setControls(CoreDevice.Control[] controls) - { - mControlAdapter.setControls( - Arrays.stream(controls) - .map(CoreDevice.Control::getName) - .toArray(String[]::new)); - } - - private void onControlClicked(String control) - { - String expression = MappingCommon.getExpressionForControl(control, mSelectedDevice, - mController.getDefaultDevice()); - - int start = Math.max(mBinding.editExpression.getSelectionStart(), 0); - int end = Math.max(mBinding.editExpression.getSelectionEnd(), 0); - mBinding.editExpression.getText().replace( - Math.min(start, end), Math.max(start, end), expression, 0, expression.length()); - } - - private void selectDefaultDevice() - { - String defaultDevice = mController.getDefaultDevice(); - boolean isInput = mControlReference.isInput(); - - if (Arrays.asList(mDevices).contains(defaultDevice) && - (isInput || deviceHasOutputs(defaultDevice))) - { - // The default device is available, and it's an appropriate choice. Pick it - setSelectedDevice(defaultDevice); - mBinding.dropdownDevice.setText(defaultDevice, false); - return; - } - else if (!isInput) - { - // Find the first device that has an output. (Most built-in devices don't have any) - Optional deviceWithOutputs = Arrays.stream(mDevices) - .filter(AdvancedMappingDialog::deviceHasOutputs) - .findFirst(); - - if (deviceWithOutputs.isPresent()) - { - setSelectedDevice(deviceWithOutputs.get()); - mBinding.dropdownDevice.setText(deviceWithOutputs.get(), false); - return; - } - } - - // Nothing found - setSelectedDevice(""); - } - - private static boolean deviceHasOutputs(String deviceString) - { - CoreDevice device = ControllerInterface.getDevice(deviceString); - return device != null && device.getOutputs().length > 0; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.kt new file mode 100644 index 0000000000..72d08c26aa --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.kt @@ -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 = 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) = + 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() + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/MotionAlertDialog.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/MotionAlertDialog.java deleted file mode 100644 index 9872bdd01e..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/MotionAlertDialog.java +++ /dev/null @@ -1,99 +0,0 @@ -// 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.annotation.NonNull; -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; - -/** - * {@link AlertDialog} derivative that listens for - * motion events from controllers and joysticks. - */ -public final class MotionAlertDialog extends AlertDialog -{ - private final Activity mActivity; - private final InputMappingControlSetting mSetting; - private final boolean mAllDevices; - private boolean mRunning = false; - - /** - * Constructor - * - * @param activity The current {@link Activity}. - * @param setting The setting to show this dialog for. - * @param allDevices Whether to detect inputs from devices other than the configured one. - */ - public MotionAlertDialog(Activity activity, InputMappingControlSetting setting, - boolean allDevices) - { - super(activity); - - mActivity = activity; - mSetting = setting; - mAllDevices = allDevices; - } - - @Override - protected void onStart() - { - super.onStart(); - - mRunning = true; - new Thread(() -> - { - String result = MappingCommon.detectInput(mSetting.getController(), mAllDevices); - mActivity.runOnUiThread(() -> - { - if (mRunning) - { - mSetting.setValue(result); - dismiss(); - } - }); - }).start(); - } - - @Override - protected void onStop() - { - super.onStop(); - mRunning = false; - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) - { - ControllerInterface.dispatchKeyEvent(event); - - if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && event.isLongPress()) - { - // Special case: Let the user cancel by long-pressing Back (intended for non-touch devices) - mSetting.clearValue(); - dismiss(); - } - - return true; - } - - @Override - public boolean dispatchGenericMotionEvent(@NonNull MotionEvent event) - { - if ((event.getSource() & 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; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/MotionAlertDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/MotionAlertDialog.kt new file mode 100644 index 0000000000..ff4a278f4c --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/MotionAlertDialog.kt @@ -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 + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileAdapter.java deleted file mode 100644 index 9908d6f5fd..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileAdapter.java +++ /dev/null @@ -1,65 +0,0 @@ -// 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.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import org.dolphinemu.dolphinemu.databinding.ListItemProfileBinding; - -public final class ProfileAdapter extends RecyclerView.Adapter -{ - private final Context mContext; - private final ProfileDialogPresenter mPresenter; - - private final String[] mStockProfileNames; - private final String[] mUserProfileNames; - - public ProfileAdapter(Context context, ProfileDialogPresenter presenter) - { - mContext = context; - mPresenter = presenter; - - mStockProfileNames = presenter.getProfileNames(true); - mUserProfileNames = presenter.getProfileNames(false); - } - - @NonNull @Override - public ProfileViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) - { - LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - - ListItemProfileBinding binding = ListItemProfileBinding.inflate(inflater, parent, false); - return new ProfileViewHolder(mPresenter, binding); - } - - @Override - public void onBindViewHolder(@NonNull ProfileViewHolder holder, int position) - { - if (position < mStockProfileNames.length) - { - holder.bind(mStockProfileNames[position], true); - return; - } - - position -= mStockProfileNames.length; - - if (position < mUserProfileNames.length) - { - holder.bind(mUserProfileNames[position], false); - return; - } - - holder.bindAsEmpty(mContext); - } - - @Override - public int getItemCount() - { - return mStockProfileNames.length + mUserProfileNames.length + 1; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileAdapter.kt new file mode 100644 index 0000000000..71aa166cc9 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileAdapter.kt @@ -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() { + private val stockProfileNames: Array = presenter.getProfileNames(true) + private val userProfileNames: Array = 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 +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialog.kt index acc3f605a9..4ef3dae51f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialog.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialog.kt @@ -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(KEY_MENU_TAG) + val menuTag = requireArguments().serializable(KEY_MENU_TAG)!! presenter = ProfileDialogPresenter(this, menuTag) @@ -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 diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialogPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialogPresenter.java deleted file mode 100644 index 667cc9017c..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialogPresenter.java +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.features.input.ui; - -import android.content.Context; -import android.view.LayoutInflater; - -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.textfield.TextInputEditText; - -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.text.Collator; -import java.util.Arrays; - -public final class ProfileDialogPresenter -{ - private static final String EXTENSION = ".ini"; - - private final Context mContext; - private final DialogFragment mDialog; - private final MenuTag mMenuTag; - - public ProfileDialogPresenter(MenuTag menuTag) - { - mContext = null; - mDialog = null; - mMenuTag = menuTag; - } - - public ProfileDialogPresenter(DialogFragment dialog, MenuTag menuTag) - { - mContext = dialog.getContext(); - mDialog = dialog; - mMenuTag = menuTag; - } - - public String[] getProfileNames(boolean stock) - { - File[] profiles = new File(getProfileDirectoryPath(stock)).listFiles( - file -> !file.isDirectory() && file.getName().endsWith(EXTENSION)); - - if (profiles == null) - return new String[0]; - - return Arrays.stream(profiles) - .map(file -> file.getName().substring(0, file.getName().length() - EXTENSION.length())) - .sorted(Collator.getInstance()) - .toArray(String[]::new); - } - - public void loadProfile(@NonNull String profileName, boolean stock) - { - new MaterialAlertDialogBuilder(mContext) - .setMessage(mContext.getString(R.string.input_profile_confirm_load, profileName)) - .setPositiveButton(R.string.yes, (dialogInterface, i) -> - { - mMenuTag.getCorrespondingEmulatedController() - .loadProfile(getProfilePath(profileName, stock)); - ((SettingsActivityView) mDialog.requireActivity()).onControllerSettingsChanged(); - mDialog.dismiss(); - }) - .setNegativeButton(R.string.no, null) - .show(); - } - - public void saveProfile(@NonNull String profileName) - { - // 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. - - String profilePath = getProfilePath(profileName, false); - if (!new File(profilePath).exists()) - { - mMenuTag.getCorrespondingEmulatedController().saveProfile(profilePath); - mDialog.dismiss(); - } - else - { - new MaterialAlertDialogBuilder(mContext) - .setMessage(mContext.getString(R.string.input_profile_confirm_save, profileName)) - .setPositiveButton(R.string.yes, (dialogInterface, i) -> - { - mMenuTag.getCorrespondingEmulatedController().saveProfile(profilePath); - mDialog.dismiss(); - }) - .setNegativeButton(R.string.no, null) - .show(); - } - } - - public void saveProfileAndPromptForName() - { - LayoutInflater inflater = LayoutInflater.from(mContext); - - DialogInputStringBinding binding = DialogInputStringBinding.inflate(inflater); - TextInputEditText input = binding.input; - - new MaterialAlertDialogBuilder(mContext) - .setView(binding.getRoot()) - .setPositiveButton(R.string.ok, (dialogInterface, i) -> - saveProfile(input.getText().toString())) - .setNegativeButton(R.string.cancel, null) - .show(); - } - - public void deleteProfile(@NonNull String profileName) - { - new MaterialAlertDialogBuilder(mContext) - .setMessage(mContext.getString(R.string.input_profile_confirm_delete, profileName)) - .setPositiveButton(R.string.yes, (dialogInterface, i) -> - { - new File(getProfilePath(profileName, false)).delete(); - mDialog.dismiss(); - }) - .setNegativeButton(R.string.no, null) - .show(); - } - - private String getProfileDirectoryName() - { - if (mMenuTag.isGCPadMenu()) - return "GCPad"; - else if (mMenuTag.isWiimoteMenu()) - return "Wiimote"; - else - throw new UnsupportedOperationException(); - } - - private String getProfileDirectoryPath(boolean stock) - { - if (stock) - { - return DirectoryInitialization.getSysDirectory() + "/Profiles/" + getProfileDirectoryName() + - '/'; - } - else - { - return DirectoryInitialization.getUserDirectory() + "/Config/Profiles/" + - getProfileDirectoryName() + '/'; - } - } - - private String getProfilePath(String profileName, boolean stock) - { - return getProfileDirectoryPath(stock) + profileName + EXTENSION; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialogPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialogPresenter.kt new file mode 100644 index 0000000000..624441aa73 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialogPresenter.kt @@ -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 { + 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" + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileViewHolder.java deleted file mode 100644 index d4d40c0c65..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileViewHolder.java +++ /dev/null @@ -1,76 +0,0 @@ -// 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.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.databinding.ListItemProfileBinding; - -public class ProfileViewHolder extends RecyclerView.ViewHolder -{ - private final ProfileDialogPresenter mPresenter; - private final ListItemProfileBinding mBinding; - - private String mProfileName; - private boolean mStock; - - public ProfileViewHolder(@NonNull ProfileDialogPresenter presenter, - @NonNull ListItemProfileBinding binding) - { - super(binding.getRoot()); - - mPresenter = presenter; - mBinding = binding; - - binding.buttonLoad.setOnClickListener(view -> loadProfile()); - binding.buttonSave.setOnClickListener(view -> saveProfile()); - binding.buttonDelete.setOnClickListener(view -> deleteProfile()); - } - - public void bind(String profileName, boolean stock) - { - mProfileName = profileName; - mStock = stock; - - mBinding.textName.setText(profileName); - - mBinding.buttonLoad.setVisibility(View.VISIBLE); - mBinding.buttonSave.setVisibility(stock ? View.GONE : View.VISIBLE); - mBinding.buttonDelete.setVisibility(stock ? View.GONE : View.VISIBLE); - } - - public void bindAsEmpty(Context context) - { - mProfileName = null; - mStock = false; - - mBinding.textName.setText(context.getText(R.string.input_profile_new)); - - mBinding.buttonLoad.setVisibility(View.GONE); - mBinding.buttonSave.setVisibility(View.VISIBLE); - mBinding.buttonDelete.setVisibility(View.GONE); - } - - private void loadProfile() - { - mPresenter.loadProfile(mProfileName, mStock); - } - - private void saveProfile() - { - if (mProfileName == null) - mPresenter.saveProfileAndPromptForName(); - else - mPresenter.saveProfile(mProfileName); - } - - private void deleteProfile() - { - mPresenter.deleteProfile(mProfileName); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileViewHolder.kt new file mode 100644 index 0000000000..671461c9aa --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileViewHolder.kt @@ -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!!) +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/viewholder/InputMappingControlSettingViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/viewholder/InputMappingControlSettingViewHolder.java deleted file mode 100644 index fd4b92cf95..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/viewholder/InputMappingControlSettingViewHolder.java +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.features.input.ui.viewholder; - -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -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; - -public final class InputMappingControlSettingViewHolder extends SettingViewHolder -{ - private InputMappingControlSetting mItem; - - private final ListItemMappingBinding mBinding; - - public InputMappingControlSettingViewHolder(@NonNull ListItemMappingBinding binding, - SettingsAdapter adapter) - { - super(binding.getRoot(), adapter); - mBinding = binding; - } - - @Override - public void bind(SettingsItem item) - { - mItem = (InputMappingControlSetting) item; - - mBinding.textSettingName.setText(mItem.getName()); - mBinding.textSettingDescription.setText(mItem.getValue()); - mBinding.buttonAdvancedSettings.setOnClickListener(this::onLongClick); - - setStyle(mBinding.textSettingName, mItem); - } - - @Override - public void onClick(View clicked) - { - if (!mItem.isEditable()) - { - showNotRuntimeEditableError(); - return; - } - - if (mItem.isInput()) - getAdapter().onInputMappingClick(mItem, getBindingAdapterPosition()); - else - getAdapter().onAdvancedInputMappingClick(mItem, getBindingAdapterPosition()); - - setStyle(mBinding.textSettingName, mItem); - } - - @Override - public boolean onLongClick(View clicked) - { - if (!mItem.isEditable()) - { - showNotRuntimeEditableError(); - return true; - } - - getAdapter().onAdvancedInputMappingClick(mItem, getBindingAdapterPosition()); - - return true; - } - - @Nullable @Override - protected SettingsItem getItem() - { - return mItem; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/viewholder/InputMappingControlSettingViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/viewholder/InputMappingControlSettingViewHolder.kt new file mode 100644 index 0000000000..5bbcbf07c4 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/viewholder/InputMappingControlSettingViewHolder.kt @@ -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 + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/PostProcessing.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/PostProcessing.kt index 1bb1aa04d7..04fe2734bb 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/PostProcessing.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/PostProcessing.kt @@ -4,14 +4,14 @@ package org.dolphinemu.dolphinemu.features.settings.model object PostProcessing { @JvmStatic - val shaderList: Array + val shaderList: Array external get @JvmStatic - val anaglyphShaderList: Array + val anaglyphShaderList: Array external get @JvmStatic - val passiveShaderList: Array + val passiveShaderList: Array external get } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/StringSingleChoiceSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/StringSingleChoiceSetting.kt index 62301e23ef..a67992ce2b 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/StringSingleChoiceSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/StringSingleChoiceSetting.kt @@ -17,9 +17,9 @@ open class StringSingleChoiceSetting : SettingsItem { override val setting: AbstractSetting? get() = stringSetting - var choices: Array? + var choices: Array? protected set - var values: Array? + var values: Array? protected set val menuTag: MenuTag? var noChoicesAvailableString = 0 @@ -37,8 +37,8 @@ open class StringSingleChoiceSetting : SettingsItem { setting: AbstractStringSetting?, titleId: Int, descriptionId: Int, - choices: Array?, - values: Array?, + choices: Array?, + values: Array?, menuTag: MenuTag? = null ) : super(context, titleId, descriptionId) { stringSetting = setting @@ -52,8 +52,8 @@ open class StringSingleChoiceSetting : SettingsItem { setting: AbstractStringSetting, titleId: Int, descriptionId: Int, - choices: Array, - values: Array, + choices: Array, + values: Array, noChoicesAvailableString: Int ) : this(context, setting, titleId, descriptionId, choices, values) { this.noChoicesAvailableString = noChoicesAvailableString @@ -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() {} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt index d5df7c0e09..3623f167bb 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt @@ -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) @@ -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() } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt index 59858193df..0947e4f294 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt @@ -41,7 +41,7 @@ class SettingsFragmentPresenter( private val fragmentView: SettingsFragmentView, private val context: Context ) { - private var menuTag: MenuTag? = null + private lateinit var menuTag: MenuTag private var gameId: String? = null private var settingsList: ArrayList? = null @@ -78,7 +78,7 @@ class SettingsFragmentPresenter( } } - fun onViewCreated(menuTag: MenuTag?, settings: Settings?) { + fun onViewCreated(menuTag: MenuTag, settings: Settings?) { this.menuTag = menuTag if (!TextUtils.isEmpty(gameId)) { @@ -1339,13 +1339,8 @@ class SettingsFragmentPresenter( val shaderList = if (stereoModeValue == anaglyphMode) PostProcessing.anaglyphShaderList else PostProcessing.shaderList - val shaderListEntries = arrayOfNulls(shaderList.size + 1) - shaderListEntries[0] = context.getString(R.string.off) - System.arraycopy(shaderList, 0, shaderListEntries, 1, shaderList.size) - - val shaderListValues = arrayOfNulls(shaderList.size + 1) - shaderListValues[0] = "" - System.arraycopy(shaderList, 0, shaderListValues, 1, shaderList.size) + val shaderListEntries = arrayOf(context.getString(R.string.off), *shaderList) + val shaderListValues = arrayOf("", *shaderList) sl.add( StringSingleChoiceSetting( @@ -2153,7 +2148,7 @@ class SettingsFragmentPresenter( profileString: String, controllerNumber: Int ) { - val profiles = ProfileDialogPresenter(menuTag!!).getProfileNames(false) + val profiles = ProfileDialogPresenter(menuTag).getProfileNames(false) val profileKey = profileString + "Profile" + (controllerNumber + 1) sl.add( StringSingleChoiceSetting( @@ -2232,7 +2227,7 @@ class SettingsFragmentPresenter( 0, 0, true - ) { fragmentView.showDialogFragment(ProfileDialog.create(menuTag!!)) }) + ) { fragmentView.showDialogFragment(ProfileDialog.create(menuTag)) }) updateOldControllerSettingsWarningVisibility(controller) } @@ -2251,15 +2246,15 @@ class SettingsFragmentPresenter( ) { updateOldControllerSettingsWarningVisibility(controller) - val groupCount = controller.groupCount + val groupCount = controller.getGroupCount() for (i in 0 until groupCount) { val group = controller.getGroup(i) - val groupType = group.groupType + val groupType = group.getGroupType() if (groupTypeFilter != null && !groupTypeFilter.contains(groupType)) continue - sl.add(HeaderSetting(group.uiName, "")) + sl.add(HeaderSetting(group.getUiName(), "")) - if (group.defaultEnabledValue != ControlGroup.DEFAULT_ENABLED_ALWAYS) { + if (group.getDefaultEnabledValue() != ControlGroup.DEFAULT_ENABLED_ALWAYS) { sl.add( SwitchSetting( context, @@ -2270,13 +2265,13 @@ class SettingsFragmentPresenter( ) } - val controlCount = group.controlCount + val controlCount = group.getControlCount() for (j in 0 until controlCount) { sl.add(InputMappingControlSetting(group.getControl(j), controller)) } if (groupType == ControlGroup.TYPE_ATTACHMENTS) { - val attachmentSetting = group.attachmentSetting + val attachmentSetting = group.getAttachmentSetting() sl.add( SingleChoiceSetting( context, InputMappingIntSetting(attachmentSetting), @@ -2287,7 +2282,7 @@ class SettingsFragmentPresenter( ) } - val numericSettingCount = group.numericSettingCount + val numericSettingCount = group.getNumericSettingCount() for (j in 0 until numericSettingCount) { addNumericSetting(sl, group.getNumericSetting(j)) } @@ -2295,34 +2290,34 @@ class SettingsFragmentPresenter( } private fun addNumericSetting(sl: ArrayList, setting: NumericSetting) { - when (setting.type) { + when (setting.getType()) { NumericSetting.TYPE_DOUBLE -> sl.add( FloatSliderSetting( InputMappingDoubleSetting(setting), - setting.uiName, + setting.getUiName(), "", - ceil(setting.doubleMin).toInt(), - floor(setting.doubleMax).toInt(), - setting.uiSuffix + ceil(setting.getDoubleMin()).toInt(), + floor(setting.getDoubleMax()).toInt(), + setting.getUiSuffix() ) ) NumericSetting.TYPE_BOOLEAN -> sl.add( SwitchSetting( InputMappingBooleanSetting(setting), - setting.uiName, - setting.uiDescription + setting.getUiName(), + setting.getUiDescription() ) ) } } fun updateOldControllerSettingsWarningVisibility() { - updateOldControllerSettingsWarningVisibility(menuTag!!.correspondingEmulatedController) + updateOldControllerSettingsWarningVisibility(menuTag.correspondingEmulatedController) } private fun updateOldControllerSettingsWarningVisibility(controller: EmulatedController) { - val defaultDevice = controller.defaultDevice + val defaultDevice = controller.getDefaultDevice() hasOldControllerSettings = defaultDevice.startsWith("Android/") && defaultDevice.endsWith("/Touchscreen") diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index f83707cebe..968d1b7e82 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -703,21 +703,21 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) const jclass control_class = env->FindClass("org/dolphinemu/dolphinemu/features/input/model/controlleremu/Control"); s_control_class = reinterpret_cast(env->NewGlobalRef(control_class)); - s_control_pointer = env->GetFieldID(control_class, "mPointer", "J"); + s_control_pointer = env->GetFieldID(control_class, "pointer", "J"); s_control_constructor = env->GetMethodID(control_class, "", "(J)V"); env->DeleteLocalRef(control_class); const jclass control_group_class = env->FindClass("org/dolphinemu/dolphinemu/features/input/model/controlleremu/ControlGroup"); s_control_group_class = reinterpret_cast(env->NewGlobalRef(control_group_class)); - s_control_group_pointer = env->GetFieldID(control_group_class, "mPointer", "J"); + s_control_group_pointer = env->GetFieldID(control_group_class, "pointer", "J"); s_control_group_constructor = env->GetMethodID(control_group_class, "", "(J)V"); env->DeleteLocalRef(control_group_class); const jclass control_reference_class = env->FindClass( "org/dolphinemu/dolphinemu/features/input/model/controlleremu/ControlReference"); s_control_reference_class = reinterpret_cast(env->NewGlobalRef(control_reference_class)); - s_control_reference_pointer = env->GetFieldID(control_reference_class, "mPointer", "J"); + s_control_reference_pointer = env->GetFieldID(control_reference_class, "pointer", "J"); s_control_reference_constructor = env->GetMethodID(control_reference_class, "", "(J)V"); env->DeleteLocalRef(control_reference_class); @@ -725,21 +725,21 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) "org/dolphinemu/dolphinemu/features/input/model/controlleremu/EmulatedController"); s_emulated_controller_class = reinterpret_cast(env->NewGlobalRef(emulated_controller_class)); - s_emulated_controller_pointer = env->GetFieldID(emulated_controller_class, "mPointer", "J"); + s_emulated_controller_pointer = env->GetFieldID(emulated_controller_class, "pointer", "J"); s_emulated_controller_constructor = env->GetMethodID(emulated_controller_class, "", "(J)V"); env->DeleteLocalRef(emulated_controller_class); const jclass numeric_setting_class = env->FindClass("org/dolphinemu/dolphinemu/features/input/model/controlleremu/NumericSetting"); s_numeric_setting_class = reinterpret_cast(env->NewGlobalRef(numeric_setting_class)); - s_numeric_setting_pointer = env->GetFieldID(numeric_setting_class, "mPointer", "J"); + s_numeric_setting_pointer = env->GetFieldID(numeric_setting_class, "pointer", "J"); s_numeric_setting_constructor = env->GetMethodID(numeric_setting_class, "", "(J)V"); env->DeleteLocalRef(numeric_setting_class); const jclass core_device_class = env->FindClass("org/dolphinemu/dolphinemu/features/input/model/CoreDevice"); s_core_device_class = reinterpret_cast(env->NewGlobalRef(core_device_class)); - s_core_device_pointer = env->GetFieldID(core_device_class, "mPointer", "J"); + s_core_device_pointer = env->GetFieldID(core_device_class, "pointer", "J"); s_core_device_constructor = env->GetMethodID(core_device_class, "", "(J)V"); env->DeleteLocalRef(core_device_class); @@ -747,7 +747,7 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) env->FindClass("org/dolphinemu/dolphinemu/features/input/model/CoreDevice$Control"); s_core_device_control_class = reinterpret_cast(env->NewGlobalRef(core_device_control_class)); - s_core_device_control_pointer = env->GetFieldID(core_device_control_class, "mPointer", "J"); + s_core_device_control_pointer = env->GetFieldID(core_device_control_class, "pointer", "J"); s_core_device_control_constructor = env->GetMethodID(core_device_control_class, "", "(Lorg/dolphinemu/dolphinemu/features/input/model/CoreDevice;J)V");