This file was deleted.

This file was deleted.

@@ -18,7 +18,6 @@
import org.dolphinemu.dolphinemu.dialogs.AlertMessage;
import org.dolphinemu.dolphinemu.utils.CompressCallback;
import org.dolphinemu.dolphinemu.utils.Log;
import org.dolphinemu.dolphinemu.utils.Rumble;

import java.lang.ref.WeakReference;
import java.util.LinkedHashMap;
@@ -44,7 +43,7 @@ public static EmulationActivity getEmulationActivity()
}

/**
* Button type for use in onTouchEvent
* Button type, for legacy use only
*/
public static final class ButtonType
{
@@ -234,52 +233,6 @@ private NativeLibrary()
// Disallows instantiation.
}

/**
* Default touchscreen device
*/
public static final String TouchScreenDevice = "Touchscreen";

/**
* Handles button press events for a gamepad.
*
* @param Device The input descriptor of the gamepad.
* @param Button Key code identifying which button was pressed.
* @param Action Mask identifying which action is happening (button pressed down, or button released).
* @return If we handled the button press.
*/
public static native boolean onGamePadEvent(String Device, int Button, int Action);

/**
* Handles gamepad movement events.
*
* @param Device The device ID of the gamepad.
* @param Axis The axis ID
* @param Value The value of the axis represented by the given ID.
*/
public static native void onGamePadMoveEvent(String Device, int Axis, float Value);

/**
* Rumble sent from native. Currently only supports phone rumble.
*
* @param padID Ignored for now. Future use would be to pass rumble to a connected controller
* @param state Ignored for now since phone rumble can't just be 'turned' on/off
*/
@Keep
public static void rumble(int padID, double state)
{
final EmulationActivity emulationActivity = sEmulationActivity.get();
if (emulationActivity == null)
{
Log.warning("[NativeLibrary] EmulationActivity is null");
return;
}

Rumble.checkRumble(padID, state);
}

public static native void SetMotionSensorsEnabled(boolean accelerometerEnabled,
boolean gyroscopeEnabled);

/**
* Gets the Dolphin version string.
*
@@ -451,8 +404,6 @@ public static native void Run(String[] path, boolean riivolution, String savesta
*/
public static native void RefreshWiimotes();

public static native void ReloadWiimoteConfig();

public static native LinkedHashMap<String, String> GetLogTypeNames();

public static native void ReloadLoggerConfig();

Large diffs are not rendered by default.

This file was deleted.

@@ -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;
}
}
@@ -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);
}
}
}
@@ -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();
}

Large diffs are not rendered by default.

