summaryrefslogtreecommitdiff
path: root/Source/Android
diff options
context:
space:
mode:
authorJosJuice <josjuice@gmail.com>2022-12-27 16:29:01 +0100
committerJosJuice <josjuice@gmail.com>2023-03-07 17:39:30 +0100
commitc2779aef060137d9062cfd29babb3c43fc5fa2b5 (patch)
tree421bbf1ba198ec00b5e7dfe9118da77e079d89c1 /Source/Android
parent42943672bbbed6c18aa2bc85c41102a0616547f7 (diff)
Android: Add the advanced input mapping dialog
It's missing a lot of features from the PC version for now, like buttons for inserting functions and the ability to see what the expression evaluates to. I mostly just wanted to get something in place so you can set up rumble. Co-authored-by: Charles Lombardo <clombardo169@gmail.com>
Diffstat (limited to 'Source/Android')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.java3
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/CoreDevice.java48
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/MappingCommon.java3
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/ControlReference.java2
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputMappingControlSetting.java10
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.java55
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.java34
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.java151
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/viewholder/InputMappingControlSettingViewHolder.java26
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java43
-rw-r--r--Source/Android/app/src/main/res/drawable/ic_more.xml9
-rw-r--r--Source/Android/app/src/main/res/layout-ldrtl/list_item_mapping.xml56
-rw-r--r--Source/Android/app/src/main/res/layout/dialog_advanced_mapping.xml54
-rw-r--r--Source/Android/app/src/main/res/layout/list_item_advanced_mapping_control.xml27
-rw-r--r--Source/Android/app/src/main/res/layout/list_item_mapping.xml56
-rw-r--r--Source/Android/app/src/main/res/values/strings.xml4
-rw-r--r--Source/Android/jni/AndroidCommon/IDCache.cpp57
-rw-r--r--Source/Android/jni/AndroidCommon/IDCache.h8
-rw-r--r--Source/Android/jni/CMakeLists.txt2
-rw-r--r--Source/Android/jni/Input/ControlReference.cpp7
-rw-r--r--Source/Android/jni/Input/CoreDevice.cpp84
-rw-r--r--Source/Android/jni/Input/CoreDevice.h15
-rw-r--r--Source/Android/jni/Input/MappingCommon.cpp13
23 files changed, 762 insertions, 5 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.java
index b2535465e8..732ed97a7c 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.java
@@ -97,6 +97,9 @@ public final class ControllerInterface
public static native String[] getAllDeviceStrings();
+ @Nullable
+ public static native CoreDevice getDevice(String deviceString);
+
@Keep
private static void registerInputDeviceListener()
{
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/CoreDevice.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/CoreDevice.java
new file mode 100644
index 0000000000..b69e9a0236
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/CoreDevice.java
@@ -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();
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/MappingCommon.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/MappingCommon.java
index b0dc489fec..5e4c0076ea 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/MappingCommon.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/MappingCommon.java
@@ -27,5 +27,8 @@ public final class MappingCommon
public static native String detectInput(@NonNull EmulatedController controller,
boolean allDevices);
+ public static native String getExpressionForControl(String control, String device,
+ String defaultDevice);
+
public static native void save();
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/ControlReference.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/ControlReference.java
index fd01cc1705..66fd9fa02d 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/ControlReference.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/ControlReference.java
@@ -34,4 +34,6 @@ public class ControlReference
*/
@Nullable
public native String setExpression(String expr);
+
+ public native boolean isInput();
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputMappingControlSetting.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputMappingControlSetting.java
index a858b35099..1e8bb1e6d3 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputMappingControlSetting.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputMappingControlSetting.java
@@ -52,4 +52,14 @@ public final class InputMappingControlSetting extends SettingsItem
{
return mController;
}
+
+ public ControlReference getControlReference()
+ {
+ return mControlReference;
+ }
+
+ public boolean isInput()
+ {
+ return mControlReference.isInput();
+ }
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.java
new file mode 100644
index 0000000000..0ad589fe40
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.java
@@ -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();
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.java
new file mode 100644
index 0000000000..2ce925235b
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.java
@@ -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);
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.java
new file mode 100644
index 0000000000..de8e8265a6
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.java
@@ -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;
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/viewholder/InputMappingControlSettingViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/viewholder/InputMappingControlSettingViewHolder.java
index 72ddcb3fd3..fd4b92cf95 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/viewholder/InputMappingControlSettingViewHolder.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/viewholder/InputMappingControlSettingViewHolder.java
@@ -7,7 +7,7 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import org.dolphinemu.dolphinemu.databinding.ListItemSettingBinding;
+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;
@@ -17,9 +17,9 @@ public final class InputMappingControlSettingViewHolder extends SettingViewHolde
{
private InputMappingControlSetting mItem;
- private final ListItemSettingBinding mBinding;
+ private final ListItemMappingBinding mBinding;
- public InputMappingControlSettingViewHolder(@NonNull ListItemSettingBinding binding,
+ public InputMappingControlSettingViewHolder(@NonNull ListItemMappingBinding binding,
SettingsAdapter adapter)
{
super(binding.getRoot(), adapter);
@@ -33,6 +33,7 @@ public final class InputMappingControlSettingViewHolder extends SettingViewHolde
mBinding.textSettingName.setText(mItem.getName());
mBinding.textSettingDescription.setText(mItem.getValue());
+ mBinding.buttonAdvancedSettings.setOnClickListener(this::onLongClick);
setStyle(mBinding.textSettingName, mItem);
}
@@ -46,11 +47,28 @@ public final class InputMappingControlSettingViewHolder extends SettingViewHolde
return;
}
- getAdapter().onInputMappingClick(mItem, getBindingAdapterPosition());
+ 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()
{
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java
index 0a965b953e..ca405b15fd 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java
@@ -31,12 +31,15 @@ import com.google.android.material.timepicker.MaterialTimePicker;
import com.google.android.material.timepicker.TimeFormat;
import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.databinding.DialogAdvancedMappingBinding;
import org.dolphinemu.dolphinemu.databinding.DialogInputStringBinding;
import org.dolphinemu.dolphinemu.databinding.DialogSliderBinding;
import org.dolphinemu.dolphinemu.databinding.ListItemHeaderBinding;
+import org.dolphinemu.dolphinemu.databinding.ListItemMappingBinding;
import org.dolphinemu.dolphinemu.databinding.ListItemSettingBinding;
import org.dolphinemu.dolphinemu.databinding.ListItemSettingSwitchBinding;
import org.dolphinemu.dolphinemu.databinding.ListItemSubmenuBinding;
+import org.dolphinemu.dolphinemu.features.input.ui.AdvancedMappingDialog;
import org.dolphinemu.dolphinemu.features.input.ui.MotionAlertDialog;
import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting;
import org.dolphinemu.dolphinemu.features.input.ui.viewholder.InputMappingControlSettingViewHolder;
@@ -123,7 +126,7 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
return new SubmenuViewHolder(ListItemSubmenuBinding.inflate(inflater), this);
case SettingsItem.TYPE_INPUT_MAPPING_CONTROL:
- return new InputMappingControlSettingViewHolder(ListItemSettingBinding.inflate(inflater),
+ return new InputMappingControlSettingViewHolder(ListItemMappingBinding.inflate(inflater),
this);
case SettingsItem.TYPE_FILE_PICKER:
@@ -359,6 +362,44 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
dialog.show();
}
+ public void onAdvancedInputMappingClick(final InputMappingControlSetting item, final int position)
+ {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+
+ DialogAdvancedMappingBinding binding = DialogAdvancedMappingBinding.inflate(inflater);
+
+ final AdvancedMappingDialog dialog = new AdvancedMappingDialog(mContext, binding,
+ item.getControlReference(), item.getController());
+
+ Drawable background = ContextCompat.getDrawable(mContext, R.drawable.dialog_round);
+ @ColorInt int color = new ElevationOverlayProvider(dialog.getContext()).compositeOverlay(
+ MaterialColors.getColor(dialog.getWindow().getDecorView(), R.attr.colorSurface),
+ dialog.getWindow().getDecorView().getElevation());
+ background.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
+ dialog.getWindow().setBackgroundDrawable(background);
+
+ dialog.setTitle(item.isInput() ?
+ R.string.input_configure_input : R.string.input_configure_output);
+ dialog.setView(binding.getRoot());
+ dialog.setButton(AlertDialog.BUTTON_POSITIVE, mContext.getString(R.string.ok),
+ (dialogInterface, i) ->
+ {
+ item.setValue(dialog.getExpression());
+ notifyItemChanged(position);
+ mView.onSettingChanged();
+ });
+ dialog.setButton(AlertDialog.BUTTON_NEGATIVE, mContext.getString(R.string.cancel), this);
+ dialog.setButton(AlertDialog.BUTTON_NEUTRAL, mContext.getString(R.string.clear),
+ (dialogInterface, i) ->
+ {
+ item.clearValue();
+ notifyItemChanged(position);
+ mView.onSettingChanged();
+ });
+ dialog.setCanceledOnTouchOutside(false);
+ dialog.show();
+ }
+
public void onFilePickerDirectoryClick(SettingsItem item, int position)
{
mClickedItem = item;
diff --git a/Source/Android/app/src/main/res/drawable/ic_more.xml b/Source/Android/app/src/main/res/drawable/ic_more.xml
new file mode 100644
index 0000000000..429471329e
--- /dev/null
+++ b/Source/Android/app/src/main/res/drawable/ic_more.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
+</vector>
diff --git a/Source/Android/app/src/main/res/layout-ldrtl/list_item_mapping.xml b/Source/Android/app/src/main/res/layout-ldrtl/list_item_mapping.xml
new file mode 100644
index 0000000000..709a7cbcf0
--- /dev/null
+++ b/Source/Android/app/src/main/res/layout-ldrtl/list_item_mapping.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout android:id="@+id/root"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:focusable="true"
+ android:nextFocusLeft="@id/button_advanced_settings">
+
+ <TextView
+ android:id="@+id/text_setting_name"
+ style="@style/TextAppearance.MaterialComponents.Headline5"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginEnd="@dimen/spacing_large"
+ android:layout_marginStart="@dimen/spacing_large"
+ android:layout_marginTop="@dimen/spacing_large"
+ android:textSize="16sp"
+ android:textAlignment="viewStart"
+ android:layout_toStartOf="@+id/button_more_settings"
+ tools:text="Setting Name" />
+
+ <TextView
+ android:id="@+id/text_setting_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignStart="@+id/text_setting_name"
+ android:layout_below="@+id/text_setting_name"
+ android:layout_marginBottom="@dimen/spacing_large"
+ android:layout_marginEnd="@dimen/spacing_large"
+ android:layout_marginStart="@dimen/spacing_large"
+ android:layout_marginTop="@dimen/spacing_small"
+ android:layout_toStartOf="@+id/button_advanced_settings"
+ android:textAlignment="viewStart"
+ tools:text="@string/overclock_enable_description" />
+
+ <Button
+ android:id="@+id/button_advanced_settings"
+ style="?attr/materialIconButtonStyle"
+ android:contentDescription="@string/advanced_settings"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:layout_marginEnd="@dimen/spacing_small"
+ android:nextFocusRight="@id/root"
+ app:icon="@drawable/ic_more"
+ app:iconTint="?attr/colorOnPrimaryContainer" />
+
+</RelativeLayout>
diff --git a/Source/Android/app/src/main/res/layout/dialog_advanced_mapping.xml b/Source/Android/app/src/main/res/layout/dialog_advanced_mapping.xml
new file mode 100644
index 0000000000..446851e8fa
--- /dev/null
+++ b/Source/Android/app/src/main/res/layout/dialog_advanced_mapping.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingHorizontal="@dimen/spacing_large"
+ android:paddingTop="@dimen/spacing_medlarge">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/list_control"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/expression"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentStart="true"
+ android:layout_below="@+id/device" />
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/device"
+ style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:hint="@string/input_device">
+
+ <com.google.android.material.textfield.MaterialAutoCompleteTextView
+ android:id="@+id/dropdown_device"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="none" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/expression"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentStart="true"
+ android:hint="@string/input_expression">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/edit_expression"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="start"
+ android:importantForAutofill="no"
+ android:typeface="monospace" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+</RelativeLayout>
diff --git a/Source/Android/app/src/main/res/layout/list_item_advanced_mapping_control.xml b/Source/Android/app/src/main/res/layout/list_item_advanced_mapping_control.xml
new file mode 100644
index 0000000000..b5ea979a09
--- /dev/null
+++ b/Source/Android/app/src/main/res/layout/list_item_advanced_mapping_control.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="54dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:focusable="true"
+ android:clickable="true">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/TextAppearance.MaterialComponents.Headline5"
+ tools:text="Button A"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginStart="@dimen/spacing_large"
+ android:layout_marginEnd="@dimen/spacing_large"
+ android:layout_marginTop="@dimen/spacing_large"
+ android:id="@+id/text_name"
+ android:textAlignment="viewStart"
+ android:textSize="16sp" />
+
+</RelativeLayout>
diff --git a/Source/Android/app/src/main/res/layout/list_item_mapping.xml b/Source/Android/app/src/main/res/layout/list_item_mapping.xml
new file mode 100644
index 0000000000..de504d3187
--- /dev/null
+++ b/Source/Android/app/src/main/res/layout/list_item_mapping.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout android:id="@+id/root"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:focusable="true"
+ android:nextFocusRight="@id/button_advanced_settings">
+
+ <TextView
+ android:id="@+id/text_setting_name"
+ style="@style/TextAppearance.MaterialComponents.Headline5"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginEnd="@dimen/spacing_large"
+ android:layout_marginStart="@dimen/spacing_large"
+ android:layout_marginTop="@dimen/spacing_large"
+ android:textSize="16sp"
+ android:textAlignment="viewStart"
+ android:layout_toStartOf="@+id/button_advanced_settings"
+ tools:text="Setting Name" />
+
+ <TextView
+ android:id="@+id/text_setting_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignStart="@+id/text_setting_name"
+ android:layout_below="@+id/text_setting_name"
+ android:layout_marginBottom="@dimen/spacing_large"
+ android:layout_marginEnd="@dimen/spacing_large"
+ android:layout_marginStart="@dimen/spacing_large"
+ android:layout_marginTop="@dimen/spacing_small"
+ android:layout_toStartOf="@+id/button_advanced_settings"
+ android:textAlignment="viewStart"
+ tools:text="@string/overclock_enable_description" />
+
+ <Button
+ android:id="@+id/button_advanced_settings"
+ style="?attr/materialIconButtonStyle"
+ android:contentDescription="@string/advanced_settings"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:layout_marginEnd="@dimen/spacing_small"
+ android:nextFocusLeft="@id/root"
+ app:icon="@drawable/ic_more"
+ app:iconTint="?attr/colorOnPrimaryContainer" />
+
+</RelativeLayout>
diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml
index 8d1a550962..f7036a0f94 100644
--- a/Source/Android/app/src/main/res/values/strings.xml
+++ b/Source/Android/app/src/main/res/values/strings.xml
@@ -50,6 +50,9 @@
<string name="input_binding">Input Binding</string>
<string name="input_binding_description">Press or move an input to bind it to %1$s.</string>
<string name="input_binding_no_device">You need to select a device first!</string>
+ <string name="input_configure_input">Configure Input</string>
+ <string name="input_configure_output">Configure Output</string>
+ <string name="input_expression">Expression</string>
<!-- Main Preference Fragment -->
<string name="settings">Settings</string>
@@ -420,6 +423,7 @@
<string name="other">Other</string>
<string name="continue_anyway">Continue Anyway</string>
<string name="more_settings">More Settings</string>
+ <string name="advanced_settings">Advanced Settings</string>
<!-- Game Grid Screen-->
<string name="platform_gamecube">GameCube Games</string>
diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp
index 6e547beaed..25a051ff2d 100644
--- a/Source/Android/jni/AndroidCommon/IDCache.cpp
+++ b/Source/Android/jni/AndroidCommon/IDCache.cpp
@@ -103,6 +103,14 @@ static jclass s_emulated_controller_class;
static jfieldID s_emulated_controller_pointer;
static jmethodID s_emulated_controller_constructor;
+static jclass s_core_device_class;
+static jfieldID s_core_device_pointer;
+static jmethodID s_core_device_constructor;
+
+static jclass s_core_device_control_class;
+static jfieldID s_core_device_control_pointer;
+static jmethodID s_core_device_control_constructor;
+
namespace IDCache
{
JNIEnv* GetEnvForThread()
@@ -478,6 +486,36 @@ jmethodID GetNumericSettingConstructor()
return s_numeric_setting_constructor;
}
+jclass GetCoreDeviceClass()
+{
+ return s_core_device_class;
+}
+
+jfieldID GetCoreDevicePointer()
+{
+ return s_core_device_pointer;
+}
+
+jmethodID GetCoreDeviceConstructor()
+{
+ return s_core_device_constructor;
+}
+
+jclass GetCoreDeviceControlClass()
+{
+ return s_core_device_control_class;
+}
+
+jfieldID GetCoreDeviceControlPointer()
+{
+ return s_core_device_control_pointer;
+}
+
+jmethodID GetCoreDeviceControlConstructor()
+{
+ return s_core_device_control_constructor;
+}
+
} // namespace IDCache
extern "C" {
@@ -672,6 +710,23 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
s_numeric_setting_constructor = env->GetMethodID(numeric_setting_class, "<init>", "(J)V");
env->DeleteLocalRef(numeric_setting_class);
+ const jclass core_device_class =
+ env->FindClass("org/dolphinemu/dolphinemu/features/input/model/CoreDevice");
+ s_core_device_class = reinterpret_cast<jclass>(env->NewGlobalRef(core_device_class));
+ s_core_device_pointer = env->GetFieldID(core_device_class, "mPointer", "J");
+ s_core_device_constructor = env->GetMethodID(core_device_class, "<init>", "(J)V");
+ env->DeleteLocalRef(core_device_class);
+
+ const jclass core_device_control_class =
+ env->FindClass("org/dolphinemu/dolphinemu/features/input/model/CoreDevice$Control");
+ s_core_device_control_class =
+ reinterpret_cast<jclass>(env->NewGlobalRef(core_device_control_class));
+ s_core_device_control_pointer = env->GetFieldID(core_device_control_class, "mPointer", "J");
+ s_core_device_control_constructor =
+ env->GetMethodID(core_device_control_class, "<init>",
+ "(Lorg/dolphinemu/dolphinemu/features/input/model/CoreDevice;J)V");
+ env->DeleteLocalRef(core_device_control_class);
+
return JNI_VERSION;
}
@@ -704,5 +759,7 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved)
env->DeleteGlobalRef(s_control_reference_class);
env->DeleteGlobalRef(s_emulated_controller_class);
env->DeleteGlobalRef(s_numeric_setting_class);
+ env->DeleteGlobalRef(s_core_device_class);
+ env->DeleteGlobalRef(s_core_device_control_class);
}
}
diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h
index 2a6462c380..967703b9b0 100644
--- a/Source/Android/jni/AndroidCommon/IDCache.h
+++ b/Source/Android/jni/AndroidCommon/IDCache.h
@@ -102,4 +102,12 @@ jclass GetNumericSettingClass();
jfieldID GetNumericSettingPointer();
jmethodID GetNumericSettingConstructor();
+jclass GetCoreDeviceClass();
+jfieldID GetCoreDevicePointer();
+jmethodID GetCoreDeviceConstructor();
+
+jclass GetCoreDeviceControlClass();
+jfieldID GetCoreDeviceControlPointer();
+jmethodID GetCoreDeviceControlConstructor();
+
} // namespace IDCache
diff --git a/Source/Android/jni/CMakeLists.txt b/Source/Android/jni/CMakeLists.txt
index 0b0d50a9ec..f2e7dccb27 100644
--- a/Source/Android/jni/CMakeLists.txt
+++ b/Source/Android/jni/CMakeLists.txt
@@ -16,6 +16,8 @@ add_library(main SHARED
Input/ControlGroup.h
Input/ControlReference.cpp
Input/ControlReference.h
+ Input/CoreDevice.cpp
+ Input/CoreDevice.h
Input/EmulatedController.cpp
Input/EmulatedController.h
Input/InputOverrider.cpp
diff --git a/Source/Android/jni/Input/ControlReference.cpp b/Source/Android/jni/Input/ControlReference.cpp
index 8bf9351109..533c8cb1a0 100644
--- a/Source/Android/jni/Input/ControlReference.cpp
+++ b/Source/Android/jni/Input/ControlReference.cpp
@@ -51,4 +51,11 @@ Java_org_dolphinemu_dolphinemu_features_input_model_controlleremu_ControlReferen
ControlReferenceFromJava(env, obj)->SetExpression(GetJString(env, expr));
return result ? ToJString(env, *result) : nullptr;
}
+
+JNIEXPORT jboolean JNICALL
+Java_org_dolphinemu_dolphinemu_features_input_model_controlleremu_ControlReference_isInput(
+ JNIEnv* env, jobject obj)
+{
+ return static_cast<jboolean>(ControlReferenceFromJava(env, obj)->IsInput());
+}
}
diff --git a/Source/Android/jni/Input/CoreDevice.cpp b/Source/Android/jni/Input/CoreDevice.cpp
new file mode 100644
index 0000000000..4fd4758ebb
--- /dev/null
+++ b/Source/Android/jni/Input/CoreDevice.cpp
@@ -0,0 +1,84 @@
+// Copyright 2022 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "jni/Input/CoreDevice.h"
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include <jni.h>
+
+#include "InputCommon/ControllerInterface/CoreDevice.h"
+#include "jni/AndroidCommon/AndroidCommon.h"
+#include "jni/AndroidCommon/IDCache.h"
+
+static ciface::Core::Device::Control* GetControlPointer(JNIEnv* env, jobject obj)
+{
+ return reinterpret_cast<ciface::Core::Device::Control*>(
+ env->GetLongField(obj, IDCache::GetCoreDeviceControlPointer()));
+}
+
+static jobject CoreDeviceControlToJava(JNIEnv* env, jobject device,
+ ciface::Core::Device::Control* control)
+{
+ if (!control)
+ return nullptr;
+
+ return env->NewObject(IDCache::GetCoreDeviceControlClass(),
+ IDCache::GetCoreDeviceControlConstructor(), device,
+ reinterpret_cast<jlong>(control));
+}
+
+template <typename T>
+static jobjectArray CoreDeviceControlVectorToJava(JNIEnv* env, jobject device,
+ const std::vector<T*>& controls)
+{
+ return VectorToJObjectArray(
+ env, controls, IDCache::GetCoreDeviceControlClass(),
+ [device](JNIEnv* env, T* control) { return CoreDeviceControlToJava(env, device, control); });
+}
+
+static std::shared_ptr<ciface::Core::Device>* GetDevicePointer(JNIEnv* env, jobject obj)
+{
+ return reinterpret_cast<std::shared_ptr<ciface::Core::Device>*>(
+ env->GetLongField(obj, IDCache::GetCoreDevicePointer()));
+}
+
+jobject CoreDeviceToJava(JNIEnv* env, std::shared_ptr<ciface::Core::Device> device)
+{
+ if (!device)
+ return nullptr;
+
+ return env->NewObject(
+ IDCache::GetCoreDeviceClass(), IDCache::GetCoreDeviceConstructor(),
+ reinterpret_cast<jlong>(new std::shared_ptr<ciface::Core::Device>(std::move(device))));
+}
+
+extern "C" {
+
+JNIEXPORT jstring JNICALL
+Java_org_dolphinemu_dolphinemu_features_input_model_CoreDevice_00024Control_getName(JNIEnv* env,
+ jobject obj)
+{
+ return ToJString(env, GetControlPointer(env, obj)->GetName());
+}
+
+JNIEXPORT void JNICALL
+Java_org_dolphinemu_dolphinemu_features_input_model_CoreDevice_finalize(JNIEnv* env, jobject obj)
+{
+ delete GetDevicePointer(env, obj);
+}
+
+JNIEXPORT jobjectArray JNICALL
+Java_org_dolphinemu_dolphinemu_features_input_model_CoreDevice_getInputs(JNIEnv* env, jobject obj)
+{
+ return CoreDeviceControlVectorToJava(env, obj, (*GetDevicePointer(env, obj))->Inputs());
+}
+
+JNIEXPORT jobjectArray JNICALL
+Java_org_dolphinemu_dolphinemu_features_input_model_CoreDevice_getOutputs(JNIEnv* env, jobject obj)
+{
+ return CoreDeviceControlVectorToJava(env, obj, (*GetDevicePointer(env, obj))->Outputs());
+}
+}
diff --git a/Source/Android/jni/Input/CoreDevice.h b/Source/Android/jni/Input/CoreDevice.h
new file mode 100644
index 0000000000..ec098916af
--- /dev/null
+++ b/Source/Android/jni/Input/CoreDevice.h
@@ -0,0 +1,15 @@
+// Copyright 2022 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <memory>
+
+#include <jni.h>
+
+namespace ciface::Core
+{
+class Device;
+}
+
+jobject CoreDeviceToJava(JNIEnv* env, std::shared_ptr<ciface::Core::Device> device);
diff --git a/Source/Android/jni/Input/MappingCommon.cpp b/Source/Android/jni/Input/MappingCommon.cpp
index 4e5d88ccf6..1cc55df2ae 100644
--- a/Source/Android/jni/Input/MappingCommon.cpp
+++ b/Source/Android/jni/Input/MappingCommon.cpp
@@ -53,6 +53,19 @@ Java_org_dolphinemu_dolphinemu_features_input_model_MappingCommon_detectInput(
ciface::MappingCommon::Quote::On));
}
+JNIEXPORT jstring JNICALL
+Java_org_dolphinemu_dolphinemu_features_input_model_MappingCommon_getExpressionForControl(
+ JNIEnv* env, jclass, jstring j_control, jstring j_device, jstring j_default_device)
+{
+ ciface::Core::DeviceQualifier device_qualifier, default_device_qualifier;
+ device_qualifier.FromString(GetJString(env, j_device));
+ default_device_qualifier.FromString(GetJString(env, j_default_device));
+
+ return ToJString(env, ciface::MappingCommon::GetExpressionForControl(GetJString(env, j_control),
+ device_qualifier,
+ default_device_qualifier));
+}
+
JNIEXPORT void JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_MappingCommon_save(JNIEnv* env, jclass)
{