Android: Add input profile management

Co-authored-by: Charles Lombardo <clombardo169@gmail.com>
This commit is contained in:
JosJuice 2022-12-26 19:26:58 +01:00
parent 7ef229d908
commit 1eeded23df
18 changed files with 612 additions and 6 deletions

View File

@ -35,6 +35,10 @@ public class EmulatedController
public native void clearSettings();
public native void loadProfile(String path);
public native void saveProfile(String path);
public static native EmulatedController getGcPad(int controllerIndex);
public static native EmulatedController getWiimote(int controllerIndex);

View File

@ -0,0 +1,65 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.input.ui;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.dolphinemu.dolphinemu.databinding.ListItemProfileBinding;
public final class ProfileAdapter extends RecyclerView.Adapter<ProfileViewHolder>
{
private final Context mContext;
private final ProfileDialogPresenter mPresenter;
private final String[] mStockProfileNames;
private final String[] mUserProfileNames;
public ProfileAdapter(Context context, ProfileDialogPresenter presenter)
{
mContext = context;
mPresenter = presenter;
mStockProfileNames = presenter.getProfileNames(true);
mUserProfileNames = presenter.getProfileNames(false);
}
@NonNull @Override
public ProfileViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType)
{
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
ListItemProfileBinding binding = ListItemProfileBinding.inflate(inflater, parent, false);
return new ProfileViewHolder(mPresenter, binding);
}
@Override
public void onBindViewHolder(@NonNull ProfileViewHolder holder, int position)
{
if (position < mStockProfileNames.length)
{
holder.bind(mStockProfileNames[position], true);
return;
}
position -= mStockProfileNames.length;
if (position < mUserProfileNames.length)
{
holder.bind(mUserProfileNames[position], false);
return;
}
holder.bindAsEmpty(mContext);
}
@Override
public int getItemCount()
{
return mStockProfileNames.length + mUserProfileNames.length + 1;
}
}

View File

@ -0,0 +1,76 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.input.ui
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.divider.MaterialDividerItemDecoration
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.DialogInputProfilesBinding
import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag
class ProfileDialog : BottomSheetDialogFragment() {
private var presenter: ProfileDialogPresenter? = null
private var _binding: DialogInputProfilesBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) {
val menuTag: MenuTag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requireArguments().getSerializable(KEY_MENU_TAG, MenuTag::class.java) as MenuTag
} else {
requireArguments().getSerializable(KEY_MENU_TAG) as MenuTag
}
presenter = ProfileDialogPresenter(this, menuTag)
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = DialogInputProfilesBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.profileList.adapter = ProfileAdapter(context, presenter)
binding.profileList.layoutManager = LinearLayoutManager(context)
val divider = MaterialDividerItemDecoration(requireActivity(), LinearLayoutManager.VERTICAL)
divider.isLastItemDecorated = false
binding.profileList.addItemDecoration(divider)
// You can't expand a bottom sheet with a controller/remote/other non-touch devices
val behavior: BottomSheetBehavior<View> = BottomSheetBehavior.from(view.parent as View)
if (!resources.getBoolean(R.bool.hasTouch)) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val KEY_MENU_TAG = "menu_tag"
@JvmStatic
fun create(menuTag: MenuTag): ProfileDialog {
val dialog = ProfileDialog()
val args = Bundle()
args.putSerializable(KEY_MENU_TAG, menuTag)
dialog.arguments = args
return dialog
}
}
}

View File