@@ -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();
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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();
}
@@ -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();
}
@@ -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();
}
@@ -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();
}
@@ -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);
}
@@ -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();
}
@@ -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, "");
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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
}
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -100,8 +100,6 @@ public enum BooleanSetting implements AbstractBooleanSetting
"UseBlackBackgrounds", false),
MAIN_JOYSTICK_REL_CENTER(Settings.FILE_DOLPHIN, Settings.SECTION_INI_ANDROID,
"JoystickRelCenter", true),
MAIN_PHONE_RUMBLE(Settings.FILE_DOLPHIN, Settings.SECTION_INI_ANDROID,
"PhoneRumble", true),
MAIN_SHOW_INPUT_OVERLAY(Settings.FILE_DOLPHIN, Settings.SECTION_INI_ANDROID,
"ShowInputOverlay", true),
MAIN_IR_ALWAYS_RECENTER(Settings.FILE_DOLPHIN, Settings.SECTION_INI_ANDROID,
@@ -33,6 +33,10 @@ public enum IntSetting implements AbstractIntSetting

MAIN_AUDIO_VOLUME(Settings.FILE_DOLPHIN, Settings.SECTION_INI_DSP, "Volume", 100),

MAIN_OVERLAY_GC_CONTROLLER(Settings.FILE_DOLPHIN, Settings.SECTION_INI_ANDROID,
"OverlayGCController", 0), // Defaults to GameCube controller 1
MAIN_OVERLAY_WII_CONTROLLER(Settings.FILE_DOLPHIN, Settings.SECTION_INI_ANDROID,
"OverlayWiiController", 4), // Defaults to Wii Remote 1
MAIN_CONTROL_SCALE(Settings.FILE_DOLPHIN, Settings.SECTION_INI_ANDROID, "ControlScale", 50),
MAIN_CONTROL_OPACITY(Settings.FILE_DOLPHIN, Settings.SECTION_INI_ANDROID, "ControlOpacity", 65),
MAIN_EMULATION_ORIENTATION(Settings.FILE_DOLPHIN, Settings.SECTION_INI_ANDROID,
@@ -41,7 +45,6 @@ public enum IntSetting implements AbstractIntSetting
MAIN_INTERFACE_THEME_MODE(Settings.FILE_DOLPHIN, Settings.SECTION_INI_ANDROID,
"InterfaceThemeMode", -1),
MAIN_LAST_PLATFORM_TAB(Settings.FILE_DOLPHIN, Settings.SECTION_INI_ANDROID, "LastPlatformTab", 0),
MAIN_MOTION_CONTROLS(Settings.FILE_DOLPHIN, Settings.SECTION_INI_ANDROID, "MotionControls", 1),
MAIN_IR_MODE(Settings.FILE_DOLPHIN, Settings.SECTION_INI_ANDROID, "IRMode",
InputOverlayPointer.MODE_FOLLOW),

@@ -189,4 +192,16 @@ public void setIntGlobal(int layer, int newValue)
{
NativeConfig.setInt(layer, mFile, mSection, mKey, newValue);
}

public static IntSetting getSettingForSIDevice(int channel)
{
return new IntSetting[]{MAIN_SI_DEVICE_0, MAIN_SI_DEVICE_1, MAIN_SI_DEVICE_2, MAIN_SI_DEVICE_3}
[channel];
}

public static IntSetting getSettingForWiimoteSource(int index)
{
return new IntSetting[]{WIIMOTE_1_SOURCE, WIIMOTE_2_SOURCE, WIIMOTE_3_SOURCE, WIIMOTE_4_SOURCE,
WIIMOTE_BB_SOURCE}[index];
}
}

This file was deleted.

@@ -8,6 +8,7 @@

import org.dolphinemu.dolphinemu.NativeLibrary;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.features.input.model.MappingCommon;
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivityView;
import org.dolphinemu.dolphinemu.features.settings.utils.SettingsFile;
import org.dolphinemu.dolphinemu.services.GameFileCacheManager;
@@ -24,7 +25,6 @@ public class Settings implements Closeable
public static final String FILE_SYSCONF = "SYSCONF";
public static final String FILE_GFX = "GFX";
public static final String FILE_LOGGER = "Logger";
public static final String FILE_GCPAD = "GCPadNew";
public static final String FILE_WIIMOTE = "WiimoteNew";

public static final String SECTION_INI_ANDROID = "Android";
@@ -46,10 +46,7 @@ public class Settings implements Closeable

public static final String SECTION_STEREOSCOPY = "Stereoscopy";

public static final String SECTION_WIIMOTE = "Wiimote";

public static final String SECTION_BINDINGS = "Android";
public static final String SECTION_CONTROLS = "Controls";
public static final String SECTION_PROFILE = "Profile";

public static final String SECTION_ANALYTICS = "Analytics";
@@ -64,7 +61,6 @@ public class Settings implements Closeable
FILE_WIIMOTE};

private Map<String, IniFile> mIniFiles = new HashMap<>();
private final Map<String, IniFile> mWiimoteProfileFiles = new HashMap<>();

private boolean mLoadedRecursiveIsoPathsValue = false;

@@ -99,50 +95,6 @@ public boolean isWii()
return mIsWii;
}

public IniFile getWiimoteProfile(String profile, int padID)
{
IniFile wiimoteProfileIni = mWiimoteProfileFiles.computeIfAbsent(profile, profileComputed ->
{
IniFile newIni = new IniFile();
newIni.load(SettingsFile.getWiiProfile(profileComputed), false);
return newIni;
});

if (!wiimoteProfileIni.exists(SECTION_PROFILE))
{
String defaultWiiProfilePath = DirectoryInitialization.getUserDirectory() +
"/Config/Profiles/Wiimote/WiimoteProfile.ini";

wiimoteProfileIni.load(defaultWiiProfilePath, false);

wiimoteProfileIni
.setString(SECTION_PROFILE, "Device", "Android/" + (padID + 4) + "/Touchscreen");
}

return wiimoteProfileIni;
}

public void enableWiimoteProfile(Settings settings, String profile, String profileKey)
{
getWiimoteControlsSection(settings).setString(profileKey, profile);
}

public boolean disableWiimoteProfile(Settings settings, String profileKey)
{
return getWiimoteControlsSection(settings).delete(profileKey);
}

public boolean isWiimoteProfileEnabled(Settings settings, String profile,
String profileKey)
{
return profile.equals(getWiimoteControlsSection(settings).getString(profileKey, ""));
}

private IniFile.Section getWiimoteControlsSection(Settings settings)
{
return settings.getSection(GAME_SETTINGS_PLACEHOLDER_FILE_NAME, SECTION_CONTROLS);
}

