| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| // 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; | ||
|
|
||
| public class ControlGroupEnabledSetting implements AbstractBooleanSetting | ||
| { | ||
| private final ControlGroup mControlGroup; | ||
|
|
||
| public ControlGroupEnabledSetting(ControlGroup controlGroup) | ||
| { | ||
| mControlGroup = controlGroup; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean getBoolean(Settings settings) | ||
| { | ||
| return mControlGroup.getEnabled(); | ||
| } | ||
|
|
||
| @Override | ||
| public void setBoolean(Settings settings, boolean newValue) | ||
| { | ||
| mControlGroup.setEnabled(newValue); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isOverridden(Settings settings) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isRuntimeEditable() | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean delete(Settings settings) | ||
| { | ||
| boolean newValue = mControlGroup.getDefaultEnabledValue() != ControlGroup.DEFAULT_ENABLED_NO; | ||
| mControlGroup.setEnabled(newValue); | ||
|
|
||
| return true; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| // 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); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| // 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(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| // 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(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| // 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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| package org.dolphinemu.dolphinemu.features.input.model; | ||
|
|
||
| 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(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| // 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; | ||
|
|
||
| public class InputMappingBooleanSetting implements AbstractBooleanSetting | ||
| { | ||
| private final NumericSetting mNumericSetting; | ||
|
|
||
| public InputMappingBooleanSetting(NumericSetting numericSetting) | ||
| { | ||
| mNumericSetting = numericSetting; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean getBoolean(Settings settings) | ||
| { | ||
| return mNumericSetting.getBooleanValue(); | ||
| } | ||
|
|
||
| @Override | ||
| public void setBoolean(Settings settings, boolean newValue) | ||
| { | ||
| mNumericSetting.setBooleanValue(newValue); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isOverridden(Settings settings) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isRuntimeEditable() | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean delete(Settings settings) | ||
| { | ||
| mNumericSetting.setBooleanValue(mNumericSetting.getBooleanDefaultValue()); | ||
| return true; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| // 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 | ||
| public class InputMappingDoubleSetting implements AbstractFloatSetting | ||
| { | ||
| private final NumericSetting mNumericSetting; | ||
|
|
||
| public InputMappingDoubleSetting(NumericSetting numericSetting) | ||
| { | ||
| mNumericSetting = numericSetting; | ||
| } | ||
|
|
||
| @Override | ||
| public float getFloat(Settings settings) | ||
| { | ||
| return (float) mNumericSetting.getDoubleValue(); | ||
| } | ||
|
|
||
| @Override | ||
| public void setFloat(Settings settings, float newValue) | ||
| { | ||
| mNumericSetting.setDoubleValue(newValue); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isOverridden(Settings settings) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isRuntimeEditable() | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean delete(Settings settings) | ||
| { | ||
| mNumericSetting.setDoubleValue(mNumericSetting.getDoubleDefaultValue()); | ||
| return true; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| // 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; | ||
|
|
||
| public class InputMappingIntSetting implements AbstractIntSetting | ||
| { | ||
| private final NumericSetting mNumericSetting; | ||
|
|
||
| public InputMappingIntSetting(NumericSetting numericSetting) | ||
| { | ||
| mNumericSetting = numericSetting; | ||
| } | ||
|
|
||
| @Override | ||
| public int getInt(Settings settings) | ||
| { | ||
| return mNumericSetting.getIntValue(); | ||
| } | ||
|
|
||
| @Override | ||
| public void setInt(Settings settings, int newValue) | ||
| { | ||
| mNumericSetting.setIntValue(newValue); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isOverridden(Settings settings) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isRuntimeEditable() | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean delete(Settings settings) | ||
| { | ||
| mNumericSetting.setIntValue(mNumericSetting.getIntDefaultValue()); | ||
| return true; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| // 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(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| package org.dolphinemu.dolphinemu.features.input.model.controlleremu; | ||
|
|
||
| import androidx.annotation.Keep; | ||
|
|
||
| /** | ||
| * Represents a C++ ControllerEmu::Control. | ||
| * | ||
| * The lifetime of this class is managed by C++ code. Calling methods on it after it's destroyed | ||
| * in C++ is undefined behavior! | ||
| */ | ||
| public class Control | ||
| { | ||
| @Keep | ||
| private final long mPointer; | ||
|
|
||
| @Keep | ||
| private Control(long pointer) | ||
| { | ||
| mPointer = pointer; | ||
| } | ||
|
|
||
| public native String getUiName(); | ||
|
|
||
| public native ControlReference getControlReference(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| // 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(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| // 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(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| package org.dolphinemu.dolphinemu.features.input.model.controlleremu; | ||
|
|
||
| import androidx.annotation.Keep; | ||
|
|
||
| /** | ||
| * Represents a C++ ControllerEmu::EmulatedController. | ||
| * | ||
| * The lifetime of this class is managed by C++ code. Calling methods on it after it's destroyed | ||
| * in C++ is undefined behavior! | ||
| */ | ||
| 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); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| // 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(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| // 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(Settings settings) | ||
| { | ||
| return mController.getDefaultDevice(); | ||
| } | ||
|
|
||
| @Override | ||
| public String getSelectedValue(Settings settings) | ||
| { | ||
| return mController.getDefaultDevice(); | ||
| } | ||
|
|
||
| @Override | ||
| public void setSelectedValue(Settings settings, String newValue) | ||
| { | ||
| mController.setDefaultDevice(newValue); | ||
| } | ||
|
|
||
| @Override | ||
| public void refreshChoicesAndValues() | ||
| { | ||
| String[] devices = ControllerInterface.getAllDeviceStrings(); | ||
|
|
||
| mChoices = devices; | ||
| mValues = devices; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isEditable() | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean canClear() | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public void clear(Settings settings) | ||
| { | ||
| setSelectedValue(settings, ""); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| // 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(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| package org.dolphinemu.dolphinemu.features.input.ui; | ||
|
|
||
| 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<AdvancedMappingControlViewHolder> | ||
| { | ||
| private final Consumer<String> mOnClickCallback; | ||
|
|
||
| private String[] mControls = new String[0]; | ||
|
|
||
| public AdvancedMappingControlAdapter(Consumer<String> 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(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| // 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<String> onClickCallback) | ||
| { | ||
| super(binding.getRoot()); | ||
|
|
||
| mBinding = binding; | ||
|
|
||
| binding.getRoot().setOnClickListener(view -> onClickCallback.accept(mName)); | ||
| } | ||
|
|
||
| public void bind(String name) | ||
| { | ||
| mName = name; | ||
|
|
||
| mBinding.textName.setText(name); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| // 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<String> 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<String> 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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| // 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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| // 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<ProfileViewHolder> | ||
| { | ||
| 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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| package org.dolphinemu.dolphinemu.features.input.ui | ||
|
|
||
| import android.os.Build | ||
| import android.os.Bundle | ||
| import android.view.LayoutInflater | ||
| import android.view.View | ||
| import android.view.ViewGroup | ||
| import androidx.recyclerview.widget.LinearLayoutManager | ||
| import com.google.android.material.bottomsheet.BottomSheetBehavior | ||
| import com.google.android.material.bottomsheet.BottomSheetDialogFragment | ||
| import com.google.android.material.divider.MaterialDividerItemDecoration | ||
| import org.dolphinemu.dolphinemu.R | ||
| import org.dolphinemu.dolphinemu.databinding.DialogInputProfilesBinding | ||
| import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag | ||
|
|
||
| class ProfileDialog : BottomSheetDialogFragment() { | ||
| private var presenter: ProfileDialogPresenter? = null | ||
|
|
||
| private var _binding: DialogInputProfilesBinding? = null | ||
| private val binding get() = _binding!! | ||
|
|
||
| override fun onCreate(savedInstanceState: Bundle?) { | ||
| val menuTag: MenuTag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { | ||
| requireArguments().getSerializable(KEY_MENU_TAG, MenuTag::class.java) as MenuTag | ||
| } else { | ||
| requireArguments().getSerializable(KEY_MENU_TAG) as MenuTag | ||
| } | ||
|
|
||
| presenter = ProfileDialogPresenter(this, menuTag) | ||
|
|
||
| super.onCreate(savedInstanceState) | ||
| } | ||
|
|
||
| override fun onCreateView( | ||
| inflater: LayoutInflater, | ||
| container: ViewGroup?, | ||
| savedInstanceState: Bundle? | ||
| ): View { | ||
| _binding = DialogInputProfilesBinding.inflate(inflater, container, false) | ||
| return binding.root | ||
| } | ||
|
|
||
| override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| binding.profileList.adapter = ProfileAdapter(context, presenter) | ||
| binding.profileList.layoutManager = LinearLayoutManager(context) | ||
| val divider = MaterialDividerItemDecoration(requireActivity(), LinearLayoutManager.VERTICAL) | ||
| divider.isLastItemDecorated = false | ||
| binding.profileList.addItemDecoration(divider) | ||
|
|
||
| // You can't expand a bottom sheet with a controller/remote/other non-touch devices | ||
| val behavior: BottomSheetBehavior<View> = BottomSheetBehavior.from(view.parent as View) | ||
| if (!resources.getBoolean(R.bool.hasTouch)) { | ||
| behavior.state = BottomSheetBehavior.STATE_EXPANDED | ||
| } | ||
| } | ||
|
|
||
| override fun onDestroyView() { | ||
| super.onDestroyView() | ||
| _binding = null | ||
| } | ||
|
|
||
| companion object { | ||
| private const val KEY_MENU_TAG = "menu_tag" | ||
|
|
||
| @JvmStatic | ||
| fun create(menuTag: MenuTag): ProfileDialog { | ||
| val dialog = ProfileDialog() | ||
| val args = Bundle() | ||
| args.putSerializable(KEY_MENU_TAG, menuTag) | ||
| dialog.arguments = args | ||
| return dialog | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| // 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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| // 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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| // 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; | ||
| } | ||
| } |