@ -0,0 +1,150 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.input.ui;
import android.content.Context;
import android.view.LayoutInflater;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputEditText;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.databinding.DialogInputStringBinding;
import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag;
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivityView;
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization;
import java.io.File;
import java.text.Collator;
import java.util.Arrays;
public final class ProfileDialogPresenter
{
private static final String EXTENSION = ".ini";
private final Context mContext;
private final DialogFragment mDialog;
private final MenuTag mMenuTag;
public ProfileDialogPresenter(DialogFragment dialog, MenuTag menuTag)
{
mContext = dialog.getContext();
mDialog = dialog;
mMenuTag = menuTag;
}
public String[] getProfileNames(boolean stock)
{
File[] profiles = new File(getProfileDirectoryPath(stock)).listFiles(
file -> !file.isDirectory() && file.getName().endsWith(EXTENSION));
if (profiles == null)
return new String[0];
return Arrays.stream(profiles)
.map(file -> file.getName().substring(0, file.getName().length() - EXTENSION.length()))
.sorted(Collator.getInstance())
.toArray(String[]::new);
}
public void loadProfile(@NonNull String profileName, boolean stock)
{
new MaterialAlertDialogBuilder(mContext)
.setMessage(mContext.getString(R.string.input_profile_confirm_load, profileName))
.setPositiveButton(R.string.yes, (dialogInterface, i) ->
{
mMenuTag.getCorrespondingEmulatedController()
.loadProfile(getProfilePath(profileName, stock));
((SettingsActivityView) mDialog.requireActivity()).onControllerSettingsChanged();
mDialog.dismiss();
})
.setNegativeButton(R.string.no, null)
.show();
}
public void saveProfile(@NonNull String profileName)
{
// If the user is saving over an existing profile, we should show an overwrite warning.
// If the user is creating a new profile, we normally shouldn't show a warning,
// but if they've entered the name of an existing profile, we should shown an overwrite warning.
String profilePath = getProfilePath(profileName, false);
if (!new File(profilePath).exists())
{
mMenuTag.getCorrespondingEmulatedController().saveProfile(profilePath);
mDialog.dismiss();
}
else
{
new MaterialAlertDialogBuilder(mContext)
.setMessage(mContext.getString(R.string.input_profile_confirm_save, profileName))
.setPositiveButton(R.string.yes, (dialogInterface, i) ->
{
mMenuTag.getCorrespondingEmulatedController().saveProfile(profilePath);
mDialog.dismiss();
})
.setNegativeButton(R.string.no, null)
.show();
}
}
public void saveProfileAndPromptForName()
{
LayoutInflater inflater = LayoutInflater.from(mContext);
DialogInputStringBinding binding = DialogInputStringBinding.inflate(inflater);
TextInputEditText input = binding.input;
new MaterialAlertDialogBuilder(mContext)
.setView(binding.getRoot())
.setPositiveButton(R.string.ok, (dialogInterface, i) ->
saveProfile(input.getText().toString()))
.setNegativeButton(R.string.cancel, null)
.show();
}
public void deleteProfile(@NonNull String profileName)
{
new MaterialAlertDialogBuilder(mContext)
.setMessage(mContext.getString(R.string.input_profile_confirm_delete, profileName))
.setPositiveButton(R.string.yes, (dialogInterface, i) ->
{
new File(getProfilePath(profileName, false)).delete();
mDialog.dismiss();
})
.setNegativeButton(R.string.no, null)
.show();
}
private String getProfileDirectoryName()
{
if (mMenuTag.isGCPadMenu())
return "GCPad";
else if (mMenuTag.isWiimoteMenu())
return "Wiimote";
else
throw new UnsupportedOperationException();
}
private String getProfileDirectoryPath(boolean stock)
{
if (stock)
{
return DirectoryInitialization.getSysDirectory() + "/Profiles/" + getProfileDirectoryName() +
'/';
}
else
{
return DirectoryInitialization.getUserDirectory() + "/Config/Profiles/" +
getProfileDirectoryName() + '/';
}
}
private String getProfilePath(String profileName, boolean stock)
{
return getProfileDirectoryPath(stock) + profileName + EXTENSION;
}
}

View File

@ -0,0 +1,76 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.input.ui;
import android.content.Context;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.databinding.ListItemProfileBinding;
public class ProfileViewHolder extends RecyclerView.ViewHolder
{
private final ProfileDialogPresenter mPresenter;
private final ListItemProfileBinding mBinding;
private String mProfileName;
private boolean mStock;
public ProfileViewHolder(@NonNull ProfileDialogPresenter presenter,
@NonNull ListItemProfileBinding binding)
{
super(binding.getRoot());
mPresenter = presenter;
mBinding = binding;
binding.buttonLoad.setOnClickListener(view -> loadProfile());
binding.buttonSave.setOnClickListener(view -> saveProfile());
binding.buttonDelete.setOnClickListener(view -> deleteProfile());
}
public void bind(String profileName, boolean stock)
{
mProfileName = profileName;
mStock = stock;
mBinding.textName.setText(profileName);
mBinding.buttonLoad.setVisibility(View.VISIBLE);
mBinding.buttonSave.setVisibility(stock ? View.GONE : View.VISIBLE);
mBinding.buttonDelete.setVisibility(stock ? View.GONE : View.VISIBLE);
}
public void bindAsEmpty(Context context)
{
mProfileName = null;
mStock = false;
mBinding.textName.setText(context.getText(R.string.input_profile_new));
mBinding.buttonLoad.setVisibility(View.GONE);
mBinding.buttonSave.setVisibility(View.VISIBLE);
mBinding.buttonDelete.setVisibility(View.GONE);
}
private void loadProfile()
{
mPresenter.loadProfile(mProfileName, mStock);
}
private void saveProfile()
{
if (mProfileName == null)
mPresenter.saveProfileAndPromptForName();
else
mPresenter.saveProfile(mProfileName);
}
private void deleteProfile()
{
mPresenter.deleteProfile(mProfileName);
}
}

