From c2779aef060137d9062cfd29babb3c43fc5fa2b5 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Tue, 27 Dec 2022 16:29:01 +0100 Subject: [PATCH] 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 --- .../input/model/ControllerInterface.java | 3 + .../features/input/model/CoreDevice.java | 48 ++++++ .../features/input/model/MappingCommon.java | 3 + .../model/controlleremu/ControlReference.java | 2 + .../view/InputMappingControlSetting.java | 10 ++ .../ui/AdvancedMappingControlAdapter.java | 55 +++++++ .../ui/AdvancedMappingControlViewHolder.java | 34 ++++ .../input/ui/AdvancedMappingDialog.java | 151 ++++++++++++++++++ .../InputMappingControlSettingViewHolder.java | 26 ++- .../features/settings/ui/SettingsAdapter.java | 43 ++++- .../app/src/main/res/drawable/ic_more.xml | 9 ++ .../res/layout-ldrtl/list_item_mapping.xml | 56 +++++++ .../res/layout/dialog_advanced_mapping.xml | 54 +++++++ .../list_item_advanced_mapping_control.xml | 27 ++++ .../src/main/res/layout/list_item_mapping.xml | 56 +++++++ .../app/src/main/res/values/strings.xml | 4 + Source/Android/jni/AndroidCommon/IDCache.cpp | 57 +++++++ Source/Android/jni/AndroidCommon/IDCache.h | 8 + Source/Android/jni/CMakeLists.txt | 2 + Source/Android/jni/Input/ControlReference.cpp | 7 + Source/Android/jni/Input/CoreDevice.cpp | 84 ++++++++++ Source/Android/jni/Input/CoreDevice.h | 15 ++ Source/Android/jni/Input/MappingCommon.cpp | 13 ++ .../ControllerInterface/Android/Android.cpp | 10 ++ 24 files changed, 772 insertions(+), 5 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/CoreDevice.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.java create mode 100644 Source/Android/app/src/main/res/drawable/ic_more.xml create mode 100644 Source/Android/app/src/main/res/layout-ldrtl/list_item_mapping.xml create mode 100644 Source/Android/app/src/main/res/layout/dialog_advanced_mapping.xml create mode 100644 Source/Android/app/src/main/res/layout/list_item_advanced_mapping_control.xml create mode 100644 Source/Android/app/src/main/res/layout/list_item_mapping.xml create mode 100644 Source/Android/jni/Input/CoreDevice.cpp create mode 100644 Source/Android/jni/Input/CoreDevice.h 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 +{ + private final Consumer mOnClickCallback; + + private String[] mControls = new String[0]; + + public AdvancedMappingControlAdapter(Consumer 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 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 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 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 + { + 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 @@ + + + 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 @@ + + + + + + + +