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>
This commit is contained in:
JosJuice
2022-12-27 16:29:01 +01:00
parent 42943672bb
commit c2779aef06
24 changed files with 772 additions and 5 deletions

View File

@ -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()
{

View File

@ -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();
}

View File

@ -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();
}

View File

@ -34,4 +34,6 @@ public class ControlReference
*/
@Nullable
public native String setExpression(String expr);
public native boolean isInput();
}

View File

@ -52,4 +52,14 @@ public final class InputMappingControlSetting extends SettingsItem
{
return mController;
}
public ControlReference getControlReference()
{
return mControlReference;
}
public boolean isInput()
{
return mControlReference.isInput();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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()
{

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>