Android: New/improved ingame pause menu

This commit is contained in:
Connor McLaughlin 2021-05-02 14:59:58 +10:00
parent 19b84cbe4d
commit 70aae89219
27 changed files with 951 additions and 693 deletions

View file

@ -47,7 +47,7 @@ static jclass s_EmulationActivity_class;
static jmethodID s_EmulationActivity_method_reportError;
static jmethodID s_EmulationActivity_method_onEmulationStarted;
static jmethodID s_EmulationActivity_method_onEmulationStopped;
static jmethodID s_EmulationActivity_method_onGameTitleChanged;
static jmethodID s_EmulationActivity_method_onRunningGameChanged;
static jmethodID s_EmulationActivity_method_setVibration;
static jmethodID s_EmulationActivity_method_getRefreshRate;
static jmethodID s_EmulationActivity_method_openPauseMenu;
@ -703,9 +703,29 @@ void AndroidHostInterface::OnRunningGameChanged(const std::string& path, CDImage
if (m_emulation_activity_object)
{
JNIEnv* env = AndroidHelpers::GetJNIEnv();
jstring title_string = env->NewStringUTF(System::GetRunningTitle().c_str());
env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_onGameTitleChanged, title_string);
jstring path_string = env->NewStringUTF(path.c_str());
jstring code_string = env->NewStringUTF(game_code.c_str());
jstring title_string = env->NewStringUTF(game_title.c_str());
const GameListEntry* game_list_entry = m_game_list->GetEntryForPath(path.c_str());
std::string cover_path_str;
if (game_list_entry)
cover_path_str = m_game_list->GetCoverImagePathForEntry(game_list_entry);
else
cover_path_str = m_game_list->GetCoverImagePath(path, game_code, game_title);
jstring cover_path = nullptr;
if (!cover_path_str.empty())
cover_path = env->NewStringUTF(cover_path_str.c_str());
env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_onRunningGameChanged, path_string, code_string, title_string, cover_path);
if (cover_path)
env->DeleteLocalRef(cover_path);
env->DeleteLocalRef(title_string);
env->DeleteLocalRef(code_string);
env->DeleteLocalRef(path_string);
}
}
@ -994,8 +1014,8 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
env->GetMethodID(s_EmulationActivity_class, "onEmulationStarted", "()V")) == nullptr ||
(s_EmulationActivity_method_onEmulationStopped =
env->GetMethodID(s_EmulationActivity_class, "onEmulationStopped", "()V")) == nullptr ||
(s_EmulationActivity_method_onGameTitleChanged =
env->GetMethodID(s_EmulationActivity_class, "onGameTitleChanged", "(Ljava/lang/String;)V")) == nullptr ||
(s_EmulationActivity_method_onRunningGameChanged =
env->GetMethodID(s_EmulationActivity_class, "onRunningGameChanged", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V")) == nullptr ||
(s_EmulationActivity_method_setVibration = env->GetMethodID(emulation_activity_class, "setVibration", "(Z)V")) ==
nullptr ||
(s_EmulationActivity_method_getRefreshRate =

View file

@ -1,5 +1,6 @@
package com.github.stenzek.duckstation;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Vibrator;
import android.text.InputType;
@ -20,8 +21,8 @@ public class ControllerAutoMapper {
public void onComplete();
}
final private ControllerSettingsActivity parent;
final private int port;
private final Context context;
private final int port;
private final CompleteCallback completeCallback;
private InputDevice device;
@ -31,8 +32,8 @@ public class ControllerAutoMapper {
private String keyBase;
private String controllerType;
public ControllerAutoMapper(ControllerSettingsActivity activity, int port, CompleteCallback completeCallback) {
this.parent = activity;
public ControllerAutoMapper(Context context, int port, CompleteCallback completeCallback) {
this.context = context;
this.port = port;
this.completeCallback = completeCallback;
}
@ -126,7 +127,7 @@ public class ControllerAutoMapper {
}
if (deviceList.isEmpty()) {
final AlertDialog.Builder builder = new AlertDialog.Builder(parent);
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.main_activity_error);
builder.setMessage(R.string.controller_auto_mapping_no_devices);
builder.setPositiveButton(R.string.main_activity_ok, (dialog, which) -> dialog.dismiss());
@ -138,7 +139,7 @@ public class ControllerAutoMapper {
for (int i = 0; i < deviceList.size(); i++)
deviceNames[i] = deviceList.get(i).getName();
final AlertDialog.Builder builder = new AlertDialog.Builder(parent);
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.controller_auto_mapping_select_device);
builder.setItems(deviceNames, (dialog, which) -> {
process(deviceList.get(which));
@ -147,13 +148,13 @@ public class ControllerAutoMapper {
}
private void process(InputDevice device) {
this.prefs = PreferenceManager.getDefaultSharedPreferences(parent);
this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
this.editor = prefs.edit();
this.log = new StringBuilder();
this.device = device;
this.keyBase = String.format("Controller%d/", port);
this.controllerType = parent.getControllerType(prefs, port);
this.controllerType = ControllerSettingsCollectionFragment.getControllerType(prefs, port);
setButtonBindings();
setAxisBindings();
@ -162,10 +163,10 @@ public class ControllerAutoMapper {
this.editor.commit();
this.editor = null;
final AlertDialog.Builder builder = new AlertDialog.Builder(parent);
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.controller_auto_mapping_results);
final EditText editText = new EditText(parent);
final EditText editText = new EditText(context);
editText.setText(log.toString());
editText.setInputType(InputType.TYPE_NULL | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
editText.setSingleLine(false);

View file

@ -2,50 +2,33 @@ package com.github.stenzek.duckstation;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreferenceCompat;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import java.util.ArrayList;
import java.util.HashMap;
public class ControllerSettingsActivity extends AppCompatActivity {
private static final int NUM_CONTROLLER_PORTS = 2;
public static final String MULTITAP_MODE_SETTINGS_KEY = "ControllerPorts/MultitapMode";
private ArrayList<ControllerBindingPreference> mPreferences = new ArrayList<>();
private ControllerSettingsCollectionFragment fragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.settings_activity);
fragment = new ControllerSettingsCollectionFragment();
fragment.setMultitapModeChangedListener(this::recreate);
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings, new SettingsCollectionFragment(this))
.replace(R.id.settings, fragment)
.commit();
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
@ -84,7 +67,7 @@ public class ControllerSettingsActivity extends AppCompatActivity {
doSaveProfile();
return true;
} else if (id == R.id.action_clear_bindings) {
doClearBindings();
fragment.clearAllBindings();
return true;
} else {
return super.onOptionsItemSelected(item);
@ -124,7 +107,7 @@ public class ControllerSettingsActivity extends AppCompatActivity {
return;
}
updateAllBindings();
fragment.updateAllBindings();
}
private void doSaveProfile() {
@ -151,355 +134,5 @@ public class ControllerSettingsActivity extends AppCompatActivity {
builder.create().show();
}
private void doClearBindings() {
SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(this).edit();
for (ControllerBindingPreference pref : mPreferences)
pref.clearBinding(prefEdit);
prefEdit.commit();
}
private void updateAllBindings() {
for (ControllerBindingPreference pref : mPreferences)
pref.updateValue();
}
public static String getControllerTypeKey(int port) {
return String.format("Controller%d/Type", port);
}
public static String getControllerType(SharedPreferences prefs, int port) {
final String defaultControllerType = (port == 1) ? "DigitalController" : "None";
return prefs.getString(getControllerTypeKey(port), defaultControllerType);
}
public static class SettingsFragment extends PreferenceFragmentCompat {
ControllerSettingsActivity parent;
public SettingsFragment(ControllerSettingsActivity parent) {
this.parent = parent;
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.controllers_preferences, rootKey);
final Preference multitapModePreference = getPreferenceScreen().findPreference(MULTITAP_MODE_SETTINGS_KEY);
if (multitapModePreference != null) {
multitapModePreference.setOnPreferenceChangeListener((pref, newValue) -> {
parent.recreate();
return true;
});
}
}
}
public static class ControllerPortFragment extends PreferenceFragmentCompat {
private ControllerSettingsActivity activity;
private int controllerIndex;
private PreferenceCategory mButtonsCategory;
private PreferenceCategory mAxisCategory;
private PreferenceCategory mSettingsCategory;
public ControllerPortFragment(ControllerSettingsActivity activity, int controllerIndex) {
this.activity = activity;
this.controllerIndex = controllerIndex;
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
setPreferenceScreen(ps);
createPreferences();
}
private SwitchPreferenceCompat createTogglePreference(String key, int title, int summary, boolean defaultValue) {
final SwitchPreferenceCompat pref = new SwitchPreferenceCompat(getContext());
pref.setKey(key);
pref.setTitle(title);
pref.setSummary(summary);
pref.setIconSpaceReserved(false);
pref.setDefaultValue(defaultValue);
return pref;
}
private void createPreferences() {
final PreferenceScreen ps = getPreferenceScreen();
final SharedPreferences sp = getPreferenceManager().getSharedPreferences();
final String controllerType = getControllerType(sp, controllerIndex);
final String[] controllerButtons = AndroidHostInterface.getControllerButtonNames(controllerType);
final String[] axisButtons = AndroidHostInterface.getControllerAxisNames(controllerType);
final int vibrationMotors = AndroidHostInterface.getControllerVibrationMotorCount(controllerType);
final ListPreference typePreference = new ListPreference(getContext());
typePreference.setEntries(R.array.settings_controller_type_entries);
typePreference.setEntryValues(R.array.settings_controller_type_values);
typePreference.setKey(getControllerTypeKey(controllerIndex));
typePreference.setValue(controllerType);
typePreference.setTitle(R.string.settings_controller_type);
typePreference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance());
typePreference.setIconSpaceReserved(false);
typePreference.setOnPreferenceChangeListener((pref, value) -> {
removePreferences();
createPreferences(value.toString());
return true;
});
ps.addPreference(typePreference);
final Preference autoBindPreference = new Preference(getContext());
autoBindPreference.setTitle(R.string.controller_settings_automatic_mapping);
autoBindPreference.setSummary(R.string.controller_settings_summary_automatic_mapping);
autoBindPreference.setIconSpaceReserved(false);
autoBindPreference.setOnPreferenceClickListener(preference -> {
final ControllerAutoMapper mapper = new ControllerAutoMapper(activity, controllerIndex, () -> {
removePreferences();
createPreferences(typePreference.getValue());
});
mapper.start();
return true;
});
ps.addPreference(autoBindPreference);
final Preference clearBindingsPreference = new Preference(getContext());
clearBindingsPreference.setTitle(R.string.controller_settings_clear_controller_bindings);
clearBindingsPreference.setSummary(R.string.controller_settings_summary_clear_controller_bindings);
clearBindingsPreference.setIconSpaceReserved(false);
clearBindingsPreference.setOnPreferenceClickListener(preference -> {
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setMessage(R.string.controller_settings_clear_controller_bindings_confirm);
builder.setPositiveButton(R.string.main_activity_yes, (dialog, which) -> {
dialog.dismiss();
clearBindings();
});
builder.setNegativeButton(R.string.main_activity_no, (dialog, which) -> dialog.dismiss());
builder.create().show();
return true;
});
ps.addPreference(clearBindingsPreference);
mButtonsCategory = new PreferenceCategory(getContext());
mButtonsCategory.setTitle(getContext().getString(R.string.controller_settings_category_button_bindings));
mButtonsCategory.setIconSpaceReserved(false);
ps.addPreference(mButtonsCategory);
mAxisCategory = new PreferenceCategory(getContext());
mAxisCategory.setTitle(getContext().getString(R.string.controller_settings_category_axis_bindings));
mAxisCategory.setIconSpaceReserved(false);
ps.addPreference(mAxisCategory);
mSettingsCategory = new PreferenceCategory(getContext());
mSettingsCategory.setTitle(getContext().getString(R.string.controller_settings_category_settings));
mSettingsCategory.setIconSpaceReserved(false);
ps.addPreference(mSettingsCategory);
createPreferences(controllerType);
}
private void createPreferences(String controllerType) {
final PreferenceScreen ps = getPreferenceScreen();
final SharedPreferences sp = getPreferenceManager().getSharedPreferences();
final String[] buttonNames = AndroidHostInterface.getControllerButtonNames(controllerType);
final String[] axisNames = AndroidHostInterface.getControllerAxisNames(controllerType);
final int vibrationMotors = AndroidHostInterface.getControllerVibrationMotorCount(controllerType);
if (buttonNames != null) {
for (String buttonName : buttonNames) {
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
cbp.initButton(controllerIndex, buttonName);
mButtonsCategory.addPreference(cbp);
activity.mPreferences.add(cbp);
}
}
if (axisNames != null) {
for (String axisName : axisNames) {
final int axisType = AndroidHostInterface.getControllerAxisType(controllerType, axisName);
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
cbp.initAxis(controllerIndex, axisName, axisType);
mAxisCategory.addPreference(cbp);
activity.mPreferences.add(cbp);
}
}
if (vibrationMotors > 0) {
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
cbp.initVibration(controllerIndex);
mSettingsCategory.addPreference(cbp);
activity.mPreferences.add(cbp);
}
if (controllerType.equals("AnalogController")) {
mSettingsCategory.addPreference(
createTogglePreference(String.format("Controller%d/ForceAnalogOnReset", controllerIndex),
R.string.settings_enable_analog_mode_on_reset, R.string.settings_summary_enable_analog_mode_on_reset, true));
mSettingsCategory.addPreference(
createTogglePreference(String.format("Controller%d/AnalogDPadInDigitalMode", controllerIndex),
R.string.settings_use_analog_sticks_for_dpad, R.string.settings_summary_use_analog_sticks_for_dpad, true));
}
}
private void removePreferences() {
for (int i = 0; i < mButtonsCategory.getPreferenceCount(); i++) {
activity.mPreferences.remove(mButtonsCategory.getPreference(i));
}
mButtonsCategory.removeAll();
for (int i = 0; i < mAxisCategory.getPreferenceCount(); i++) {
activity.mPreferences.remove(mAxisCategory.getPreference(i));
}
mAxisCategory.removeAll();
for (int i = 0; i < mSettingsCategory.getPreferenceCount(); i++) {
activity.mPreferences.remove(mSettingsCategory.getPreference(i));
}
mSettingsCategory.removeAll();
}
private static void clearBindingsInCategory(SharedPreferences.Editor editor, PreferenceCategory category) {
for (int i = 0; i < category.getPreferenceCount(); i++) {
final Preference preference = category.getPreference(i);
if (preference instanceof ControllerBindingPreference)
((ControllerBindingPreference)preference).clearBinding(editor);
}
}
private void clearBindings() {
final SharedPreferences.Editor editor = getPreferenceManager().getSharedPreferences().edit();
clearBindingsInCategory(editor, mButtonsCategory);
clearBindingsInCategory(editor, mAxisCategory);
clearBindingsInCategory(editor, mSettingsCategory);
editor.commit();
Toast.makeText(activity, activity.getString(
R.string.controller_settings_clear_controller_bindings_done, controllerIndex),
Toast.LENGTH_LONG).show();
}
}
public static class HotkeyFragment extends PreferenceFragmentCompat {
private ControllerSettingsActivity activity;
private HotkeyInfo[] mHotkeyInfo;
public HotkeyFragment(ControllerSettingsActivity activity) {
this.activity = activity;
this.mHotkeyInfo = AndroidHostInterface.getInstance().getHotkeyInfoList();
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
if (mHotkeyInfo != null) {
final HashMap<String, PreferenceCategory> categoryMap = new HashMap<>();
for (HotkeyInfo hotkeyInfo : mHotkeyInfo) {
PreferenceCategory category = categoryMap.containsKey(hotkeyInfo.getCategory()) ?
categoryMap.get(hotkeyInfo.getCategory()) : null;
if (category == null) {
category = new PreferenceCategory(getContext());
category.setTitle(hotkeyInfo.getCategory());
category.setIconSpaceReserved(false);
categoryMap.put(hotkeyInfo.getCategory(), category);
ps.addPreference(category);
}
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
cbp.initHotkey(hotkeyInfo);
category.addPreference(cbp);
activity.mPreferences.add(cbp);
}
}
setPreferenceScreen(ps);
}
}
public static class SettingsCollectionFragment extends Fragment {
private ControllerSettingsActivity activity;
private SettingsCollectionAdapter adapter;
private ViewPager2 viewPager;
private String[] controllerPortNames;
private static final int NUM_MAIN_CONTROLLER_PORTS = 2;
private static final int NUM_SUB_CONTROLLER_PORTS = 4;
private static final char[] SUB_CONTROLLER_PORT_NAMES = new char[] {'A', 'B', 'C', 'D'};
public SettingsCollectionFragment(ControllerSettingsActivity activity) {
this.activity = activity;
final String multitapMode = PreferenceManager.getDefaultSharedPreferences(activity).getString(
MULTITAP_MODE_SETTINGS_KEY, "Disabled");
final ArrayList<String> portNames = new ArrayList<>();
for (int i = 0; i < NUM_MAIN_CONTROLLER_PORTS; i++) {
final boolean isMultitap = (multitapMode.equals("BothPorts") ||
(i == 0 && multitapMode.equals("Port1Only")) ||
(i == 1 && multitapMode.equals("Port2Only")));
if (isMultitap) {
for (int j = 0; j < NUM_SUB_CONTROLLER_PORTS; j++) {
portNames.add(activity.getString(
R.string.controller_settings_sub_port_format,
i + 1, SUB_CONTROLLER_PORT_NAMES[j]));
}
} else {
portNames.add(activity.getString(
R.string.controller_settings_main_port_format,
i + 1));
}
}
controllerPortNames = new String[portNames.size()];
portNames.toArray(controllerPortNames);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_controller_settings, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
adapter = new SettingsCollectionAdapter(activity, this, controllerPortNames.length);
viewPager = view.findViewById(R.id.view_pager);
viewPager.setAdapter(adapter);
TabLayout tabLayout = view.findViewById(R.id.tab_layout);
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
if (position == 0)
tab.setText(R.string.controller_settings_tab_settings);
else if (position <= controllerPortNames.length)
tab.setText(controllerPortNames[position - 1]);
else
tab.setText(R.string.controller_settings_tab_hotkeys);
}).attach();
}
}
public static class SettingsCollectionAdapter extends FragmentStateAdapter {
private ControllerSettingsActivity activity;
private int controllerPorts;
public SettingsCollectionAdapter(@NonNull ControllerSettingsActivity activity, @NonNull Fragment fragment, int controllerPorts) {
super(fragment);
this.activity = activity;
this.controllerPorts = controllerPorts;
}
@NonNull
@Override
public Fragment createFragment(int position) {
if (position == 0)
return new SettingsFragment(activity);
else if (position <= controllerPorts)
return new ControllerPortFragment(activity, position);
else
return new HotkeyFragment(activity);
}
@Override
public int getItemCount() {
return controllerPorts + 2;
}
}
}

View file

@ -0,0 +1,393 @@
package com.github.stenzek.duckstation;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreferenceCompat;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import java.util.ArrayList;
import java.util.HashMap;
public class ControllerSettingsCollectionFragment extends Fragment {
public static final String MULTITAP_MODE_SETTINGS_KEY = "ControllerPorts/MultitapMode";
private static final int NUM_MAIN_CONTROLLER_PORTS = 2;
private static final int NUM_SUB_CONTROLLER_PORTS = 4;
private static final char[] SUB_CONTROLLER_PORT_NAMES = new char[]{'A', 'B', 'C', 'D'};
public interface MultitapModeChangedListener {
void onChanged();
}
private final ArrayList<ControllerBindingPreference> preferences = new ArrayList<>();
private SettingsCollectionAdapter adapter;
private ViewPager2 viewPager;
private String[] controllerPortNames;
private MultitapModeChangedListener multitapModeChangedListener;
public ControllerSettingsCollectionFragment() {
}
public static String getControllerTypeKey(int port) {
return String.format("Controller%d/Type", port);
}
public static String getControllerType(SharedPreferences prefs, int port) {
final String defaultControllerType = (port == 1) ? "DigitalController" : "None";
return prefs.getString(getControllerTypeKey(port), defaultControllerType);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_controller_settings, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
final String multitapMode = PreferenceManager.getDefaultSharedPreferences(getContext()).getString(
MULTITAP_MODE_SETTINGS_KEY, "Disabled");
final ArrayList<String> portNames = new ArrayList<>();
for (int i = 0; i < NUM_MAIN_CONTROLLER_PORTS; i++) {
final boolean isMultitap = (multitapMode.equals("BothPorts") ||
(i == 0 && multitapMode.equals("Port1Only")) ||
(i == 1 && multitapMode.equals("Port2Only")));
if (isMultitap) {
for (int j = 0; j < NUM_SUB_CONTROLLER_PORTS; j++) {
portNames.add(getContext().getString(
R.string.controller_settings_sub_port_format,
i + 1, SUB_CONTROLLER_PORT_NAMES[j]));
}
} else {
portNames.add(getContext().getString(
R.string.controller_settings_main_port_format,
i + 1));
}
}
controllerPortNames = new String[portNames.size()];
portNames.toArray(controllerPortNames);
adapter = new SettingsCollectionAdapter(this, controllerPortNames.length);
viewPager = view.findViewById(R.id.view_pager);
viewPager.setAdapter(adapter);
TabLayout tabLayout = view.findViewById(R.id.tab_layout);
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
if (position == 0)
tab.setText(R.string.controller_settings_tab_settings);
else if (position <= controllerPortNames.length)
tab.setText(controllerPortNames[position - 1]);
else
tab.setText(R.string.controller_settings_tab_hotkeys);
}).attach();
}
public void setMultitapModeChangedListener(MultitapModeChangedListener multitapModeChangedListener) {
this.multitapModeChangedListener = multitapModeChangedListener;
}
public void clearAllBindings() {
SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(getContext()).edit();
for (ControllerBindingPreference pref : preferences)
pref.clearBinding(prefEdit);
prefEdit.commit();
}
public void updateAllBindings() {
for (ControllerBindingPreference pref : preferences)
pref.updateValue();
}
public static class SettingsFragment extends PreferenceFragmentCompat {
private final ControllerSettingsCollectionFragment parent;
public SettingsFragment(ControllerSettingsCollectionFragment parent) {
this.parent = parent;
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.controllers_preferences, rootKey);
final Preference multitapModePreference = getPreferenceScreen().findPreference(MULTITAP_MODE_SETTINGS_KEY);
if (multitapModePreference != null) {
multitapModePreference.setOnPreferenceChangeListener((pref, newValue) -> {
if (parent.multitapModeChangedListener != null)
parent.multitapModeChangedListener.onChanged();
return true;
});
}
}
}
public static class ControllerPortFragment extends PreferenceFragmentCompat {
private final ControllerSettingsCollectionFragment parent;
private final int controllerIndex;
private PreferenceCategory mButtonsCategory;
private PreferenceCategory mAxisCategory;
private PreferenceCategory mSettingsCategory;
public ControllerPortFragment(ControllerSettingsCollectionFragment parent, int controllerIndex) {
this.parent = parent;
this.controllerIndex = controllerIndex;
}
private static void clearBindingsInCategory(SharedPreferences.Editor editor, PreferenceCategory category) {
for (int i = 0; i < category.getPreferenceCount(); i++) {
final Preference preference = category.getPreference(i);
if (preference instanceof ControllerBindingPreference)
((ControllerBindingPreference) preference).clearBinding(editor);
}
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
setPreferenceScreen(ps);
createPreferences();
}
private SwitchPreferenceCompat createTogglePreference(String key, int title, int summary, boolean defaultValue) {
final SwitchPreferenceCompat pref = new SwitchPreferenceCompat(getContext());
pref.setKey(key);
pref.setTitle(title);
pref.setSummary(summary);
pref.setIconSpaceReserved(false);
pref.setDefaultValue(defaultValue);
return pref;
}
private void createPreferences() {
final PreferenceScreen ps = getPreferenceScreen();
final SharedPreferences sp = getPreferenceManager().getSharedPreferences();
final String controllerType = getControllerType(sp, controllerIndex);
final String[] controllerButtons = AndroidHostInterface.getControllerButtonNames(controllerType);
final String[] axisButtons = AndroidHostInterface.getControllerAxisNames(controllerType);
final int vibrationMotors = AndroidHostInterface.getControllerVibrationMotorCount(controllerType);
final ListPreference typePreference = new ListPreference(getContext());
typePreference.setEntries(R.array.settings_controller_type_entries);
typePreference.setEntryValues(R.array.settings_controller_type_values);
typePreference.setKey(getControllerTypeKey(controllerIndex));
typePreference.setValue(controllerType);
typePreference.setTitle(R.string.settings_controller_type);
typePreference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance());
typePreference.setIconSpaceReserved(false);
typePreference.setOnPreferenceChangeListener((pref, value) -> {
removePreferences();
createPreferences(value.toString());
return true;
});
ps.addPreference(typePreference);
final Preference autoBindPreference = new Preference(getContext());
autoBindPreference.setTitle(R.string.controller_settings_automatic_mapping);
autoBindPreference.setSummary(R.string.controller_settings_summary_automatic_mapping);
autoBindPreference.setIconSpaceReserved(false);
autoBindPreference.setOnPreferenceClickListener(preference -> {
final ControllerAutoMapper mapper = new ControllerAutoMapper(getContext(), controllerIndex, () -> {
removePreferences();
createPreferences(typePreference.getValue());
});
mapper.start();
return true;
});
ps.addPreference(autoBindPreference);
final Preference clearBindingsPreference = new Preference(getContext());
clearBindingsPreference.setTitle(R.string.controller_settings_clear_controller_bindings);
clearBindingsPreference.setSummary(R.string.controller_settings_summary_clear_controller_bindings);
clearBindingsPreference.setIconSpaceReserved(false);
clearBindingsPreference.setOnPreferenceClickListener(preference -> {
final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setMessage(R.string.controller_settings_clear_controller_bindings_confirm);
builder.setPositiveButton(R.string.main_activity_yes, (dialog, which) -> {
dialog.dismiss();
clearBindings();
});
builder.setNegativeButton(R.string.main_activity_no, (dialog, which) -> dialog.dismiss());
builder.create().show();
return true;
});
ps.addPreference(clearBindingsPreference);
mButtonsCategory = new PreferenceCategory(getContext());
mButtonsCategory.setTitle(getContext().getString(R.string.controller_settings_category_button_bindings));
mButtonsCategory.setIconSpaceReserved(false);
ps.addPreference(mButtonsCategory);
mAxisCategory = new PreferenceCategory(getContext());
mAxisCategory.setTitle(getContext().getString(R.string.controller_settings_category_axis_bindings));
mAxisCategory.setIconSpaceReserved(false);
ps.addPreference(mAxisCategory);
mSettingsCategory = new PreferenceCategory(getContext());
mSettingsCategory.setTitle(getContext().getString(R.string.controller_settings_category_settings));
mSettingsCategory.setIconSpaceReserved(false);
ps.addPreference(mSettingsCategory);
createPreferences(controllerType);
}
private void createPreferences(String controllerType) {
final PreferenceScreen ps = getPreferenceScreen();
final SharedPreferences sp = getPreferenceManager().getSharedPreferences();
final String[] buttonNames = AndroidHostInterface.getControllerButtonNames(controllerType);
final String[] axisNames = AndroidHostInterface.getControllerAxisNames(controllerType);
final int vibrationMotors = AndroidHostInterface.getControllerVibrationMotorCount(controllerType);
if (buttonNames != null) {
for (String buttonName : buttonNames) {
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
cbp.initButton(controllerIndex, buttonName);
mButtonsCategory.addPreference(cbp);
parent.preferences.add(cbp);
}
}
if (axisNames != null) {
for (String axisName : axisNames) {
final int axisType = AndroidHostInterface.getControllerAxisType(controllerType, axisName);
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
cbp.initAxis(controllerIndex, axisName, axisType);
mAxisCategory.addPreference(cbp);
parent.preferences.add(cbp);
}
}
if (vibrationMotors > 0) {
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
cbp.initVibration(controllerIndex);
mSettingsCategory.addPreference(cbp);
parent.preferences.add(cbp);
}
if (controllerType.equals("AnalogController")) {
mSettingsCategory.addPreference(
createTogglePreference(String.format("Controller%d/ForceAnalogOnReset", controllerIndex),
R.string.settings_enable_analog_mode_on_reset, R.string.settings_summary_enable_analog_mode_on_reset, true));
mSettingsCategory.addPreference(
createTogglePreference(String.format("Controller%d/AnalogDPadInDigitalMode", controllerIndex),
R.string.settings_use_analog_sticks_for_dpad, R.string.settings_summary_use_analog_sticks_for_dpad, true));
}
}
private void removePreferences() {
for (int i = 0; i < mButtonsCategory.getPreferenceCount(); i++) {
parent.preferences.remove(mButtonsCategory.getPreference(i));
}
mButtonsCategory.removeAll();
for (int i = 0; i < mAxisCategory.getPreferenceCount(); i++) {
parent.preferences.remove(mAxisCategory.getPreference(i));
}
mAxisCategory.removeAll();
for (int i = 0; i < mSettingsCategory.getPreferenceCount(); i++) {
parent.preferences.remove(mSettingsCategory.getPreference(i));
}
mSettingsCategory.removeAll();
}
private void clearBindings() {
final SharedPreferences.Editor editor = getPreferenceManager().getSharedPreferences().edit();
clearBindingsInCategory(editor, mButtonsCategory);
clearBindingsInCategory(editor, mAxisCategory);
clearBindingsInCategory(editor, mSettingsCategory);
editor.commit();
Toast.makeText(parent.getContext(), parent.getString(
R.string.controller_settings_clear_controller_bindings_done, controllerIndex),
Toast.LENGTH_LONG).show();
}
}
public static class HotkeyFragment extends PreferenceFragmentCompat {
private final ControllerSettingsCollectionFragment parent;
private final HotkeyInfo[] mHotkeyInfo;
public HotkeyFragment(ControllerSettingsCollectionFragment parent) {
this.parent = parent;
this.mHotkeyInfo = AndroidHostInterface.getInstance().getHotkeyInfoList();
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
if (mHotkeyInfo != null) {
final HashMap<String, PreferenceCategory> categoryMap = new HashMap<>();
for (HotkeyInfo hotkeyInfo : mHotkeyInfo) {
PreferenceCategory category = categoryMap.containsKey(hotkeyInfo.getCategory()) ?
categoryMap.get(hotkeyInfo.getCategory()) : null;
if (category == null) {
category = new PreferenceCategory(getContext());
category.setTitle(hotkeyInfo.getCategory());
category.setIconSpaceReserved(false);
categoryMap.put(hotkeyInfo.getCategory(), category);
ps.addPreference(category);
}
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
cbp.initHotkey(hotkeyInfo);
category.addPreference(cbp);
parent.preferences.add(cbp);
}
}
setPreferenceScreen(ps);
}
}
public static class SettingsCollectionAdapter extends FragmentStateAdapter {
private final ControllerSettingsCollectionFragment parent;
private final int controllerPorts;
public SettingsCollectionAdapter(@NonNull ControllerSettingsCollectionFragment parent, int controllerPorts) {
super(parent);
this.parent = parent;
this.controllerPorts = controllerPorts;
}
@NonNull
@Override
public Fragment createFragment(int position) {
if (position == 0)
return new SettingsFragment(parent);
else if (position <= controllerPorts)
return new ControllerPortFragment(parent, position);
else
return new HotkeyFragment(parent);
}
@Override
public int getItemCount() {
return controllerPorts + 2;
}
}
}

View file

@ -1,29 +1,41 @@
package com.github.stenzek.duckstation;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.hardware.input.InputManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Vibrator;
import android.util.Log;
import android.view.Display;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
/**
@ -38,7 +50,10 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
private boolean mWasDestroyed = false;
private boolean mStopRequested = false;
private boolean mApplySettingsOnSurfaceRestored = false;
private String mGamePath = null;
private String mGameCode = null;
private String mGameTitle = null;
private String mGameCoverPath = null;
private EmulationSurfaceView mContentView;
private boolean getBooleanSetting(String key, boolean defaultValue) {
@ -139,9 +154,12 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
});
}
public void onGameTitleChanged(String title) {
public void onRunningGameChanged(String path, String code, String title, String coverPath) {
runOnUiThread(() -> {
mGamePath = path;
mGameTitle = title;
mGameCode = code;
mGameCoverPath = coverPath;
});
}
@ -162,7 +180,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
public void openPauseMenu() {
runOnUiThread(() -> {
showMenu();
showPauseMenu();
});
}
@ -332,7 +350,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
@Override
public void onBackPressed() {
showMenu();
showPauseMenu();
}
@Override
@ -427,70 +445,13 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
return true;
}
private void showMenu() {
if (getBooleanSetting("Main/PauseOnMenu", true) &&
!AndroidHostInterface.getInstance().isEmulationThreadPaused()) {
private void showPauseMenu() {
if (!AndroidHostInterface.getInstance().isEmulationThreadPaused()) {
AndroidHostInterface.getInstance().pauseEmulationThread(true);
}
AlertDialog.Builder builder = new AlertDialog.Builder(this);
if (mGameTitle != null && !mGameTitle.isEmpty())
builder.setTitle(mGameTitle);
builder.setItems(R.array.emulation_menu, (dialogInterface, i) -> {
switch (i) {
case 0: // Load State
{
showSaveStateMenu(false);
return;
}
case 1: // Save State
{
showSaveStateMenu(true);
return;
}
case 2: // Toggle Fast Forward
{
AndroidHostInterface.getInstance().setFastForwardEnabled(!AndroidHostInterface.getInstance().isFastForwardEnabled());
onMenuClosed();
return;
}
case 3: // Achievements
{
showAchievementsPopup();
return;
}
case 4: // More Options
{
showMoreMenu();
return;
}
case 5: // Quit
{
mStopRequested = true;
finish();
return;
}
}
});
builder.setOnCancelListener(dialogInterface -> onMenuClosed());
final AlertDialog dialog = builder.create();
dialog.setOnShowListener(dialogInterface -> {
// Disable cheevos if not loaded.
if (AndroidHostInterface.getInstance().getCheevoCount() == 0)
disableDialogMenuItem(dialog, 3);
// Disable load state for challenge mode.
if (AndroidHostInterface.getInstance().isCheevosChallengeModeActive())
disableDialogMenuItem(dialog, 0);
});
dialog.show();
final MenuDialogFragment fragment = new MenuDialogFragment(this);
fragment.show(getSupportFragmentManager(), "MenuDialogFragment");
}
private void showSaveStateMenu(boolean saving) {
@ -523,66 +484,6 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
dialog.show();
}
private void showMoreMenu() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
if (mGameTitle != null && !mGameTitle.isEmpty())
builder.setTitle(mGameTitle);
builder.setItems(R.array.emulation_more_menu, (dialogInterface, i) -> {
switch (i) {
case 0: // Reset
{
AndroidHostInterface.getInstance().resetSystem();
onMenuClosed();
return;
}
case 1: // Patch Codes
{
showPatchesMenu();
return;
}
case 2: // Change Disc
{
showDiscChangeMenu();
return;
}
case 3: // Change Touchscreen Controller
{
showTouchscreenControllerMenu();
return;
}
case 4: // Settings
{
Intent intent = new Intent(EmulationActivity.this, ControllerSettingsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResult(intent, REQUEST_CODE_SETTINGS);
return;
}
case 5: // Controller Settings
{
Intent intent = new Intent(EmulationActivity.this, SettingsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResult(intent, REQUEST_CODE_SETTINGS);
return;
}
}
});
builder.setOnCancelListener(dialogInterface -> onMenuClosed());
final AlertDialog dialog = builder.create();
dialog.setOnShowListener(dialogInterface -> {
// Disable patch codes when challenge mode is active.
if (AndroidHostInterface.getInstance().isCheevosChallengeModeActive())
disableDialogMenuItem(dialog, 1);
});
dialog.show();
}
private void showTouchscreenControllerMenu() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.dialog_touchscreen_controller_settings);
@ -882,4 +783,170 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
mSustainedPerformanceModeEnabled = enabled;
}
public static class MenuDialogFragment extends DialogFragment {
private EmulationActivity emulationActivity;
private boolean settingsChanged = false;
public MenuDialogFragment(EmulationActivity emulationActivity) {
this.emulationActivity = emulationActivity;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(STYLE_NO_FRAME, R.style.EmulationActivityOverlay);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_emulation_activity_overlay, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
setContentFragment(new MenuSettingsFragment(this, emulationActivity), false);
final ImageView coverView =((ImageView)view.findViewById(R.id.cover_image));
if (emulationActivity.mGameCoverPath != null && !emulationActivity.mGameCoverPath.isEmpty()) {
new ImageLoadTask(coverView).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
emulationActivity.mGameCoverPath);
} else {
new GenerateCoverTask(getContext(), coverView, emulationActivity.mGameTitle)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
coverView.setOnClickListener(v -> close(true));
((TextView)view.findViewById(R.id.title)).setText(emulationActivity.mGameTitle);
final String subtitle = String.format("%s - %s", emulationActivity.mGameCode,
FileHelper.getFileNameForPath(emulationActivity.mGamePath));
((TextView)view.findViewById(R.id.subtitle)).setText(subtitle);
((ImageButton)view.findViewById(R.id.menu)).setOnClickListener(v -> onMenuClicked());
((ImageButton)view.findViewById(R.id.controller_settings)).setOnClickListener(v -> onControllerSettingsClicked());
((ImageButton)view.findViewById(R.id.settings)).setOnClickListener(v -> onSettingsClicked());
((ImageButton)view.findViewById(R.id.quit)).setOnClickListener(v -> onQuitClicked());
}
@Override
public void onCancel(@NonNull DialogInterface dialog) {
onClosed(true);
}
private void onClosed(boolean resumeGame) {
if (settingsChanged)
emulationActivity.applySettings();
if (resumeGame)
emulationActivity.onMenuClosed();
}
private void close(boolean resumeGame) {
dismiss();
onClosed(resumeGame);
}
private void setContentFragment(Fragment fragment, boolean transition) {
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
if (transition)
transaction.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out);
transaction.replace(R.id.content, fragment).commit();
}
private void onMenuClicked() {
setContentFragment(new MenuSettingsFragment(this, emulationActivity), true);
}
private void onControllerSettingsClicked() {
ControllerSettingsCollectionFragment fragment = new ControllerSettingsCollectionFragment();
setContentFragment(fragment, true);
fragment.setMultitapModeChangedListener(this::onControllerSettingsClicked);
settingsChanged = true;
}
private void onSettingsClicked() {
setContentFragment(new SettingsCollectionFragment(), true);
settingsChanged = true;
}
private void onQuitClicked() {
close(false);
emulationActivity.mStopRequested = true;
emulationActivity.finish();
}
}
public static class MenuSettingsFragment extends PreferenceFragmentCompat {
private MenuDialogFragment menuDialogFragment;
private EmulationActivity emulationActivity;
public MenuSettingsFragment(MenuDialogFragment menuDialogFragment, EmulationActivity emulationActivity) {
this.menuDialogFragment = menuDialogFragment;
this.emulationActivity = emulationActivity;
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getContext()));
final boolean cheevosActive = AndroidHostInterface.getInstance().isCheevosActive();
final boolean cheevosChallengeModeEnabled = AndroidHostInterface.getInstance().isCheevosChallengeModeActive();
createPreference(R.string.emulation_menu_load_state, R.drawable.ic_baseline_folder_open_24, !cheevosChallengeModeEnabled, preference -> {
menuDialogFragment.close(false);
emulationActivity.showSaveStateMenu(false);
return true;
});
createPreference(R.string.emulation_menu_save_state, R.drawable.ic_baseline_save_24, true, preference -> {
menuDialogFragment.close(false);
emulationActivity.showSaveStateMenu(true);
return true;
});
createPreference(R.string.emulation_menu_toggle_fast_forward, R.drawable.ic_baseline_fast_forward_24, !cheevosChallengeModeEnabled, preference -> {
AndroidHostInterface.getInstance().setFastForwardEnabled(!AndroidHostInterface.getInstance().isFastForwardEnabled());
menuDialogFragment.close(true);
return true;
});
createPreference(R.string.emulation_menu_achievements, R.drawable.ic_baseline_trophy_24, cheevosActive, preference -> {
menuDialogFragment.close(false);
emulationActivity.showAchievementsPopup();
return true;
});
createPreference(R.string.emulation_menu_patch_codes, R.drawable.ic_baseline_tips_and_updates_24, !cheevosChallengeModeEnabled, preference -> {
menuDialogFragment.close(false);
emulationActivity.showPatchesMenu();
return true;
});
createPreference(R.string.emulation_menu_change_disc, R.drawable.ic_baseline_album_24, true, preference -> {
menuDialogFragment.close(false);
emulationActivity.showDiscChangeMenu();
return true;
});
createPreference(R.string.emulation_menu_touchscreen_controller_settings, R.drawable.ic_baseline_touch_app_24, true, preference -> {
menuDialogFragment.close(false);
emulationActivity.showTouchscreenControllerMenu();
return true;
});
createPreference(R.string.emulation_menu_toggle_analog_mode, R.drawable.ic_baseline_gamepad_24, true, preference -> {
AndroidHostInterface.getInstance().toggleControllerAnalogMode();
menuDialogFragment.close(true);
return true;
});
createPreference(R.string.emulation_menu_reset_console, R.drawable.ic_baseline_restart_alt_24, true, preference -> {
AndroidHostInterface.getInstance().resetSystem();
menuDialogFragment.close(true);
return true;
});
}
private void createPreference(int titleId, int icon, boolean enabled, Preference.OnPreferenceClickListener action) {
final Preference preference = new Preference(getContext());
preference.setTitle(titleId);
preference.setIcon(icon);
preference.setOnPreferenceClickListener(action);
preference.setEnabled(enabled);
getPreferenceScreen().addPreference(preference);
}
}
}

View file

@ -1,22 +1,14 @@
package com.github.stenzek.duckstation;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceFragmentCompat;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
public class SettingsActivity extends AppCompatActivity {
@ -49,78 +41,4 @@ public class SettingsActivity extends AppCompatActivity {
return super.onOptionsItemSelected(item);
}
public static class SettingsFragment extends PreferenceFragmentCompat {
private final int resourceId;
public SettingsFragment(int resourceId) {
this.resourceId = resourceId;
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(resourceId, rootKey);
}
}
public static class SettingsCollectionFragment extends Fragment {
private SettingsCollectionAdapter adapter;
private ViewPager2 viewPager;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_settings_collection, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
adapter = new SettingsCollectionAdapter(this);
viewPager = view.findViewById(R.id.view_pager);
viewPager.setAdapter(adapter);
TabLayout tabLayout = view.findViewById(R.id.tab_layout);
new TabLayoutMediator(tabLayout, viewPager,
(tab, position) -> tab.setText(getResources().getStringArray(R.array.settings_tabs)[position])
).attach();
}
}
public static class SettingsCollectionAdapter extends FragmentStateAdapter {
public SettingsCollectionAdapter(@NonNull Fragment fragment) {
super(fragment);
}
@NonNull
@Override
public Fragment createFragment(int position) {
switch (position) {
case 0: // General
return new SettingsFragment(R.xml.general_preferences);
case 1: // Display
return new SettingsFragment(R.xml.display_preferences);
case 2: // Audio
return new SettingsFragment(R.xml.audio_preferences);
case 3: // Enhancements
return new SettingsFragment(R.xml.enhancements_preferences);
case 4: // Achievements
return new AchievementSettingsFragment();
case 5: // Advanced
return new SettingsFragment(R.xml.advanced_preferences);
default:
return new Fragment();
}
}
@Override
public int getItemCount() {
return 6;
}
}
}

View file

@ -0,0 +1,90 @@
package com.github.stenzek.duckstation;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceFragmentCompat;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
public class SettingsCollectionFragment extends Fragment {
private SettingsCollectionAdapter adapter;
private ViewPager2 viewPager;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_settings_collection, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
adapter = new SettingsCollectionAdapter(this);
viewPager = view.findViewById(R.id.view_pager);
viewPager.setAdapter(adapter);
TabLayout tabLayout = view.findViewById(R.id.tab_layout);
new TabLayoutMediator(tabLayout, viewPager,
(tab, position) -> tab.setText(getResources().getStringArray(R.array.settings_tabs)[position])
).attach();
}
public static class SettingsFragment extends PreferenceFragmentCompat {
private final int resourceId;
public SettingsFragment(int resourceId) {
this.resourceId = resourceId;
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(resourceId, rootKey);
}
}
public static class SettingsCollectionAdapter extends FragmentStateAdapter {
public SettingsCollectionAdapter(@NonNull Fragment fragment) {
super(fragment);
}
@NonNull
@Override
public Fragment createFragment(int position) {
switch (position) {
case 0: // General
return new SettingsFragment(R.xml.general_preferences);
case 1: // Display
return new SettingsFragment(R.xml.display_preferences);
case 2: // Audio
return new SettingsFragment(R.xml.audio_preferences);
case 3: // Enhancements
return new SettingsFragment(R.xml.enhancements_preferences);
case 4: // Achievements
return new AchievementSettingsFragment();
case 5: // Advanced
return new SettingsFragment(R.xml.advanced_preferences);
default:
return new Fragment();
}
}
@Override
public int getItemCount() {
return 6;
}
}
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,16.5c-2.49,0 -4.5,-2.01 -4.5,-4.5S9.51,7.5 12,7.5s4.5,2.01 4.5,4.5 -2.01,4.5 -4.5,4.5zM12,11c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1z"/>
</vector>

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M10.09,15.59L11.5,17l5,-5 -5,-5 -1.41,1.41L12.67,11H3v2h9.67l-2.58,2.59zM19,3H5c-1.11,0 -2,0.9 -2,2v4h2V5h14v14H5v-4H3v4c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M4,18l8.5,-6L4,6v12zM13,6v12l8.5,-6L13,6z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M11,18L11,6l-8.5,6 8.5,6zM11.5,12l8.5,6L20,6l-8.5,6z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z"/>
</vector>

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,5V2L8,6l4,4V7c3.31,0 6,2.69 6,6c0,2.97 -2.17,5.43 -5,5.91v2.02c3.95,-0.49 7,-3.85 7,-7.93C20,8.58 16.42,5 12,5z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M6,13c0,-1.65 0.67,-3.15 1.76,-4.24L6.34,7.34C4.9,8.79 4,10.79 4,13c0,4.08 3.05,7.44 7,7.93v-2.02C8.17,18.43 6,15.97 6,13z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M7,20h4c0,1.1 -0.9,2 -2,2S7,21.1 7,20zM5,19h8v-2H5V19zM16.5,9.5c0,3.82 -2.66,5.86 -3.77,6.5H5.27C4.16,15.36 1.5,13.32 1.5,9.5C1.5,5.36 4.86,2 9,2S16.5,5.36 16.5,9.5zM21.37,7.37L20,8l1.37,0.63L22,10l0.63,-1.37L24,8l-1.37,-0.63L22,6L21.37,7.37zM19,6l0.94,-2.06L22,3l-2.06,-0.94L19,0l-0.94,2.06L16,3l2.06,0.94L19,6z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M9,11.24V7.5C9,6.12 10.12,5 11.5,5S14,6.12 14,7.5v3.74c1.21,-0.81 2,-2.18 2,-3.74C16,5.01 13.99,3 11.5,3S7,5.01 7,7.5C7,9.06 7.79,10.43 9,11.24zM18.84,15.87l-4.54,-2.26c-0.17,-0.07 -0.35,-0.11 -0.54,-0.11H13v-6C13,6.67 12.33,6 11.5,6S10,6.67 10,7.5v10.74c-3.6,-0.76 -3.54,-0.75 -3.67,-0.75c-0.31,0 -0.59,0.13 -0.79,0.33l-0.79,0.8l4.94,4.94C9.96,23.83 10.34,24 10.75,24h6.79c0.75,0 1.33,-0.55 1.44,-1.28l0.75,-5.27c0.01,-0.07 0.02,-0.14 0.02,-0.2C19.75,16.63 19.37,16.09 18.84,15.87z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,5h-2V3H7v2H5C3.9,5 3,5.9 3,7v1c0,2.55 1.92,4.63 4.39,4.94c0.63,1.5 1.98,2.63 3.61,2.96V19H7v2h10v-2h-4v-3.1c1.63,-0.33 2.98,-1.46 3.61,-2.96C19.08,12.63 21,10.55 21,8V7C21,5.9 20.1,5 19,5zM5,8V7h2v3.82C5.84,10.4 5,9.3 5,8zM19,8c0,1.3 -0.84,2.4 -2,2.82V7h2V8z"/>
</vector>

View file

@ -0,0 +1,107 @@
<LinearLayout 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="match_parent"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#99111111"
android:orientation="horizontal">
<ImageView
android:id="@+id/cover_image"
android:layout_width="60dp"
android:layout_height="60dp"
android:background="?android:attr/selectableItemBackground"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="4dp"
android:layout_marginTop="4dp"
android:text="Title"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:scrollHorizontally="true"
android:ellipsize="end"
android:maxLines="1"
app:layout_constraintEnd_toStartOf="@id/button_container"
app:layout_constraintStart_toEndOf="@id/cover_image"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp"
android:text="Code - Path"
android:scrollHorizontally="true"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintEnd_toStartOf="@id/button_container"
app:layout_constraintStart_toEndOf="@id/cover_image"
app:layout_constraintTop_toBottomOf="@id/title" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:id="@+id/button_container"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/menu"
android:layout_width="40dp"
android:layout_height="60dp"
android:layout_marginEnd="5dp"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Pause Menu"
android:src="@drawable/ic_baseline_menu_24" />
<ImageButton
android:id="@+id/controller_settings"
android:layout_width="40dp"
android:layout_height="60dp"
android:layout_marginEnd="5dp"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Controller Settings"
android:src="@drawable/ic_baseline_gamepad_24" />
<ImageButton
android:id="@+id/settings"
android:layout_width="40dp"
android:layout_height="60dp"
android:layout_marginEnd="5dp"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Settings"
android:src="@drawable/ic_baseline_settings_24" />
<ImageButton
android:id="@+id/quit"
android:layout_width="40dp"
android:layout_height="60dp"
android:layout_marginEnd="5dp"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Quit"
android:src="@drawable/ic_baseline_exit_to_app_24" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View file

@ -75,22 +75,6 @@
<item>Tarjeta separada por juego (Código)</item>
<item>Tarjeta separada por juego (Título)</item>
</string-array>
<string-array name="emulation_menu">
<item>Cargar Estado</item>
<item>Guardar Estado</item>
<item>Activar Avance rápido</item>
<item>Logros</item>
<item>Más Opciones</item>
<item>Salir</item>
</string-array>
<string-array name="emulation_more_menu">
<item>Reiniciar</item>
<item>Código de trucos</item>
<item>Cambiar disco</item>
<item>Cambiar control de pantalla tactil</item>
<item>Configuración del control</item>
<item>Configuraciones</item>
</string-array>
<string-array name="settings_cdrom_read_speedup_entries">
<item>Ninguno (Velocidad doble)</item>
<item>2x (Velocidad cuádruple)</item>

View file

@ -75,22 +75,6 @@
<item>MC separata per ogni gioco (Codice Gioco)</item>
<item>MC separata per ogni gioco (Titolo Gioco)</item>
</string-array>
<string-array name="emulation_menu">
<item>Carica Stato</item>
<item>Salva Stato</item>
<item>Abilita/Disabilita Avanti Veloce</item>
<item>Achievements</item>
<item>Altre Opzioni</item>
<item>Esci</item>
</string-array>
<string-array name="emulation_more_menu">
<item>Reset</item>
<item>Codici Patch</item>
<item>Cambia Disco</item>
<item>Cambia Controller Touchscreen</item>
<item>Controller Settings</item>
<item>Impostazioni</item>
</string-array>
<string-array name="settings_cdrom_read_speedup_entries">
<item>Nessuna Velocità Doppia)</item>
<item>2x (Velocità Quadrupla</item>

View file

@ -75,22 +75,6 @@
<item>Aparte Kaart Per Spel (Spelcode)</item>
<item>Aparte Kaart Per Spel (Speltitel)</item>
</string-array>
<string-array name="emulation_menu">
<item>Staat Laden</item>
<item>Staat Opslaan</item>
<item>Doorspoelen aan/uitzetten</item>
<item>Achievements</item>
<item>Meer Opties</item>
<item>Afsluiten</item>
</string-array>
<string-array name="emulation_more_menu">
<item>Resetten</item>
<item>Patch Codes</item>
<item>Disc Veranderen</item>
<item>Touchscreen Controller Aanpassen</item>
<item>Controller Settings</item>
<item>Instellingen</item>
</string-array>
<string-array name="settings_cdrom_read_speedup_entries">
<item>Geen (Dubbele Snelheid)</item>
<item>2x (Vierdubbele Snelheid)</item>

View file

@ -75,23 +75,7 @@
<item>Separado Por Jogo (Cód. Jogo)</item>
<item>Separado Por Jogo (Título Jogo)</item>
</string-array>
<string-array name="emulation_menu">
<item>Carregar Estado</item>
<item>Salvar Estado</item>
<item>Avanço (Fixo)</item>
<item>Achievements</item>
<item>Mais Opções</item>
<item>Sair</item>
</string-array>
<string-array name="emulation_more_menu">
<item>Reiniciar</item>
<item>Trapaças</item>
<item>Mudar Disco</item>
<item>Mudar controle em Tela</item>
<item>Controller Settings</item>
<item>Configurações</item>
</string-array>
<string-array name="emulation_touchscreen_menu">
<string-array name="emulation_touchscreen_menu">
<item>Mudar</item>
<item>Ajustar Visibilidade</item>
<item>Adicionar/Remover botões</item>

View file

@ -75,22 +75,6 @@
<item>Своя карта для каждой игры (по коду)</item>
<item>Своя карта для каждой игры (по названию)</item>
</string-array>
<string-array name="emulation_menu">
<item>Загрузить состояние</item>
<item>Сохранить состояние</item>
<item>Включить ускорение</item>
<item>Достижения</item>
<item>Опции</item>
<item>Выход</item>
</string-array>
<string-array name="emulation_more_menu">
<item>Сброс</item>
<item>Чит-коды</item>
<item>Сменить диск</item>
<item>Экранный геймпад</item>
<item>Управление</item>
<item>Настройки</item>
</string-array>
<string-array name="emulation_touchscreen_menu">
<item>Сменить вид</item>
<item>Настроить видимость</item>

View file

@ -151,22 +151,6 @@
<item>PerGame</item>
<item>PerGameTitle</item>
</string-array>
<string-array name="emulation_menu">
<item>Load State</item>
<item>Save State</item>
<item>Toggle Fast Forward</item>
<item>Achievements</item>
<item>More Options</item>
<item>Quit</item>
</string-array>
<string-array name="emulation_more_menu">
<item>Reset</item>
<item>Patch Codes</item>
<item>Change Disc</item>
<item>Change Touchscreen Controller</item>
<item>Controller Settings</item>
<item>Settings</item>
</string-array>
<string-array name="emulation_touchscreen_menu">
<item>Change Type</item>
<item>Change Opacity</item>

View file

@ -7,4 +7,6 @@
<color name="black_overlay">#66000000</color>
<color name="fab_background">#ffffffff</color>
<color name="settings_overlay_background">#dd111111</color>
</resources>

View file

@ -336,4 +336,13 @@
<string name="settings_category_achievements">Achievement Settings</string>
<string name="settings_multitap_mode">Multitap Mode</string>
<string name="settings_touchscreen_controller_port">Touchscreen Controller Port</string>
<string name="emulation_menu_load_state">Load State</string>
<string name="emulation_menu_save_state">Save State</string>
<string name="emulation_menu_toggle_fast_forward">Toggle Fast Forward</string>
<string name="emulation_menu_achievements">Achievements</string>
<string name="emulation_menu_patch_codes">Patch Codes</string>
<string name="emulation_menu_change_disc">Change Disc</string>
<string name="emulation_menu_touchscreen_controller_settings">Touchscreen Controller Settings</string>
<string name="emulation_menu_toggle_analog_mode">Toggle Controller Analog Mode</string>
<string name="emulation_menu_reset_console">Reset Console</string>
</resources>

View file

@ -35,4 +35,13 @@
<item name="android:textAllCaps">false</item>
</style>
<style name="EmulationActivityOverlay" parent="Theme.AppCompat.DayNight.DarkActionBar">
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:backgroundDimAmount">0.8</item>
</style>
</resources>