summaryrefslogtreecommitdiff
path: root/Source/Android/app/src/main/java
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/app/src/main/java
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/app/src/main/java')
-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
10 files changed, 370 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;