public int getWriteLayer()
{
return isGameSpecific() ? NativeConfig.LAYER_LOCAL_GAME : NativeConfig.LAYER_BASE_OR_CURRENT;
@@ -218,13 +170,14 @@ public void saveSettings(SettingsActivityView view, Context context)
SettingsFile.saveFile(entry.getKey(), entry.getValue(), view);
}

MappingCommon.save();

NativeConfig.save(NativeConfig.LAYER_BASE);

if (!NativeLibrary.IsRunning())
{
// Notify the native code of the changes to legacy settings
NativeLibrary.ReloadConfig();
NativeLibrary.ReloadWiimoteConfig();
}

// LogManager does use the new config system, but doesn't pick up on changes automatically
@@ -251,11 +204,6 @@ public void saveSettings(SettingsActivityView view, Context context)

NativeConfig.save(NativeConfig.LAYER_LOCAL_GAME);
}

for (Map.Entry<String, IniFile> entry : mWiimoteProfileFiles.entrySet())
{
entry.getValue().save(SettingsFile.getWiiProfile(entry.getKey()));
}
}

public void clearSettings()

This file was deleted.

This file was deleted.

@@ -19,6 +19,13 @@ public FloatSliderSetting(Context context, AbstractFloatSetting setting, int tit
mSetting = setting;
}

public FloatSliderSetting(AbstractFloatSetting setting, CharSequence name,
CharSequence description, int min, int max, String units)
{
super(name, description, min, max, units);
mSetting = setting;
}

public int getSelectedValue(Settings settings)
{
return Math.round(mSetting.getFloat(settings));
@@ -13,6 +13,11 @@ public HeaderSetting(Context context, int titleId, int descriptionId)
super(context, titleId, descriptionId);
}

public HeaderSetting(CharSequence title, CharSequence description)
{
super(title, description);
}

@Override
public int getType()
{

This file was deleted.

This file was deleted.

@@ -21,9 +21,8 @@
public static final int TYPE_SINGLE_CHOICE = 2;
public static final int TYPE_SLIDER = 3;
public static final int TYPE_SUBMENU = 4;
public static final int TYPE_INPUT_BINDING = 5;
public static final int TYPE_INPUT_MAPPING_CONTROL = 5;
public static final int TYPE_STRING_SINGLE_CHOICE = 6;
public static final int TYPE_RUMBLE_BINDING = 7;
public static final int TYPE_SINGLE_CHOICE_DYNAMIC_DESCRIPTIONS = 8;
public static final int TYPE_FILE_PICKER = 9;
public static final int TYPE_RUN_RUNNABLE = 10;
@@ -96,6 +95,11 @@ public boolean hasSetting()
return getSetting() != null;
}

public boolean canClear()
{
return hasSetting();
}