View File

@ -4,6 +4,8 @@ package org.dolphinemu.dolphinemu.features.settings.ui;
import androidx.annotation.NonNull;
import org.dolphinemu.dolphinemu.features.input.model.controlleremu.EmulatedController;
public enum MenuTag
{
SETTINGS("settings"),
@ -88,6 +90,16 @@ public enum MenuTag
return subType;
}
public EmulatedController getCorrespondingEmulatedController()
{
if (isGCPadMenu())
return EmulatedController.getGcPad(getSubType());
else if (isWiimoteMenu())
return EmulatedController.getWiimote(getSubType());
else
throw new UnsupportedOperationException();
}
public boolean isSerialPort1Menu()
{
return this == CONFIG_SERIALPORT1;
@ -143,7 +155,8 @@ public enum MenuTag
{
for (MenuTag menuTag : MenuTag.values())
{
if (menuTag.tag.equals(tag) && menuTag.subType == subtype) return menuTag;
if (menuTag.tag.equals(tag) && menuTag.subType == subtype)
return menuTag;
}
throw new IllegalArgumentException("You are asking for a menu that is not available or " +

View File

@ -19,6 +19,7 @@ import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.ViewModelProvider;
@ -44,6 +45,7 @@ public final class SettingsActivity extends AppCompatActivity implements Setting
private static final String ARG_IS_WII = "is_wii";
private static final String KEY_MAPPING_ALL_DEVICES = "all_devices";
private static final String FRAGMENT_TAG = "settings";
private static final String FRAGMENT_DIALOG_TAG = "settings_dialog";
private SettingsActivityPresenter mPresenter;
private AlertDialog dialog;
@ -191,6 +193,12 @@ public final class SettingsActivity extends AppCompatActivity implements Setting
transaction.commit();
}
@Override
public void showDialogFragment(DialogFragment fragment)
{
fragment.show(getSupportFragmentManager(), FRAGMENT_DIALOG_TAG);
}
private boolean areSystemAnimationsEnabled()
{
float duration = Settings.Global.getFloat(
@ -314,6 +322,12 @@ public final class SettingsActivity extends AppCompatActivity implements Setting
mPresenter.onSettingChanged();
}
@Override
public void onControllerSettingsChanged()
{
getFragment().onControllerSettingsChanged();
}
@Override
public void onMenuTagAction(@NonNull MenuTag menuTag, int value)
{

View File

@ -5,6 +5,7 @@ package org.dolphinemu.dolphinemu.features.settings.ui;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import org.dolphinemu.dolphinemu.features.settings.model.Settings;
@ -21,6 +22,13 @@ public interface SettingsActivityView
*/
void showSettingsFragment(MenuTag menuTag, Bundle extras, boolean addToStack, String gameId);
/**
* Shows a DialogFragment.
*
* Only one can be shown at a time.
*/
void showDialogFragment(DialogFragment fragment);
/**
* Called by a contained Fragment to get access to the Setting HashMap
* loaded from disk, so that each Fragment doesn't need to perform its own
@ -60,6 +68,14 @@ public interface SettingsActivityView
*/
void onSettingChanged();
/**
* Refetches the values of all controller settings.
*
* To be used when loading an input profile or performing some other action that changes all
* controller settings at once.
*/
void onControllerSettingsChanged();
/**
* Called by a containing Fragment to tell the containing Activity that the user wants to open the
* MenuTag associated with a setting.

View File

@ -13,6 +13,7 @@ import androidx.annotation.Nullable;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@ -221,6 +222,12 @@ public final class SettingsFragment extends Fragment implements SettingsFragment
mActivity.showSettingsFragment(menuKey, null, true, getArguments().getString(ARGUMENT_GAME_ID));
}
@Override
public void showDialogFragment(DialogFragment fragment)
{
mActivity.showDialogFragment(fragment);
}
@Override
public void showToastMessage(String message)
{
@ -239,6 +246,13 @@ public final class SettingsFragment extends Fragment implements SettingsFragment
mActivity.onSettingChanged();
}
@Override
public void onControllerSettingsChanged()
{
mAdapter.notifyAllSettingsChanged();
mPresenter.updateOldControllerSettingsWarningVisibility();
}
@Override
public void onMenuTagAction(@NonNull MenuTag menuTag, int value)
{

View File

@ -24,6 +24,7 @@ import org.dolphinemu.dolphinemu.features.input.model.controlleremu.EmulatedCont
import org.dolphinemu.dolphinemu.features.input.model.controlleremu.NumericSetting;
import org.dolphinemu.dolphinemu.features.input.model.view.InputDeviceSetting;
import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting;
import org.dolphinemu.dolphinemu.features.input.ui.ProfileDialog;
import org.dolphinemu.dolphinemu.features.settings.model.AbstractBooleanSetting;
import org.dolphinemu.dolphinemu.features.settings.model.AbstractIntSetting;
import org.dolphinemu.dolphinemu.features.settings.model.AdHocBooleanSetting;
@ -1223,6 +1224,9 @@ public final class SettingsFragmentPresenter
sl.add(new RunRunnable(mContext, R.string.input_clear, R.string.input_clear_description,
R.string.input_reset_warning, 0, true, () -> clearControllerSettings(controller)));
sl.add(new RunRunnable(mContext, R.string.input_profiles, 0, 0, 0, true,
() -> mView.showDialogFragment(ProfileDialog.create(mMenuTag))));
updateOldControllerSettingsWarningVisibility(controller);
}
@ -1293,6 +1297,11 @@ public final class SettingsFragmentPresenter
}
}
public void updateOldControllerSettingsWarningVisibility()
{
updateOldControllerSettingsWarningVisibility(mMenuTag.getCorrespondingEmulatedController());
}
private void updateOldControllerSettingsWarningVisibility(EmulatedController controller)
{
String defaultDevice = controller.getDefaultDevice();
@ -1306,15 +1315,13 @@ public final class SettingsFragmentPresenter
private void loadDefaultControllerSettings(EmulatedController controller)
{
controller.loadDefaultSettings();
mView.getAdapter().notifyAllSettingsChanged();
updateOldControllerSettingsWarningVisibility(controller);
mView.onControllerSettingsChanged();
}
private void clearControllerSettings(EmulatedController controller)
{
controller.clearSettings();
mView.getAdapter().notifyAllSettingsChanged();
updateOldControllerSettingsWarningVisibility(controller);
mView.onControllerSettingsChanged();
}
private static int getLogVerbosityEntries()

View File

@ -3,6 +3,7 @@
package org.dolphinemu.dolphinemu.features.settings.ui;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentActivity;
import org.dolphinemu.dolphinemu.features.settings.model.Settings;
@ -55,6 +56,8 @@ public interface SettingsFragmentView
*/
void loadSubMenu(MenuTag menuKey);
void showDialogFragment(DialogFragment fragment);
/**
* Tell the Fragment to tell the containing activity to display a toast message.
*
@ -72,6 +75,14 @@ public interface SettingsFragmentView
*/
void onSettingChanged();
/**
* Refetches the values of all controller settings.
*
* To be used when loading an input profile or performing some other action that changes all
* controller settings at once.
*/
void onControllerSettingsChanged();
/**
* Have the fragment tell the containing Activity that the user wants to open the MenuTag
* associated with a setting.

View File

@ -45,6 +45,7 @@ public final class DirectoryInitialization
new MutableLiveData<>(DirectoryInitializationState.NOT_YET_INITIALIZED);
private static volatile boolean areDirectoriesAvailable = false;
private static String userPath;
private static String sysPath;
private static boolean isUsingLegacyUserDirectory = false;
public enum DirectoryInitializationState
@ -153,7 +154,8 @@ public final class DirectoryInitialization
}
// Let the native code know where the Sys directory is.
SetSysDirectory(sysDirectory.getPath());
sysPath = sysDirectory.getPath();
SetSysDirectory(sysPath);
}
private static void deleteDirectoryRecursively(@NonNull final File file)
@ -204,6 +206,16 @@ public final class DirectoryInitialization
return userPath;
}
public static String getSysDirectory()
{
if (!areDirectoriesAvailable)
{
throw new IllegalStateException(
"DirectoryInitialization must run before accessing the Sys directory!");
}
return sysPath;
}
public static File getGameListCache(Context context)
{
return new File(context.getExternalCacheDir(), "gamelist.cache");

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M7,21Q6.175,21 5.588,20.413Q5,19.825 5,19V6H4V4H9V3H15V4H20V6H19V19Q19,19.825 18.413,20.413Q17.825,21 17,21ZM17,6H7V19Q7,19 7,19Q7,19 7,19H17Q17,19 17,19Q17,19 17,19ZM9,17H11V8H9ZM13,17H15V8H13ZM7,6V19Q7,19 7,19Q7,19 7,19Q7,19 7,19Q7,19 7,19Z"/>
</vector>

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="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" />
</vector>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/profile_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/profile_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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:paddingHorizontal="@dimen/spacing_medlarge"
android:paddingVertical="@dimen/spacing_medlarge">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_marginEnd="@dimen/spacing_large"
android:layout_marginStart="@dimen/spacing_small"
android:textColor="?attr/colorOnSurface"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/button_delete"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Wii Remote with Motion Plus Pointing" />
<Button
android:id="@+id/button_delete"
style="?attr/materialIconButtonFilledTonalStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_small"
android:contentDescription="@string/input_profile_delete"
android:tooltipText="@string/input_profile_delete"
app:icon="@drawable/ic_delete"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/button_load"
app:layout_constraintStart_toEndOf="@id/text_name"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button_load"
style="?attr/materialIconButtonFilledTonalStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_small"
android:contentDescription="@string/input_profile_load"
android:tooltipText="@string/input_profile_load"
app:icon="@drawable/ic_load"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/button_save"
app:layout_constraintStart_toEndOf="@id/button_delete"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button_save"
style="?attr/materialIconButtonFilledTonalStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_small"
android:contentDescription="@string/input_profile_save"
android:tooltipText="@string/input_profile_save"
app:icon="@drawable/ic_save"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/button_load"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -30,6 +30,14 @@
<string name="input_device">Device</string>
<string name="input_device_all_devices">Create Mappings for Other Devices</string>
<string name="input_device_all_devices_description">Detects inputs from all devices, not just the selected device.</string>
<string name="input_profiles">Profiles</string>
<string name="input_profile_new">(New Profile)</string>
<string name="input_profile_load">Load</string>
<string name="input_profile_save">Save</string>
<string name="input_profile_delete">Delete</string>
<string name="input_profile_confirm_load">Do you want to discard your current controller settings and load the profile \"%1$s\"?</string>
<string name="input_profile_confirm_save">Do you want to overwrite the profile \"%1$s\"?</string>
<string name="input_profile_confirm_delete">Do you want to delete the profile \"%1$s\"?</string>
<string name="input_reset_to_default">Default</string>
<string name="input_reset_to_default_description">Reset settings for this controller to the default.</string>
<string name="input_clear">Clear</string>

View File

@ -3,6 +3,7 @@
#include <jni.h>
#include "Common/FileUtil.h"
#include "Common/IniFile.h"
#include "Core/HW/GCPad.h"
#include "Core/HW/Wiimote.h"
@ -95,6 +96,33 @@ Java_org_dolphinemu_dolphinemu_features_input_model_controlleremu_EmulatedContro
controller->UpdateReferences(g_controller_interface);
}
JNIEXPORT void JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_controlleremu_EmulatedController_loadProfile(
JNIEnv* env, jobject obj, jstring j_path)
{
ControllerEmu::EmulatedController* controller = EmulatedControllerFromJava(env, obj);
IniFile ini;
ini.Load(GetJString(env, j_path));
controller->LoadConfig(ini.GetOrCreateSection("Profile"));
controller->UpdateReferences(g_controller_interface);
}
JNIEXPORT void JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_controlleremu_EmulatedController_saveProfile(
JNIEnv* env, jobject obj, jstring j_path)
{
const std::string path = GetJString(env, j_path);
File::CreateFullPath(path);
IniFile ini;
EmulatedControllerFromJava(env, obj)->SaveConfig(ini.GetOrCreateSection("Profile"));
ini.Save(path);
}
JNIEXPORT jobject JNICALL
Java_org_dolphinemu_dolphinemu_features_input_model_controlleremu_EmulatedController_getGcPad(
JNIEnv* env, jclass, jint controller_index)