From 70aae8921963d83145ca5a411321e61d97ccd017 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 2 May 2021 14:59:58 +1000 Subject: [PATCH] Android: New/improved ingame pause menu --- .../app/src/cpp/android_host_interface.cpp | 30 +- .../duckstation/ControllerAutoMapper.java | 21 +- .../ControllerSettingsActivity.java | 383 +---------------- .../ControllerSettingsCollectionFragment.java | 393 ++++++++++++++++++ .../duckstation/EmulationActivity.java | 315 ++++++++------ .../stenzek/duckstation/SettingsActivity.java | 82 ---- .../SettingsCollectionFragment.java | 90 ++++ .../res/drawable/ic_baseline_album_24.xml | 10 + .../drawable/ic_baseline_arrow_back_24.xml | 11 + .../drawable/ic_baseline_exit_to_app_24.xml | 11 + .../drawable/ic_baseline_fast_forward_24.xml | 10 + .../drawable/ic_baseline_fast_rewind_24.xml | 10 + .../main/res/drawable/ic_baseline_menu_24.xml | 10 + .../drawable/ic_baseline_restart_alt_24.xml | 13 + .../ic_baseline_tips_and_updates_24.xml | 10 + .../res/drawable/ic_baseline_touch_app_24.xml | 10 + .../res/drawable/ic_baseline_trophy_24.xml | 10 + .../fragment_emulation_activity_overlay.xml | 107 +++++ android/app/src/main/res/values-es/arrays.xml | 16 - android/app/src/main/res/values-it/arrays.xml | 16 - android/app/src/main/res/values-nl/arrays.xml | 16 - .../app/src/main/res/values-pt-rBR/arrays.xml | 18 +- android/app/src/main/res/values-ru/arrays.xml | 16 - android/app/src/main/res/values/arrays.xml | 16 - android/app/src/main/res/values/colors.xml | 2 + android/app/src/main/res/values/strings.xml | 9 + android/app/src/main/res/values/styles.xml | 9 + 27 files changed, 951 insertions(+), 693 deletions(-) create mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/ControllerSettingsCollectionFragment.java create mode 100644 android/app/src/main/java/com/github/stenzek/duckstation/SettingsCollectionFragment.java create mode 100644 android/app/src/main/res/drawable/ic_baseline_album_24.xml create mode 100644 android/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml create mode 100644 android/app/src/main/res/drawable/ic_baseline_exit_to_app_24.xml create mode 100644 android/app/src/main/res/drawable/ic_baseline_fast_forward_24.xml create mode 100644 android/app/src/main/res/drawable/ic_baseline_fast_rewind_24.xml create mode 100644 android/app/src/main/res/drawable/ic_baseline_menu_24.xml create mode 100644 android/app/src/main/res/drawable/ic_baseline_restart_alt_24.xml create mode 100644 android/app/src/main/res/drawable/ic_baseline_tips_and_updates_24.xml create mode 100644 android/app/src/main/res/drawable/ic_baseline_touch_app_24.xml create mode 100644 android/app/src/main/res/drawable/ic_baseline_trophy_24.xml create mode 100644 android/app/src/main/res/layout/fragment_emulation_activity_overlay.xml diff --git a/android/app/src/cpp/android_host_interface.cpp b/android/app/src/cpp/android_host_interface.cpp index c68d535d1..1928a73d7 100644 --- a/android/app/src/cpp/android_host_interface.cpp +++ b/android/app/src/cpp/android_host_interface.cpp @@ -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 = diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerAutoMapper.java b/android/app/src/main/java/com/github/stenzek/duckstation/ControllerAutoMapper.java index ae846ad79..175fb3cb3 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerAutoMapper.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/ControllerAutoMapper.java @@ -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); diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerSettingsActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/ControllerSettingsActivity.java index 7cca19c21..fcc5dd7a4 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerSettingsActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/ControllerSettingsActivity.java @@ -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 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 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 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; - } - } } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/ControllerSettingsCollectionFragment.java b/android/app/src/main/java/com/github/stenzek/duckstation/ControllerSettingsCollectionFragment.java new file mode 100644 index 000000000..90c04aa8c --- /dev/null +++ b/android/app/src/main/java/com/github/stenzek/duckstation/ControllerSettingsCollectionFragment.java @@ -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 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 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 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; + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java index b16865042..9ea85bc93 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java @@ -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); + } + } } diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/SettingsActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/SettingsActivity.java index 16d879a7f..538a58cec 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/SettingsActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/SettingsActivity.java @@ -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; - } - } } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/SettingsCollectionFragment.java b/android/app/src/main/java/com/github/stenzek/duckstation/SettingsCollectionFragment.java new file mode 100644 index 000000000..b5aa1138b --- /dev/null +++ b/android/app/src/main/java/com/github/stenzek/duckstation/SettingsCollectionFragment.java @@ -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; + } + } +} diff --git a/android/app/src/main/res/drawable/ic_baseline_album_24.xml b/android/app/src/main/res/drawable/ic_baseline_album_24.xml new file mode 100644 index 000000000..58c0ba697 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_album_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml b/android/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml new file mode 100644 index 000000000..2a31b2ef3 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_baseline_exit_to_app_24.xml b/android/app/src/main/res/drawable/ic_baseline_exit_to_app_24.xml new file mode 100644 index 000000000..ed42b700b --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_exit_to_app_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_baseline_fast_forward_24.xml b/android/app/src/main/res/drawable/ic_baseline_fast_forward_24.xml new file mode 100644 index 000000000..e3f30c6c7 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_fast_forward_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_baseline_fast_rewind_24.xml b/android/app/src/main/res/drawable/ic_baseline_fast_rewind_24.xml new file mode 100644 index 000000000..81f79b322 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_fast_rewind_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_baseline_menu_24.xml b/android/app/src/main/res/drawable/ic_baseline_menu_24.xml new file mode 100644 index 000000000..4350ba96a --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_menu_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_baseline_restart_alt_24.xml b/android/app/src/main/res/drawable/ic_baseline_restart_alt_24.xml new file mode 100644 index 000000000..2f9f24ca9 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_restart_alt_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_baseline_tips_and_updates_24.xml b/android/app/src/main/res/drawable/ic_baseline_tips_and_updates_24.xml new file mode 100644 index 000000000..93d588228 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_tips_and_updates_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_baseline_touch_app_24.xml b/android/app/src/main/res/drawable/ic_baseline_touch_app_24.xml new file mode 100644 index 000000000..27f451658 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_touch_app_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_baseline_trophy_24.xml b/android/app/src/main/res/drawable/ic_baseline_trophy_24.xml new file mode 100644 index 000000000..dbf6358f8 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_trophy_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/layout/fragment_emulation_activity_overlay.xml b/android/app/src/main/res/layout/fragment_emulation_activity_overlay.xml new file mode 100644 index 000000000..f7e8627fb --- /dev/null +++ b/android/app/src/main/res/layout/fragment_emulation_activity_overlay.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values-es/arrays.xml b/android/app/src/main/res/values-es/arrays.xml index 6cc05fdfb..491182118 100644 --- a/android/app/src/main/res/values-es/arrays.xml +++ b/android/app/src/main/res/values-es/arrays.xml @@ -75,22 +75,6 @@ Tarjeta separada por juego (Código) Tarjeta separada por juego (Título) - - Cargar Estado - Guardar Estado - Activar Avance rápido - Logros - Más Opciones - Salir - - - Reiniciar - Código de trucos - Cambiar disco - Cambiar control de pantalla tactil - Configuración del control - Configuraciones - Ninguno (Velocidad doble) 2x (Velocidad cuádruple) diff --git a/android/app/src/main/res/values-it/arrays.xml b/android/app/src/main/res/values-it/arrays.xml index 49d62453d..c53b6106d 100644 --- a/android/app/src/main/res/values-it/arrays.xml +++ b/android/app/src/main/res/values-it/arrays.xml @@ -75,22 +75,6 @@ MC separata per ogni gioco (Codice Gioco) MC separata per ogni gioco (Titolo Gioco) - - Carica Stato - Salva Stato - Abilita/Disabilita Avanti Veloce - Achievements - Altre Opzioni - Esci - - - Reset - Codici Patch - Cambia Disco - Cambia Controller Touchscreen - Controller Settings - Impostazioni - Nessuna Velocità Doppia) 2x (Velocità Quadrupla diff --git a/android/app/src/main/res/values-nl/arrays.xml b/android/app/src/main/res/values-nl/arrays.xml index 1a4d85040..93c3f6a10 100644 --- a/android/app/src/main/res/values-nl/arrays.xml +++ b/android/app/src/main/res/values-nl/arrays.xml @@ -75,22 +75,6 @@ Aparte Kaart Per Spel (Spelcode) Aparte Kaart Per Spel (Speltitel) - - Staat Laden - Staat Opslaan - Doorspoelen aan/uitzetten - Achievements - Meer Opties - Afsluiten - - - Resetten - Patch Codes - Disc Veranderen - Touchscreen Controller Aanpassen - Controller Settings - Instellingen - Geen (Dubbele Snelheid) 2x (Vierdubbele Snelheid) diff --git a/android/app/src/main/res/values-pt-rBR/arrays.xml b/android/app/src/main/res/values-pt-rBR/arrays.xml index 67fa47521..bc58d2c29 100644 --- a/android/app/src/main/res/values-pt-rBR/arrays.xml +++ b/android/app/src/main/res/values-pt-rBR/arrays.xml @@ -75,23 +75,7 @@ Separado Por Jogo (Cód. Jogo) Separado Por Jogo (Título Jogo) - - Carregar Estado - Salvar Estado - Avanço (Fixo) - Achievements - Mais Opções - Sair - - - Reiniciar - Trapaças - Mudar Disco - Mudar controle em Tela - Controller Settings - Configurações - - + Mudar Ajustar Visibilidade Adicionar/Remover botões diff --git a/android/app/src/main/res/values-ru/arrays.xml b/android/app/src/main/res/values-ru/arrays.xml index c3a1a1b67..aae5d66c7 100644 --- a/android/app/src/main/res/values-ru/arrays.xml +++ b/android/app/src/main/res/values-ru/arrays.xml @@ -75,22 +75,6 @@ Своя карта для каждой игры (по коду) Своя карта для каждой игры (по названию) - - Загрузить состояние - Сохранить состояние - Включить ускорение - Достижения - Опции - Выход - - - Сброс - Чит-коды - Сменить диск - Экранный геймпад - Управление - Настройки - Сменить вид Настроить видимость diff --git a/android/app/src/main/res/values/arrays.xml b/android/app/src/main/res/values/arrays.xml index d484f69d7..49fa76d9e 100644 --- a/android/app/src/main/res/values/arrays.xml +++ b/android/app/src/main/res/values/arrays.xml @@ -151,22 +151,6 @@ PerGame PerGameTitle - - Load State - Save State - Toggle Fast Forward - Achievements - More Options - Quit - - - Reset - Patch Codes - Change Disc - Change Touchscreen Controller - Controller Settings - Settings - Change Type Change Opacity diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index 564f8f716..51a368a41 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -7,4 +7,6 @@ #66000000 #ffffffff + + #dd111111 diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 61da2691d..50708cd3f 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -336,4 +336,13 @@ Achievement Settings Multitap Mode Touchscreen Controller Port + Load State + Save State + Toggle Fast Forward + Achievements + Patch Codes + Change Disc + Touchscreen Controller Settings + Toggle Controller Analog Mode + Reset Console diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 0979905b3..5ceccaa6d 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -35,4 +35,13 @@ false +