public void clear(Settings settings)
{
getSetting().delete(settings);
@@ -23,6 +23,14 @@ public SliderSetting(Context context, int nameId, int descriptionId, int min, in
mStepSize = stepSize;
}

public SliderSetting(CharSequence name, CharSequence description, int min, int max, String units)
{
super(name, description);
mMin = min;
mMax = max;
mUnits = units;
}

public abstract int getSelectedValue(Settings settings);

public int getMin()
@@ -12,11 +12,12 @@

public class StringSingleChoiceSetting extends SettingsItem
{
private AbstractStringSetting mSetting;
private final AbstractStringSetting mSetting;

private String[] mChoices;
private String[] mValues;
private MenuTag mMenuTag;
protected String[] mChoices;
protected String[] mValues;
private final MenuTag mMenuTag;
private int mNoChoicesAvailableString = 0;

public StringSingleChoiceSetting(Context context, AbstractStringSetting setting, int titleId,
int descriptionId, String[] choices, String[] values, MenuTag menuTag)
@@ -34,6 +35,13 @@ public StringSingleChoiceSetting(Context context, AbstractStringSetting setting,
this(context, setting, titleId, descriptionId, choices, values, null);
}

public StringSingleChoiceSetting(Context context, AbstractStringSetting setting, int titleId,
int descriptionId, String[] choices, String[] values, int noChoicesAvailableString)
{
this(context, setting, titleId, descriptionId, choices, values, null);
mNoChoicesAvailableString = noChoicesAvailableString;
}

public StringSingleChoiceSetting(Context context, AbstractStringSetting setting, int titleId,
int descriptionId, int choicesId, int valuesId, MenuTag menuTag)
{
@@ -60,6 +68,19 @@ public String[] getValues()
return mValues;
}

public String getChoiceAt(int index)
{
if (mChoices == null)
return null;

if (index >= 0 && index < mChoices.length)
{
return mChoices[index];
}

return "";
}

public String getValueAt(int index)
{
if (mValues == null)
@@ -73,6 +94,11 @@ public String getValueAt(int index)
return "";
}

public String getSelectedChoice(Settings settings)
{
return getChoiceAt(getSelectedValueIndex(settings));
}

public String getSelectedValue(Settings settings)
{
return mSetting.getString(settings);
@@ -97,11 +123,20 @@ public MenuTag getMenuTag()
return mMenuTag;
}

public int getNoChoicesAvailableString()
{
return mNoChoicesAvailableString;
}

public void setSelectedValue(Settings settings, String selection)
{
mSetting.setString(settings, selection);
}

public void refreshChoicesAndValues()
{
}

@Override
public int getType()
{
@@ -4,6 +4,8 @@

import androidx.annotation.NonNull;

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

public enum MenuTag
{
SETTINGS("settings"),
@@ -31,14 +33,26 @@
GCPAD_2("gcpad", 1),
GCPAD_3("gcpad", 2),
GCPAD_4("gcpad", 3),
WIIMOTE_1("wiimote", 4),
WIIMOTE_2("wiimote", 5),
WIIMOTE_3("wiimote", 6),
WIIMOTE_4("wiimote", 7),
WIIMOTE_EXTENSION_1("wiimote_extension", 4),
WIIMOTE_EXTENSION_2("wiimote_extension", 5),
WIIMOTE_EXTENSION_3("wiimote_extension", 6),
WIIMOTE_EXTENSION_4("wiimote_extension", 7);
WIIMOTE_1("wiimote", 0),
WIIMOTE_2("wiimote", 1),
WIIMOTE_3("wiimote", 2),
WIIMOTE_4("wiimote", 3),
WIIMOTE_EXTENSION_1("wiimote_extension", 0),
WIIMOTE_EXTENSION_2("wiimote_extension", 1),
WIIMOTE_EXTENSION_3("wiimote_extension", 2),
WIIMOTE_EXTENSION_4("wiimote_extension", 3),
WIIMOTE_GENERAL_1("wiimote_general", 0),
WIIMOTE_GENERAL_2("wiimote_general", 1),
WIIMOTE_GENERAL_3("wiimote_general", 2),
WIIMOTE_GENERAL_4("wiimote_general", 3),
WIIMOTE_MOTION_SIMULATION_1("wiimote_motion_simulation", 0),
WIIMOTE_MOTION_SIMULATION_2("wiimote_motion_simulation", 1),
WIIMOTE_MOTION_SIMULATION_3("wiimote_motion_simulation", 2),
WIIMOTE_MOTION_SIMULATION_4("wiimote_motion_simulation", 3),
WIIMOTE_MOTION_INPUT_1("wiimote_motion_input", 0),
WIIMOTE_MOTION_INPUT_2("wiimote_motion_input", 1),
WIIMOTE_MOTION_INPUT_3("wiimote_motion_input", 2),
WIIMOTE_MOTION_INPUT_4("wiimote_motion_input", 3);

private String tag;
private int subType = -1;
@@ -76,6 +90,16 @@ public int getSubType()
return subType;
}

public EmulatedController getCorrespondingEmulatedController()
{
if (isGCPadMenu())
return EmulatedController.getGcPad(getSubType());
else if (isWiimoteMenu())
return EmulatedController.getWiimote(getSubType());
else
throw new UnsupportedOperationException();
}

public boolean isSerialPort1Menu()
{
return this == CONFIG_SERIALPORT1;
@@ -112,11 +136,27 @@ public static MenuTag getWiimoteExtensionMenuTag(int subtype)
return getMenuTag("wiimote_extension", subtype);
}

public static MenuTag getWiimoteGeneralMenuTag(int subtype)
{
return getMenuTag("wiimote_general", subtype);
}

public static MenuTag getWiimoteMotionSimulationMenuTag(int subtype)
{
return getMenuTag("wiimote_motion_simulation", subtype);
}

public static MenuTag getWiimoteMotionInputMenuTag(int subtype)
{
return getMenuTag("wiimote_motion_input", subtype);
}

private static MenuTag getMenuTag(String tag, int subtype)
{
for (MenuTag menuTag : MenuTag.values())
{
if (menuTag.tag.equals(tag) && menuTag.subType == subtype) return menuTag;
if (menuTag.tag.equals(tag) && menuTag.subType == subtype)
return menuTag;
}

throw new IllegalArgumentException("You are asking for a menu that is not available or " +