mirror of
https://github.com/RetroDECK/Duckstation.git
synced 2024-11-26 23:55:40 +00:00
Android: Add automatic controller mapping option
This commit is contained in:
parent
ad991c122d
commit
0155d6ed61
|
@ -0,0 +1,290 @@
|
|||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Vibrator;
|
||||
import android.text.InputType;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ControllerAutoMapper {
|
||||
public interface CompleteCallback {
|
||||
public void onComplete();
|
||||
}
|
||||
|
||||
final private ControllerSettingsActivity parent;
|
||||
final private int port;
|
||||
private final CompleteCallback completeCallback;
|
||||
|
||||
private InputDevice device;
|
||||
private SharedPreferences prefs;
|
||||
private SharedPreferences.Editor editor;
|
||||
private StringBuilder log;
|
||||
private String keyBase;
|
||||
private String controllerType;
|
||||
|
||||
public ControllerAutoMapper(ControllerSettingsActivity activity, int port, CompleteCallback completeCallback) {
|
||||
this.parent = activity;
|
||||
this.port = port;
|
||||
this.completeCallback = completeCallback;
|
||||
}
|
||||
|
||||
private void log(String format, Object... args) {
|
||||
log.append(String.format(format, args));
|
||||
log.append('\n');
|
||||
}
|
||||
|
||||
private void setButtonBindingToKeyCode(String buttonName, int keyCode) {
|
||||
log("Binding button '%s' to key '%s' (%d)", buttonName, KeyEvent.keyCodeToString(keyCode), keyCode);
|
||||
|
||||
final String key = String.format("%sButton%s", keyBase, buttonName);
|
||||
final String value = String.format("%s/Button%d", device.getDescriptor(), keyCode);
|
||||
editor.putString(key, value);
|
||||
}
|
||||
|
||||
private void setButtonBindingToAxis(String buttonName, int axis, int direction) {
|
||||
final char directionIndicator = (direction < 0) ? '-' : '+';
|
||||
log("Binding button '%s' to axis '%s' (%d) direction %c", buttonName, MotionEvent.axisToString(axis), axis, directionIndicator);
|
||||
|
||||
final String key = String.format("%sButton%s", keyBase, buttonName);
|
||||
final String value = String.format("%s/%cAxis%d", device.getDescriptor(), directionIndicator, axis);
|
||||
editor.putString(key, value);
|
||||
}
|
||||
|
||||
private void setAxisBindingToAxis(String axisName, int axis) {
|
||||
log("Binding axis '%s' to axis '%s' (%d)", axisName, MotionEvent.axisToString(axis), axis);
|
||||
|
||||
final String key = String.format("%sAxis%s", keyBase, axisName);
|
||||
final String value = String.format("%s/Axis%d", device.getDescriptor(), axis);
|
||||
editor.putString(key, value);
|
||||
}
|
||||
|
||||
private void doAutoBindingButton(String buttonName, int[] keyCodes, int[][] axisCodes) {
|
||||
// Prefer the axis codes, as it dispatches to that first.
|
||||
if (axisCodes != null) {
|
||||
final List<InputDevice.MotionRange> motionRangeList = device.getMotionRanges();
|
||||
for (int[] axisAndDirection : axisCodes) {
|
||||
final int axis = axisAndDirection[0];
|
||||
final int direction = axisAndDirection[1];
|
||||
for (InputDevice.MotionRange range : motionRangeList) {
|
||||
if (range.getAxis() == axis) {
|
||||
setButtonBindingToAxis(buttonName, axis, direction);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (keyCodes != null) {
|
||||
final boolean[] keysPresent = device.hasKeys(keyCodes);
|
||||
for (int i = 0; i < keysPresent.length; i++) {
|
||||
if (keysPresent[i]) {
|
||||
setButtonBindingToKeyCode(buttonName, keyCodes[i]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log("No automatic bindings found for button '%s'", buttonName);
|
||||
}
|
||||
|
||||
private void doAutoBindingAxis(String axisName, int[] axisCodes) {
|
||||
// Prefer the axis codes, as it dispatches to that first.
|
||||
if (axisCodes != null) {
|
||||
final List<InputDevice.MotionRange> motionRangeList = device.getMotionRanges();
|
||||
for (final int axis : axisCodes) {
|
||||
for (InputDevice.MotionRange range : motionRangeList) {
|
||||
if (range.getAxis() == axis) {
|
||||
setAxisBindingToAxis(axisName, axis);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.append(String.format("No automatic bindings found for axis '%s'\n", axisName));
|
||||
}
|
||||
|
||||
public void start() {
|
||||
final ArrayList<InputDevice> deviceList = new ArrayList<>();
|
||||
for (final int deviceId : InputDevice.getDeviceIds()) {
|
||||
final InputDevice inputDevice = InputDevice.getDevice(deviceId);
|
||||
if (inputDevice == null || !EmulationSurfaceView.isBindableDevice(inputDevice) ||
|
||||
!EmulationSurfaceView.isGamepadDevice(inputDevice)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
deviceList.add(inputDevice);
|
||||
}
|
||||
|
||||
if (deviceList.isEmpty()) {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(parent);
|
||||
builder.setTitle(R.string.main_activity_error);
|
||||
builder.setMessage(R.string.controller_auto_mapping_no_devices);
|
||||
builder.setPositiveButton(R.string.main_activity_ok, (dialog, which) -> dialog.dismiss());
|
||||
builder.create().show();
|
||||
return;
|
||||
}
|
||||
|
||||
final String[] deviceNames = new String[deviceList.size()];
|
||||
for (int i = 0; i < deviceList.size(); i++)
|
||||
deviceNames[i] = deviceList.get(i).getName();
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(parent);
|
||||
builder.setTitle(R.string.controller_auto_mapping_select_device);
|
||||
builder.setItems(deviceNames, (dialog, which) -> {
|
||||
process(deviceList.get(which));
|
||||
});
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void process(InputDevice device) {
|
||||
this.prefs = PreferenceManager.getDefaultSharedPreferences(parent);
|
||||
this.editor = prefs.edit();
|
||||
this.log = new StringBuilder();
|
||||
this.device = device;
|
||||
|
||||
this.keyBase = String.format("Controller%d/", port);
|
||||
this.controllerType = parent.getControllerType(prefs, port);
|
||||
|
||||
setButtonBindings();
|
||||
setAxisBindings();
|
||||
setVibrationBinding();
|
||||
|
||||
this.editor.commit();
|
||||
this.editor = null;
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(parent);
|
||||
builder.setTitle(R.string.controller_auto_mapping_results);
|
||||
|
||||
final EditText editText = new EditText(parent);
|
||||
editText.setText(log.toString());
|
||||
editText.setInputType(InputType.TYPE_NULL | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
|
||||
editText.setSingleLine(false);
|
||||
editText.setMinLines(10);
|
||||
builder.setView(editText);
|
||||
|
||||
builder.setPositiveButton(R.string.main_activity_ok, (dialog, which) -> dialog.dismiss());
|
||||
builder.create().show();
|
||||
|
||||
if (completeCallback != null)
|
||||
completeCallback.onComplete();
|
||||
}
|
||||
|
||||
private void setButtonBindings() {
|
||||
final String[] buttonNames = AndroidHostInterface.getInstance().getControllerButtonNames(controllerType);
|
||||
if (buttonNames == null || buttonNames.length == 0) {
|
||||
log("No axes to bind.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (final String buttonName : buttonNames) {
|
||||
switch (buttonName) {
|
||||
case "Up":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_DPAD_UP}, new int[][]{{MotionEvent.AXIS_HAT_Y, -1}});
|
||||
break;
|
||||
case "Down":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_DPAD_DOWN}, new int[][]{{MotionEvent.AXIS_HAT_Y, 1}});
|
||||
break;
|
||||
case "Left":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_DPAD_LEFT}, new int[][]{{MotionEvent.AXIS_HAT_X, -1}});
|
||||
break;
|
||||
case "Right":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_DPAD_RIGHT}, new int[][]{{MotionEvent.AXIS_HAT_X, 1}});
|
||||
break;
|
||||
case "Select":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_SELECT}, null);
|
||||
break;
|
||||
case "Start":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_START}, null);
|
||||
break;
|
||||
case "Triangle":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_Y}, null);
|
||||
break;
|
||||
case "Cross":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_A}, null);
|
||||
break;
|
||||
case "Circle":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_B}, null);
|
||||
break;
|
||||
case "Square":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_X}, null);
|
||||
break;
|
||||
case "L1":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_L1}, null);
|
||||
break;
|
||||
case "L2":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_L2}, new int[][]{{MotionEvent.AXIS_LTRIGGER, 1}, {MotionEvent.AXIS_BRAKE, 1}});
|
||||
break;
|
||||
case "R1":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_R1}, null);
|
||||
break;
|
||||
case "R2":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_R2}, new int[][]{{MotionEvent.AXIS_RTRIGGER, 1}, {MotionEvent.AXIS_GAS, 1}});
|
||||
break;
|
||||
case "L3":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_THUMBL}, null);
|
||||
break;
|
||||
case "R3":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_THUMBR}, null);
|
||||
break;
|
||||
case "Analog":
|
||||
doAutoBindingButton(buttonName, new int[]{KeyEvent.KEYCODE_BUTTON_MODE}, null);
|
||||
break;
|
||||
default:
|
||||
log("Button '%s' not supported by auto mapping.", buttonName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setAxisBindings() {
|
||||
final String[] axisNames = AndroidHostInterface.getInstance().getControllerAxisNames(controllerType);
|
||||
if (axisNames == null || axisNames.length == 0) {
|
||||
log("No axes to bind.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (final String axisName : axisNames) {
|
||||
switch (axisName) {
|
||||
case "LeftX":
|
||||
doAutoBindingAxis(axisName, new int[]{MotionEvent.AXIS_X});
|
||||
break;
|
||||
case "LeftY":
|
||||
doAutoBindingAxis(axisName, new int[]{MotionEvent.AXIS_Y});
|
||||
break;
|
||||
case "RightX":
|
||||
doAutoBindingAxis(axisName, new int[]{MotionEvent.AXIS_Z, MotionEvent.AXIS_RX});
|
||||
break;
|
||||
case "RightY":
|
||||
doAutoBindingAxis(axisName, new int[]{MotionEvent.AXIS_RZ, MotionEvent.AXIS_RY});
|
||||
break;
|
||||
default:
|
||||
log("Axis '%s' not supported by auto mapping.", axisName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setVibrationBinding() {
|
||||
final int motorCount = AndroidHostInterface.getInstance().getControllerVibrationMotorCount(controllerType);
|
||||
if (motorCount == 0) {
|
||||
log("No vibration motors to bind.");
|
||||
return;
|
||||
}
|
||||
|
||||
final Vibrator vibrator = device.getVibrator();
|
||||
if (vibrator == null || !vibrator.hasVibrator()) {
|
||||
log("Selected device has no vibrator, cannot bind vibration.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -163,6 +163,15 @@ public class ControllerSettingsActivity extends AppCompatActivity {
|
|||
pref.updateValue();
|
||||
}
|
||||
|
||||
public static String getControllerTypeKey(int port) {
|
||||
return String.format("Controller%d/Type", port);
|
||||
}
|
||||
|
||||
public static String getControllerType(SharedPreferences prefs, int port) {
|
||||
final String defaultControllerType = (port == 1) ? "DigitalController" : "None";
|
||||
return prefs.getString(getControllerTypeKey(port), defaultControllerType);
|
||||
}
|
||||
|
||||
public static class SettingsFragment extends PreferenceFragmentCompat {
|
||||
ControllerSettingsActivity parent;
|
||||
|
||||
|
@ -216,9 +225,7 @@ public class ControllerSettingsActivity extends AppCompatActivity {
|
|||
private void createPreferences() {
|
||||
final PreferenceScreen ps = getPreferenceScreen();
|
||||
final SharedPreferences sp = getPreferenceManager().getSharedPreferences();
|
||||
final String defaultControllerType = (controllerIndex == 1) ? "DigitalController" : "None";
|
||||
final String controllerTypeKey = String.format("Controller%d/Type", controllerIndex);
|
||||
final String controllerType = sp.getString(controllerTypeKey, defaultControllerType);
|
||||
final String controllerType = getControllerType(sp, controllerIndex);
|
||||
final String[] controllerButtons = AndroidHostInterface.getControllerButtonNames(controllerType);
|
||||
final String[] axisButtons = AndroidHostInterface.getControllerAxisNames(controllerType);
|
||||
final int vibrationMotors = AndroidHostInterface.getControllerVibrationMotorCount(controllerType);
|
||||
|
@ -226,7 +233,7 @@ public class ControllerSettingsActivity extends AppCompatActivity {
|
|||
final ListPreference typePreference = new ListPreference(getContext());
|
||||
typePreference.setEntries(R.array.settings_controller_type_entries);
|
||||
typePreference.setEntryValues(R.array.settings_controller_type_values);
|
||||
typePreference.setKey(controllerTypeKey);
|
||||
typePreference.setKey(getControllerTypeKey(controllerIndex));
|
||||
typePreference.setValue(controllerType);
|
||||
typePreference.setTitle(R.string.settings_controller_type);
|
||||
typePreference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance());
|
||||
|
@ -238,6 +245,37 @@ public class ControllerSettingsActivity extends AppCompatActivity {
|
|||
});
|
||||
ps.addPreference(typePreference);
|
||||
|
||||
final Preference autoBindPreference = new Preference(getContext());
|
||||
autoBindPreference.setTitle(R.string.controller_settings_automatic_mapping);
|
||||
autoBindPreference.setSummary(R.string.controller_settings_summary_automatic_mapping);
|
||||
autoBindPreference.setIconSpaceReserved(false);
|
||||
autoBindPreference.setOnPreferenceClickListener(preference -> {
|
||||
final ControllerAutoMapper mapper = new ControllerAutoMapper(activity, controllerIndex, () -> {
|
||||
removePreferences();
|
||||
createPreferences(typePreference.getValue());
|
||||
});
|
||||
mapper.start();
|
||||
return true;
|
||||
});
|
||||
ps.addPreference(autoBindPreference);
|
||||
|
||||
final Preference clearBindingsPreference = new Preference(getContext());
|
||||
clearBindingsPreference.setTitle(R.string.controller_settings_clear_controller_bindings);
|
||||
clearBindingsPreference.setSummary(R.string.controller_settings_summary_clear_controller_bindings);
|
||||
clearBindingsPreference.setIconSpaceReserved(false);
|
||||
clearBindingsPreference.setOnPreferenceClickListener(preference -> {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setMessage(R.string.controller_settings_clear_controller_bindings_confirm);
|
||||
builder.setPositiveButton(R.string.main_activity_yes, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
clearBindings();
|
||||
});
|
||||
builder.setNegativeButton(R.string.main_activity_no, (dialog, which) -> dialog.dismiss());
|
||||
builder.create().show();
|
||||
return true;
|
||||
});
|
||||
ps.addPreference(clearBindingsPreference);
|
||||
|
||||
mButtonsCategory = new PreferenceCategory(getContext());
|
||||
mButtonsCategory.setTitle(getContext().getString(R.string.controller_settings_category_button_bindings));
|
||||
mButtonsCategory.setIconSpaceReserved(false);
|
||||
|
@ -315,6 +353,26 @@ public class ControllerSettingsActivity extends AppCompatActivity {
|
|||
}
|
||||
mSettingsCategory.removeAll();
|
||||
}
|
||||
|
||||
private static void clearBindingsInCategory(SharedPreferences.Editor editor, PreferenceCategory category) {
|
||||
for (int i = 0; i < category.getPreferenceCount(); i++) {
|
||||
final Preference preference = category.getPreference(i);
|
||||
if (preference instanceof ControllerBindingPreference)
|
||||
((ControllerBindingPreference)preference).clearBinding(editor);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearBindings() {
|
||||
final SharedPreferences.Editor editor = getPreferenceManager().getSharedPreferences().edit();
|
||||
clearBindingsInCategory(editor, mButtonsCategory);
|
||||
clearBindingsInCategory(editor, mAxisCategory);
|
||||
clearBindingsInCategory(editor, mSettingsCategory);
|
||||
editor.commit();
|
||||
|
||||
Toast.makeText(activity, activity.getString(
|
||||
R.string.controller_settings_clear_controller_bindings_done, controllerIndex),
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
public static class HotkeyFragment extends PreferenceFragmentCompat {
|
||||
|
|
|
@ -312,6 +312,15 @@
|
|||
<string name="memory_card_editor_import_card_failed">Failed to import card \'%s\'. It may not be a supported format.</string>
|
||||
<string name="memory_card_editor_import_card_success">Imported card \'%s\'.</string>
|
||||
<string name="menu_game_list_entry_choose_cover_image">Choose Cover Image</string>
|
||||
<string name="controller_settings_automatic_mapping">Perform Automatic Mapping</string>
|
||||
<string name="controller_settings_summary_automatic_mapping">Attempts to automatically bind all buttons/axes to a connected controller.</string>
|
||||
<string name="controller_settings_clear_controller_bindings">Clear Bindings</string>
|
||||
<string name="controller_settings_summary_clear_controller_bindings">Unbinds all buttons/axes for this controller.</string>
|
||||
<string name="controller_settings_clear_controller_bindings_confirm">Are you sure you want to clear all bindings? This cannot be reversed.</string>
|
||||
<string name="controller_settings_clear_controller_bindings_done">All bindings cleared for Controller %d.</string>
|
||||
<string name="controller_auto_mapping_no_devices">No suitable devices found. Automatic binding only supports gamepad devices, but you can still bind other device types manually.</string>
|
||||
<string name="controller_auto_mapping_select_device">Select Device</string>
|
||||
<string name="controller_auto_mapping_results">Automatic Binding Results</string>
|
||||
<string name="update_notes_title">Update Notes</string>
|
||||
<string name="update_notes_message_version_controller_update">This DuckStation update includes support for multiple controllers, and binding devices such as keyboards/volume buttons.\n\nYou must re-bind your controllers, otherwise they will no longer function. Do you want to do this now?</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue