diff options
| author | JosJuice <josjuice@gmail.com> | 2022-12-27 16:29:01 +0100 |
|---|---|---|
| committer | JosJuice <josjuice@gmail.com> | 2023-03-07 17:39:30 +0100 |
| commit | c2779aef060137d9062cfd29babb3c43fc5fa2b5 (patch) | |
| tree | 421bbf1ba198ec00b5e7dfe9118da77e079d89c1 /Source/Android | |
| parent | 42943672bbbed6c18aa2bc85c41102a0616547f7 (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')
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) { |
