From c17ec1d1aa867a6cb302d7f8c86f6929be6c5c40 Mon Sep 17 00:00:00 2001 From: Charles Lombardo <clombardo169@gmail.com> Date: Wed, 22 Nov 2023 17:31:48 -0500 Subject: [PATCH] Android UI Overhaul Part 2 (#7147) --- .../java/org/citra/citra_emu/NativeLibrary.kt | 8 - .../activities/EmulationActivity.java | 6 +- .../citra_emu/dialogs/MotionAlertDialog.java | 140 --- .../settings/model/AbstractBooleanSetting.kt | 9 + .../settings/model/AbstractFloatSetting.kt | 9 + .../settings/model/AbstractIntSetting.kt | 9 + .../settings/model/AbstractSetting.kt | 13 + .../settings/model/AbstractStringSetting.kt | 9 + .../settings/model/BooleanSetting.java | 23 - .../features/settings/model/BooleanSetting.kt | 43 + .../features/settings/model/FloatSetting.java | 23 - .../features/settings/model/FloatSetting.kt | 37 + .../features/settings/model/IntSetting.java | 23 - .../features/settings/model/IntSetting.kt | 75 ++ .../settings/model/ScaledFloatSetting.kt | 41 + .../features/settings/model/Setting.java | 42 - .../settings/model/SettingSection.java | 55 - .../features/settings/model/SettingSection.kt | 38 + .../features/settings/model/Settings.java | 131 --- .../features/settings/model/Settings.kt | 201 ++++ .../settings/model/SettingsViewModel.kt | 11 + .../settings/model/StringSetting.java | 23 - .../features/settings/model/StringSetting.kt | 50 + .../model/view/AbstractShortSetting.kt | 11 + .../settings/model/view/CheckBoxSetting.java | 80 -- .../settings/model/view/DateTimeSetting.java | 40 - .../settings/model/view/DateTimeSetting.kt | 32 + .../settings/model/view/HeaderSetting.java | 14 - .../settings/model/view/HeaderSetting.kt | 9 + .../model/view/InputBindingSetting.java | 382 ------ .../model/view/InputBindingSetting.kt | 299 +++++ .../settings/model/view/RunnableSetting.kt | 15 + .../settings/model/view/SettingsItem.java | 100 -- .../settings/model/view/SettingsItem.kt | 42 + .../model/view/SingleChoiceSetting.java | 60 - .../model/view/SingleChoiceSetting.kt | 60 + .../settings/model/view/SliderSetting.java | 101 -- .../settings/model/view/SliderSetting.kt | 70 ++ .../settings/model/view/StringInputSetting.kt | 27 + .../model/view/StringSingleChoiceSetting.java | 82 -- .../model/view/StringSingleChoiceSetting.kt | 78 ++ .../settings/model/view/SubmenuSetting.java | 21 - .../settings/model/view/SubmenuSetting.kt | 13 + .../settings/model/view/SwitchSetting.kt | 63 + .../settings/ui/SettingsActivity.java | 227 ---- .../features/settings/ui/SettingsActivity.kt | 292 +++++ .../ui/SettingsActivityPresenter.java | 91 -- .../settings/ui/SettingsActivityPresenter.kt | 78 ++ .../settings/ui/SettingsActivityView.java | 87 -- .../settings/ui/SettingsActivityView.kt | 58 + .../features/settings/ui/SettingsAdapter.java | 393 ------- .../features/settings/ui/SettingsAdapter.kt | 503 ++++++++ .../settings/ui/SettingsFragment.java | 151 --- .../features/settings/ui/SettingsFragment.kt | 128 ++ .../ui/SettingsFragmentPresenter.java | 410 ------- .../settings/ui/SettingsFragmentPresenter.kt | 1040 +++++++++++++++++ .../settings/ui/SettingsFragmentView.java | 78 -- .../settings/ui/SettingsFragmentView.kt | 59 + .../viewholder/CheckBoxSettingViewHolder.java | 54 - .../ui/viewholder/DateTimeViewHolder.java | 47 - .../ui/viewholder/DateTimeViewHolder.kt | 77 ++ .../ui/viewholder/HeaderViewHolder.java | 32 - .../ui/viewholder/HeaderViewHolder.kt | 31 + .../InputBindingSettingViewHolder.java | 55 - .../InputBindingSettingViewHolder.kt | 60 + .../ui/viewholder/RunnableViewHolder.kt | 58 + .../ui/viewholder/SettingViewHolder.java | 49 - .../ui/viewholder/SettingViewHolder.kt | 37 + .../ui/viewholder/SingleChoiceViewHolder.java | 62 - .../ui/viewholder/SingleChoiceViewHolder.kt | 94 ++ .../ui/viewholder/SliderViewHolder.java | 46 - .../ui/viewholder/SliderViewHolder.kt | 65 ++ .../ui/viewholder/StringInputViewHolder.kt | 46 + .../ui/viewholder/SubmenuViewHolder.java | 45 - .../ui/viewholder/SubmenuViewHolder.kt | 36 + .../ui/viewholder/SwitchSettingViewHolder.kt | 62 + .../features/settings/utils/SettingsFile.java | 344 ------ .../features/settings/utils/SettingsFile.kt | 258 ++++ .../fragments/HomeSettingsFragment.kt | 7 + .../MotionBottomSheetDialogFragment.kt | 208 ++++ .../fragments/ResetSettingsDialogFragment.kt | 31 + .../fragments/SystemFilesFragment.kt | 10 +- .../citra/citra_emu/ui/main/MainActivity.kt | 18 +- .../citra/citra_emu/ui/main/ThemeProvider.kt | 12 + .../citra/citra_emu/utils/SystemSaveGame.kt | 67 ++ .../org/citra/citra_emu/utils/ThemeUtil.kt | 23 + src/android/app/src/main/jni/CMakeLists.txt | 1 + src/android/app/src/main/jni/config.cpp | 31 +- src/android/app/src/main/jni/config.h | 1 - src/android/app/src/main/jni/native.cpp | 26 - .../app/src/main/jni/system_save_game.cpp | 122 ++ .../app/src/main/res/drawable/ic_palette.xml | 9 + .../src/main/res/layout/activity_settings.xml | 35 +- .../app/src/main/res/layout/dialog_input.xml | 64 + .../res/layout/dialog_software_keyboard.xml | 26 + .../src/main/res/layout/list_item_setting.xml | 67 +- .../res/layout/list_item_setting_switch.xml | 53 + .../res/layout/list_item_settings_header.xml | 29 +- .../res/layout/sysclock_datetime_picker.xml | 22 - .../src/main/res/values-night-v31/themes.xml | 29 + .../app/src/main/res/values-night/themes.xml | 9 + .../app/src/main/res/values-v31/themes.xml | 29 + .../app/src/main/res/values/arrays.xml | 397 +++++-- .../app/src/main/res/values/strings.xml | 268 ++++- 104 files changed, 5613 insertions(+), 3752 deletions(-) delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/dialogs/MotionAlertDialog.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractBooleanSetting.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractFloatSetting.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractIntSetting.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractSetting.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractStringSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/ScaledFloatSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Setting.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/AbstractShortSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/CheckBoxSetting.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringInputSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SwitchSetting.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/fragments/ResetSettingsDialogFragment.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/ui/main/ThemeProvider.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/SystemSaveGame.kt create mode 100644 src/android/app/src/main/jni/system_save_game.cpp create mode 100644 src/android/app/src/main/res/drawable/ic_palette.xml create mode 100644 src/android/app/src/main/res/layout/dialog_input.xml create mode 100644 src/android/app/src/main/res/layout/dialog_software_keyboard.xml create mode 100644 src/android/app/src/main/res/layout/list_item_setting_switch.xml delete mode 100644 src/android/app/src/main/res/layout/sysclock_datetime_picker.xml create mode 100644 src/android/app/src/main/res/values-night-v31/themes.xml create mode 100644 src/android/app/src/main/res/values-night/themes.xml create mode 100644 src/android/app/src/main/res/values-v31/themes.xml diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt index ebcfa1933e..3a357ed319 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt @@ -515,14 +515,6 @@ object NativeLibrary { */ external fun logDeviceInfo() - external fun loadSystemConfig() - - external fun saveSystemConfig() - - external fun setSystemSetupNeeded(needed: Boolean) - - external fun getIsSystemSetupNeeded(): Boolean - @Keep @JvmStatic fun createFile(directory: String, filename: String): Boolean = diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java index 6a6075782c..2f631b61ee 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java @@ -535,7 +535,7 @@ public final class EmulationActivity extends AppCompatActivity { @Override public boolean dispatchKeyEvent(KeyEvent event) { int action; - int button = mPreferences.getInt(InputBindingSetting.getInputButtonKey(event.getKeyCode()), event.getKeyCode()); + int button = mPreferences.getInt(InputBindingSetting.Companion.getInputButtonKey(event.getKeyCode()), event.getKeyCode()); switch (event.getAction()) { case KeyEvent.ACTION_DOWN: @@ -693,8 +693,8 @@ public final class EmulationActivity extends AppCompatActivity { int axis = range.getAxis(); float origValue = event.getAxisValue(axis); float value = mControllerMappingHelper.scaleAxis(input, axis, origValue); - int nextMapping = mPreferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1); - int guestOrientation = mPreferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1); + int nextMapping = mPreferences.getInt(InputBindingSetting.Companion.getInputAxisButtonKey(axis), -1); + int guestOrientation = mPreferences.getInt(InputBindingSetting.Companion.getInputAxisOrientationKey(axis), -1); if (nextMapping == -1 || guestOrientation == -1) { // Axis is unmapped diff --git a/src/android/app/src/main/java/org/citra/citra_emu/dialogs/MotionAlertDialog.java b/src/android/app/src/main/java/org/citra/citra_emu/dialogs/MotionAlertDialog.java deleted file mode 100644 index 0f10f1858e..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/dialogs/MotionAlertDialog.java +++ /dev/null @@ -1,140 +0,0 @@ -package org.citra.citra_emu.dialogs; - -import android.content.Context; -import android.view.InputDevice; -import android.view.KeyEvent; -import android.view.MotionEvent; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; - -import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; -import org.citra.citra_emu.utils.Log; - -import java.util.ArrayList; -import java.util.List; - -/** - * {@link AlertDialog} derivative that listens for - * motion events from controllers and joysticks. - */ -public final class MotionAlertDialog extends AlertDialog { - // The selected input preference - private final InputBindingSetting setting; - private final ArrayList<Float> mPreviousValues = new ArrayList<>(); - private int mPrevDeviceId = 0; - private boolean mWaitingForEvent = true; - - /** - * Constructor - * - * @param context The current {@link Context}. - * @param setting The Preference to show this dialog for. - */ - public MotionAlertDialog(Context context, InputBindingSetting setting) { - super(context); - - this.setting = setting; - } - - public boolean onKeyEvent(int keyCode, KeyEvent event) { - Log.debug("[MotionAlertDialog] Received key event: " + event.getAction()); - switch (event.getAction()) { - case KeyEvent.ACTION_UP: - setting.onKeyInput(event); - dismiss(); - // Even if we ignore the key, we still consume it. Thus return true regardless. - return true; - - default: - return false; - } - } - - @Override - public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) { - return super.onKeyLongPress(keyCode, event); - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - // Handle this key if we care about it, otherwise pass it down the framework - return onKeyEvent(event.getKeyCode(), event) || super.dispatchKeyEvent(event); - } - - @Override - public boolean dispatchGenericMotionEvent(@NonNull MotionEvent event) { - // Handle this event if we care about it, otherwise pass it down the framework - return onMotionEvent(event) || super.dispatchGenericMotionEvent(event); - } - - private boolean onMotionEvent(MotionEvent event) { - if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0) - return false; - if (event.getAction() != MotionEvent.ACTION_MOVE) - return false; - - InputDevice input = event.getDevice(); - - List<InputDevice.MotionRange> motionRanges = input.getMotionRanges(); - - if (input.getId() != mPrevDeviceId) { - mPreviousValues.clear(); - } - mPrevDeviceId = input.getId(); - boolean firstEvent = mPreviousValues.isEmpty(); - - int numMovedAxis = 0; - float axisMoveValue = 0.0f; - InputDevice.MotionRange lastMovedRange = null; - char lastMovedDir = '?'; - if (mWaitingForEvent) { - for (int i = 0; i < motionRanges.size(); i++) { - InputDevice.MotionRange range = motionRanges.get(i); - int axis = range.getAxis(); - float origValue = event.getAxisValue(axis); - float value = origValue;//ControllerMappingHelper.scaleAxis(input, axis, origValue); - if (firstEvent) { - mPreviousValues.add(value); - } else { - float previousValue = mPreviousValues.get(i); - - // Only handle the axes that are not neutral (more than 0.5) - // but ignore any axis that has a constant value (e.g. always 1) - if (Math.abs(value) > 0.5f && value != previousValue) { - // It is common to have multiple axes with the same physical input. For example, - // shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE. - // To handle this, we ignore an axis motion that's the exact same as a motion - // we already saw. This way, we ignore axes with two names, but catch the case - // where a joystick is moved in two directions. - // ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html - if (value != axisMoveValue) { - axisMoveValue = value; - numMovedAxis++; - lastMovedRange = range; - lastMovedDir = value < 0.0f ? '-' : '+'; - } - } - // Special case for d-pads (axis value jumps between 0 and 1 without any values - // in between). Without this, the user would need to press the d-pad twice - // due to the first press being caught by the "if (firstEvent)" case further up. - else if (Math.abs(value) < 0.25f && Math.abs(previousValue) > 0.75f) { - numMovedAxis++; - lastMovedRange = range; - lastMovedDir = previousValue < 0.0f ? '-' : '+'; - } - } - - mPreviousValues.set(i, value); - } - - // If only one axis moved, that's the winner. - if (numMovedAxis == 1) { - mWaitingForEvent = false; - setting.onMotionInput(input, lastMovedRange, lastMovedDir); - dismiss(); - } - } - return true; - } -} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractBooleanSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractBooleanSetting.kt new file mode 100644 index 0000000000..e60b1ca36b --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractBooleanSetting.kt @@ -0,0 +1,9 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +interface AbstractBooleanSetting : AbstractSetting { + var boolean: Boolean +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractFloatSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractFloatSetting.kt new file mode 100644 index 0000000000..c3b2c8e2e6 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractFloatSetting.kt @@ -0,0 +1,9 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +interface AbstractFloatSetting : AbstractSetting { + var float: Float +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractIntSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractIntSetting.kt new file mode 100644 index 0000000000..7c36608547 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractIntSetting.kt @@ -0,0 +1,9 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +interface AbstractIntSetting : AbstractSetting { + var int: Int +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractSetting.kt new file mode 100644 index 0000000000..54af79efb0 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractSetting.kt @@ -0,0 +1,13 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +interface AbstractSetting { + val key: String? + val section: String? + val isRuntimeEditable: Boolean + val valueAsString: String + val defaultValue: Any +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractStringSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractStringSetting.kt new file mode 100644 index 0000000000..41ecc50381 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractStringSetting.kt @@ -0,0 +1,9 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +interface AbstractStringSetting : AbstractSetting { + var string: String +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.java deleted file mode 100644 index 932dcf1d32..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.citra.citra_emu.features.settings.model; - -public final class BooleanSetting extends Setting { - private boolean mValue; - - public BooleanSetting(String key, String section, boolean value) { - super(key, section); - mValue = value; - } - - public boolean getValue() { - return mValue; - } - - public void setValue(boolean value) { - mValue = value; - } - - @Override - public String getValueAsString() { - return mValue ? "True" : "False"; - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt new file mode 100644 index 0000000000..28b026f918 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt @@ -0,0 +1,43 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +enum class BooleanSetting( + override val key: String, + override val section: String, + override val defaultValue: Boolean +) : AbstractBooleanSetting { + SPIRV_SHADER_GEN("spirv_shader_gen", Settings.SECTION_RENDERER, true), + ASYNC_SHADERS("async_shader_compilation", Settings.SECTION_RENDERER, false), + PLUGIN_LOADER("plugin_loader", Settings.SECTION_SYSTEM, false), + ALLOW_PLUGIN_LOADER("allow_plugin_loader", Settings.SECTION_SYSTEM, true); + + override var boolean: Boolean = defaultValue + + override val valueAsString: String + get() = boolean.toString() + + override val isRuntimeEditable: Boolean + get() { + for (setting in NOT_RUNTIME_EDITABLE) { + if (setting == this) { + return false + } + } + return true + } + + companion object { + private val NOT_RUNTIME_EDITABLE = listOf( + PLUGIN_LOADER, + ALLOW_PLUGIN_LOADER + ) + + fun from(key: String): BooleanSetting? = + BooleanSetting.values().firstOrNull { it.key == key } + + fun clear() = BooleanSetting.values().forEach { it.boolean = it.defaultValue } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.java deleted file mode 100644 index 275f0eceae..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.citra.citra_emu.features.settings.model; - -public final class FloatSetting extends Setting { - private float mValue; - - public FloatSetting(String key, String section, float value) { - super(key, section); - mValue = value; - } - - public float getValue() { - return mValue; - } - - public void setValue(float value) { - mValue = value; - } - - @Override - public String getValueAsString() { - return Float.toString(mValue); - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.kt new file mode 100644 index 0000000000..81c5dbbf84 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.kt @@ -0,0 +1,37 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +enum class FloatSetting( + override val key: String, + override val section: String, + override val defaultValue: Float +) : AbstractFloatSetting { + // There are no float settings currently + EMPTY_SETTING("", "", 0.0f); + + override var float: Float = defaultValue + + override val valueAsString: String + get() = float.toString() + + override val isRuntimeEditable: Boolean + get() { + for (setting in NOT_RUNTIME_EDITABLE) { + if (setting == this) { + return false + } + } + return true + } + + companion object { + private val NOT_RUNTIME_EDITABLE = emptyList<FloatSetting>() + + fun from(key: String): FloatSetting? = FloatSetting.values().firstOrNull { it.key == key } + + fun clear() = FloatSetting.values().forEach { it.float = it.defaultValue } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.java deleted file mode 100644 index f712e5bfa4..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.citra.citra_emu.features.settings.model; - -public final class IntSetting extends Setting { - private int mValue; - - public IntSetting(String key, String section, int value) { - super(key, section); - mValue = value; - } - - public int getValue() { - return mValue; - } - - public void setValue(int value) { - mValue = value; - } - - @Override - public String getValueAsString() { - return Integer.toString(mValue); - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt new file mode 100644 index 0000000000..12d0b4ae42 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt @@ -0,0 +1,75 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +enum class IntSetting( + override val key: String, + override val section: String, + override val defaultValue: Int +) : AbstractIntSetting { + FRAME_LIMIT("frame_limit", Settings.SECTION_RENDERER, 100), + EMULATED_REGION("region_value", Settings.SECTION_SYSTEM, -1), + INIT_CLOCK("init_clock", Settings.SECTION_SYSTEM, 0), + CAMERA_INNER_FLIP("camera_inner_flip", Settings.SECTION_CAMERA, 0), + CAMERA_OUTER_LEFT_FLIP("camera_outer_left_flip", Settings.SECTION_CAMERA, 0), + CAMERA_OUTER_RIGHT_FLIP("camera_outer_right_flip", Settings.SECTION_CAMERA, 0), + GRAPHICS_API("graphics_api", Settings.SECTION_RENDERER, 1), + RESOLUTION_FACTOR("resolution_factor", Settings.SECTION_RENDERER, 1), + STEREOSCOPIC_3D_MODE("render_3d", Settings.SECTION_RENDERER, 0), + STEREOSCOPIC_3D_DEPTH("factor_3d", Settings.SECTION_RENDERER, 0), + CARDBOARD_SCREEN_SIZE("cardboard_screen_size", Settings.SECTION_LAYOUT, 85), + CARDBOARD_X_SHIFT("cardboard_x_shift", Settings.SECTION_LAYOUT, 0), + CARDBOARD_Y_SHIFT("cardboard_y_shift", Settings.SECTION_LAYOUT, 0), + AUDIO_INPUT_TYPE("output_type", Settings.SECTION_AUDIO, 0), + NEW_3DS("is_new_3ds", Settings.SECTION_SYSTEM, 1), + CPU_CLOCK_SPEED("cpu_clock_percentage", Settings.SECTION_CORE, 100), + LINEAR_FILTERING("filter_mode", Settings.SECTION_RENDERER, 1), + SHADERS_ACCURATE_MUL("shaders_accurate_mul", Settings.SECTION_RENDERER, 0), + DISK_SHADER_CACHE("use_disk_shader_cache", Settings.SECTION_RENDERER, 1), + DUMP_TEXTURES("dump_textures", Settings.SECTION_UTILITY, 0), + CUSTOM_TEXTURES("custom_textures", Settings.SECTION_UTILITY, 0), + ASYNC_CUSTOM_LOADING("async_custom_loading", Settings.SECTION_UTILITY, 1), + PRELOAD_TEXTURES("preload_textures", Settings.SECTION_UTILITY, 0), + ENABLE_AUDIO_STRETCHING("enable_audio_stretching", Settings.SECTION_AUDIO, 1), + CPU_JIT("use_cpu_jit", Settings.SECTION_CORE, 1), + HW_SHADER("use_hw_shader", Settings.SECTION_RENDERER, 1), + VSYNC("use_vsync_new", Settings.SECTION_RENDERER, 1), + DEBUG_RENDERER("renderer_debug", Settings.SECTION_DEBUG, 0), + TEXTURE_FILTER("texture_filter", Settings.SECTION_RENDERER, 0), + USE_FRAME_LIMIT("use_frame_limit", Settings.SECTION_RENDERER, 1); + + override var int: Int = defaultValue + + override val valueAsString: String + get() = int.toString() + + override val isRuntimeEditable: Boolean + get() { + for (setting in NOT_RUNTIME_EDITABLE) { + if (setting == this) { + return false + } + } + return true + } + + companion object { + private val NOT_RUNTIME_EDITABLE = listOf( + EMULATED_REGION, + INIT_CLOCK, + NEW_3DS, + GRAPHICS_API, + VSYNC, + DEBUG_RENDERER, + CPU_JIT, + ASYNC_CUSTOM_LOADING, + AUDIO_INPUT_TYPE + ) + + fun from(key: String): IntSetting? = IntSetting.values().firstOrNull { it.key == key } + + fun clear() = IntSetting.values().forEach { it.int = it.defaultValue } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/ScaledFloatSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/ScaledFloatSetting.kt new file mode 100644 index 0000000000..21629f7b0a --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/ScaledFloatSetting.kt @@ -0,0 +1,41 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +enum class ScaledFloatSetting( + override val key: String, + override val section: String, + override val defaultValue: Float, + val scale: Int +) : AbstractFloatSetting { + AUDIO_VOLUME("volume", Settings.SECTION_AUDIO, 1.0f, 100); + + override var float: Float = defaultValue + get() = field * scale + set(value) { + field = value / scale + } + + override val valueAsString: String get() = (float / scale).toString() + + override val isRuntimeEditable: Boolean + get() { + for (setting in NOT_RUNTIME_EDITABLE) { + if (setting == this) { + return false + } + } + return true + } + + companion object { + private val NOT_RUNTIME_EDITABLE = emptyList<ScaledFloatSetting>() + + fun from(key: String): ScaledFloatSetting? = + ScaledFloatSetting.values().firstOrNull { it.key == key } + + fun clear() = ScaledFloatSetting.values().forEach { it.float = it.defaultValue * it.scale } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Setting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Setting.java deleted file mode 100644 index b762847c94..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Setting.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.citra.citra_emu.features.settings.model; - -/** - * Abstraction for a setting item as read from / written to Citra's configuration ini files. - * These files generally consist of a key/value pair, though the type of value is ambiguous and - * must be inferred at read-time. The type of value determines which child of this class is used - * to represent the Setting. - */ -public abstract class Setting { - private String mKey; - private String mSection; - - /** - * Base constructor. - * - * @param key Everything to the left of the = in a line from the ini file. - * @param section The corresponding recent section header; e.g. [Core] or [Enhancements] without the brackets. - */ - public Setting(String key, String section) { - mKey = key; - mSection = section; - } - - /** - * @return The identifier used to write this setting to the ini file. - */ - public String getKey() { - return mKey; - } - - /** - * @return The name of the header under which this Setting should be written in the ini file. - */ - public String getSection() { - return mSection; - } - - /** - * @return A representation of this Setting's backing value converted to a String (e.g. for serialization). - */ - public abstract String getValueAsString(); -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.java deleted file mode 100644 index 0a291aa6bb..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.citra.citra_emu.features.settings.model; - -import java.util.HashMap; - -/** - * A semantically-related group of Settings objects. These Settings are - * internally stored as a HashMap. - */ -public final class SettingSection { - private String mName; - - private HashMap<String, Setting> mSettings = new HashMap<>(); - - /** - * Create a new SettingSection with no Settings in it. - * - * @param name The header of this section; e.g. [Core] or [Enhancements] without the brackets. - */ - public SettingSection(String name) { - mName = name; - } - - public String getName() { - return mName; - } - - /** - * Convenience method; inserts a value directly into the backing HashMap. - * - * @param setting The Setting to be inserted. - */ - public void putSetting(Setting setting) { - mSettings.put(setting.getKey(), setting); - } - - /** - * Convenience method; gets a value directly from the backing HashMap. - * - * @param key Used to retrieve the Setting. - * @return A Setting object (you should probably cast this before using) - */ - public Setting getSetting(String key) { - return mSettings.get(key); - } - - public HashMap<String, Setting> getSettings() { - return mSettings; - } - - public void mergeSection(SettingSection settingSection) { - for (Setting setting : settingSection.mSettings.values()) { - putSetting(setting); - } - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.kt new file mode 100644 index 0000000000..02c5fa2d56 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.kt @@ -0,0 +1,38 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +/** + * A semantically-related group of Settings objects. These Settings are + * internally stored as a HashMap. + */ +class SettingSection(val name: String) { + val settings = HashMap<String, AbstractSetting>() + + /** + * Convenience method; inserts a value directly into the backing HashMap. + * + * @param setting The Setting to be inserted. + */ + fun putSetting(setting: AbstractSetting) { + settings[setting.key!!] = setting + } + + /** + * Convenience method; gets a value directly from the backing HashMap. + * + * @param key Used to retrieve the Setting. + * @return A Setting object (you should probably cast this before using) + */ + fun getSetting(key: String): AbstractSetting? { + return settings[key] + } + + fun mergeSection(settingSection: SettingSection) { + for (setting in settingSection.settings.values) { + putSetting(setting) + } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.java deleted file mode 100644 index 997dd1e26c..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.java +++ /dev/null @@ -1,131 +0,0 @@ -package org.citra.citra_emu.features.settings.model; - -import android.text.TextUtils; - -import org.citra.citra_emu.CitraApplication; -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.ui.SettingsActivityView; -import org.citra.citra_emu.features.settings.utils.SettingsFile; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -public class Settings { - public static final String PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"; - public static final String PREF_MATERIAL_YOU = "MaterialYouTheme"; - public static final String PREF_THEME_MODE = "ThemeMode"; - public static final String PREF_BLACK_BACKGROUNDS = "BlackBackgrounds"; - public static final String PREF_SHOW_HOME_APPS = "ShowHomeApps"; - - public static final String SECTION_CORE = "Core"; - public static final String SECTION_SYSTEM = "System"; - public static final String SECTION_CAMERA = "Camera"; - public static final String SECTION_CONTROLS = "Controls"; - public static final String SECTION_RENDERER = "Renderer"; - public static final String SECTION_LAYOUT = "Layout"; - public static final String SECTION_UTILITY = "Utility"; - public static final String SECTION_AUDIO = "Audio"; - public static final String SECTION_DEBUG = "Debug"; - - private String gameId; - - private static final Map<String, List<String>> configFileSectionsMap = new HashMap<>(); - - static { - configFileSectionsMap.put(SettingsFile.FILE_NAME_CONFIG, Arrays.asList(SECTION_CORE, SECTION_SYSTEM, SECTION_CAMERA, SECTION_CONTROLS, SECTION_RENDERER, SECTION_LAYOUT, SECTION_UTILITY, SECTION_AUDIO, SECTION_DEBUG)); - } - - /** - * A HashMap<String, SettingSection> that constructs a new SettingSection instead of returning null - * when getting a key not already in the map - */ - public static final class SettingsSectionMap extends HashMap<String, SettingSection> { - @Override - public SettingSection get(Object key) { - if (!(key instanceof String)) { - return null; - } - - String stringKey = (String) key; - - if (!super.containsKey(stringKey)) { - SettingSection section = new SettingSection(stringKey); - super.put(stringKey, section); - return section; - } - return super.get(key); - } - } - - private HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap(); - - public SettingSection getSection(String sectionName) { - return sections.get(sectionName); - } - - public boolean isEmpty() { - return sections.isEmpty(); - } - - public HashMap<String, SettingSection> getSections() { - return sections; - } - - public void loadSettings(SettingsActivityView view) { - sections = new Settings.SettingsSectionMap(); - loadCitraSettings(view); - - if (!TextUtils.isEmpty(gameId)) { - loadCustomGameSettings(gameId, view); - } - } - - private void loadCitraSettings(SettingsActivityView view) { - for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) { - String fileName = entry.getKey(); - sections.putAll(SettingsFile.readFile(fileName, view)); - } - } - - private void loadCustomGameSettings(String gameId, SettingsActivityView view) { - // custom game settings - mergeSections(SettingsFile.readCustomGameSettings(gameId, view)); - } - - private void mergeSections(HashMap<String, SettingSection> updatedSections) { - for (Map.Entry<String, SettingSection> entry : updatedSections.entrySet()) { - if (sections.containsKey(entry.getKey())) { - SettingSection originalSection = sections.get(entry.getKey()); - SettingSection updatedSection = entry.getValue(); - originalSection.mergeSection(updatedSection); - } else { - sections.put(entry.getKey(), entry.getValue()); - } - } - } - - public void loadSettings(String gameId, SettingsActivityView view) { - this.gameId = gameId; - loadSettings(view); - } - - public void saveSettings(SettingsActivityView view) { - if (TextUtils.isEmpty(gameId)) { - view.showToastMessage(CitraApplication.Companion.getAppContext().getString(R.string.ini_saved), false); - - for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) { - String fileName = entry.getKey(); - List<String> sectionNames = entry.getValue(); - TreeMap<String, SettingSection> iniSections = new TreeMap<>(); - for (String section : sectionNames) { - iniSections.put(section, sections.get(section)); - } - - SettingsFile.saveFile(fileName, iniSections, view); - } - } - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt new file mode 100644 index 0000000000..926f669cc7 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt @@ -0,0 +1,201 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +import android.text.TextUtils +import org.citra.citra_emu.CitraApplication +import org.citra.citra_emu.R +import org.citra.citra_emu.features.settings.ui.SettingsActivityView +import org.citra.citra_emu.features.settings.utils.SettingsFile +import java.util.TreeMap + +class Settings { + private var gameId: String? = null + + var isLoaded = false + + /** + * A HashMap<String></String>, SettingSection> that constructs a new SettingSection instead of returning null + * when getting a key not already in the map + */ + class SettingsSectionMap : HashMap<String, SettingSection?>() { + override operator fun get(key: String): SettingSection? { + if (!super.containsKey(key)) { + val section = SettingSection(key) + super.put(key, section) + return section + } + return super.get(key) + } + } + + var sections: HashMap<String, SettingSection?> = SettingsSectionMap() + + fun getSection(sectionName: String): SettingSection? { + return sections[sectionName] + } + + val isEmpty: Boolean + get() = sections.isEmpty() + + fun loadSettings(view: SettingsActivityView? = null) { + sections = SettingsSectionMap() + loadCitraSettings(view) + if (!TextUtils.isEmpty(gameId)) { + loadCustomGameSettings(gameId!!, view) + } + isLoaded = true + } + + private fun loadCitraSettings(view: SettingsActivityView?) { + for ((fileName) in configFileSectionsMap) { + sections.putAll(SettingsFile.readFile(fileName, view)) + } + } + + private fun loadCustomGameSettings(gameId: String, view: SettingsActivityView?) { + // Custom game settings + mergeSections(SettingsFile.readCustomGameSettings(gameId, view)) + } + + private fun mergeSections(updatedSections: HashMap<String, SettingSection?>) { + for ((key, updatedSection) in updatedSections) { + if (sections.containsKey(key)) { + val originalSection = sections[key] + originalSection!!.mergeSection(updatedSection!!) + } else { + sections[key] = updatedSection + } + } + } + + fun loadSettings(gameId: String, view: SettingsActivityView) { + this.gameId = gameId + loadSettings(view) + } + + fun saveSettings(view: SettingsActivityView) { + if (TextUtils.isEmpty(gameId)) { + view.showToastMessage( + CitraApplication.appContext.getString(R.string.ini_saved), + false + ) + for ((fileName, sectionNames) in configFileSectionsMap.entries) { + val iniSections = TreeMap<String, SettingSection?>() + for (section in sectionNames) { + iniSections[section] = sections[section] + } + SettingsFile.saveFile(fileName, iniSections, view) + } + } else { + // TODO: Implement per game settings + } + } + + companion object { + const val SECTION_CORE = "Core" + const val SECTION_SYSTEM = "System" + const val SECTION_CAMERA = "Camera" + const val SECTION_CONTROLS = "Controls" + const val SECTION_RENDERER = "Renderer" + const val SECTION_LAYOUT = "Layout" + const val SECTION_UTILITY = "Utility" + const val SECTION_AUDIO = "Audio" + const val SECTION_DEBUG = "Debugging" + const val SECTION_THEME = "Theme" + + const val KEY_BUTTON_A = "button_a" + const val KEY_BUTTON_B = "button_b" + const val KEY_BUTTON_X = "button_x" + const val KEY_BUTTON_Y = "button_y" + const val KEY_BUTTON_SELECT = "button_select" + const val KEY_BUTTON_START = "button_start" + const val KEY_BUTTON_HOME = "button_home" + const val KEY_BUTTON_UP = "button_up" + const val KEY_BUTTON_DOWN = "button_down" + const val KEY_BUTTON_LEFT = "button_left" + const val KEY_BUTTON_RIGHT = "button_right" + const val KEY_BUTTON_L = "button_l" + const val KEY_BUTTON_R = "button_r" + const val KEY_BUTTON_ZL = "button_zl" + const val KEY_BUTTON_ZR = "button_zr" + const val KEY_CIRCLEPAD_AXIS_VERTICAL = "circlepad_axis_vertical" + const val KEY_CIRCLEPAD_AXIS_HORIZONTAL = "circlepad_axis_horizontal" + const val KEY_CSTICK_AXIS_VERTICAL = "cstick_axis_vertical" + const val KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal" + const val KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical" + const val KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal" + + val buttonKeys = listOf( + KEY_BUTTON_A, + KEY_BUTTON_B, + KEY_BUTTON_X, + KEY_BUTTON_Y, + KEY_BUTTON_SELECT, + KEY_BUTTON_START, + KEY_BUTTON_HOME + ) + val buttonTitles = listOf( + R.string.button_a, + R.string.button_b, + R.string.button_x, + R.string.button_y, + R.string.button_select, + R.string.button_start, + R.string.button_home + ) + val circlePadKeys = listOf( + KEY_CIRCLEPAD_AXIS_VERTICAL, + KEY_CIRCLEPAD_AXIS_HORIZONTAL + ) + val cStickKeys = listOf( + KEY_CSTICK_AXIS_VERTICAL, + KEY_CSTICK_AXIS_HORIZONTAL + ) + val dPadKeys = listOf( + KEY_DPAD_AXIS_VERTICAL, + KEY_DPAD_AXIS_HORIZONTAL + ) + val axisTitles = listOf( + R.string.controller_axis_vertical, + R.string.controller_axis_horizontal + ) + val triggerKeys = listOf( + KEY_BUTTON_L, + KEY_BUTTON_R, + KEY_BUTTON_ZL, + KEY_BUTTON_ZR + ) + val triggerTitles = listOf( + R.string.button_l, + R.string.button_r, + R.string.button_zl, + R.string.button_zr + ) + + const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch" + const val PREF_MATERIAL_YOU = "MaterialYouTheme" + const val PREF_THEME_MODE = "ThemeMode" + const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds" + const val PREF_SHOW_HOME_APPS = "ShowHomeApps" + + private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap() + + init { + configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] = + listOf( + SECTION_CORE, + SECTION_SYSTEM, + SECTION_CAMERA, + SECTION_CONTROLS, + SECTION_RENDERER, + SECTION_LAYOUT, + SECTION_UTILITY, + SECTION_AUDIO, + SECTION_DEBUG + ) + } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt new file mode 100644 index 0000000000..3f9b4ad1fc --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt @@ -0,0 +1,11 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +import androidx.lifecycle.ViewModel + +class SettingsViewModel : ViewModel() { + val settings = Settings() +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.java deleted file mode 100644 index b906b70109..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.citra.citra_emu.features.settings.model; - -public final class StringSetting extends Setting { - private String mValue; - - public StringSetting(String key, String section, String value) { - super(key, section); - mValue = value; - } - - public String getValue() { - return mValue; - } - - public void setValue(String value) { - mValue = value; - } - - @Override - public String getValueAsString() { - return mValue; - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.kt new file mode 100644 index 0000000000..87b425f5b6 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.kt @@ -0,0 +1,50 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +enum class StringSetting( + override val key: String, + override val section: String, + override val defaultValue: String +) : AbstractStringSetting { + INIT_TIME("init_time", Settings.SECTION_SYSTEM, "946731601"), + CAMERA_INNER_NAME("camera_inner_name", Settings.SECTION_CAMERA, "ndk"), + CAMERA_INNER_CONFIG("camera_inner_config", Settings.SECTION_CAMERA, "_front"), + CAMERA_OUTER_LEFT_NAME("camera_outer_left_name", Settings.SECTION_CAMERA, "ndk"), + CAMERA_OUTER_LEFT_CONFIG("camera_outer_left_config", Settings.SECTION_CAMERA, "_back"), + CAMERA_OUTER_RIGHT_NAME("camera_outer_right_name", Settings.SECTION_CAMERA, "ndk"), + CAMERA_OUTER_RIGHT_CONFIG("camera_outer_right_config", Settings.SECTION_CAMERA, "_back"); + + override var string: String = defaultValue + + override val valueAsString: String + get() = string + + override val isRuntimeEditable: Boolean + get() { + for (setting in NOT_RUNTIME_EDITABLE) { + if (setting == this) { + return false + } + } + return true + } + + companion object { + private val NOT_RUNTIME_EDITABLE = listOf( + INIT_TIME, + CAMERA_INNER_NAME, + CAMERA_INNER_CONFIG, + CAMERA_OUTER_LEFT_NAME, + CAMERA_OUTER_LEFT_CONFIG, + CAMERA_OUTER_RIGHT_NAME, + CAMERA_OUTER_RIGHT_CONFIG + ) + + fun from(key: String): StringSetting? = StringSetting.values().firstOrNull { it.key == key } + + fun clear() = StringSetting.values().forEach { it.string = it.defaultValue } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/AbstractShortSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/AbstractShortSetting.kt new file mode 100644 index 0000000000..865ebbdd0e --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/AbstractShortSetting.kt @@ -0,0 +1,11 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model.view + +import org.citra.citra_emu.features.settings.model.AbstractSetting + +interface AbstractShortSetting : AbstractSetting { + var short: Short +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/CheckBoxSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/CheckBoxSetting.java deleted file mode 100644 index 6bafecfe03..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/CheckBoxSetting.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -import org.citra.citra_emu.CitraApplication; -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.BooleanSetting; -import org.citra.citra_emu.features.settings.model.IntSetting; -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.features.settings.ui.SettingsFragmentView; - -public final class CheckBoxSetting extends SettingsItem { - private boolean mDefaultValue; - private boolean mShowPerformanceWarning; - private SettingsFragmentView mView; - - public CheckBoxSetting(String key, String section, int titleId, int descriptionId, - boolean defaultValue, Setting setting) { - super(key, section, setting, titleId, descriptionId); - mDefaultValue = defaultValue; - mShowPerformanceWarning = false; - } - - public CheckBoxSetting(String key, String section, int titleId, int descriptionId, - boolean defaultValue, Setting setting, boolean show_performance_warning, SettingsFragmentView view) { - super(key, section, setting, titleId, descriptionId); - mDefaultValue = defaultValue; - mView = view; - mShowPerformanceWarning = show_performance_warning; - } - - public boolean isChecked() { - if (getSetting() == null) { - return mDefaultValue; - } - - // Try integer setting - try { - IntSetting setting = (IntSetting) getSetting(); - return setting.getValue() == 1; - } catch (ClassCastException exception) { - } - - // Try boolean setting - try { - BooleanSetting setting = (BooleanSetting) getSetting(); - return setting.getValue() == true; - } catch (ClassCastException exception) { - } - - return mDefaultValue; - } - - /** - * Write a value to the backing boolean. If that boolean was previously null, - * initializes a new one and returns it, so it can be added to the Hashmap. - * - * @param checked Pretty self explanatory. - * @return null if overwritten successfully; otherwise, a newly created BooleanSetting. - */ - public IntSetting setChecked(boolean checked) { - // Show a performance warning if the setting has been disabled - if (mShowPerformanceWarning && !checked) { - mView.showToastMessage(CitraApplication.Companion.getAppContext().getString(R.string.performance_warning), true); - } - - if (getSetting() == null) { - IntSetting setting = new IntSetting(getKey(), getSection(), checked ? 1 : 0); - setSetting(setting); - return setting; - } else { - IntSetting setting = (IntSetting) getSetting(); - setting.setValue(checked ? 1 : 0); - return null; - } - } - - @Override - public int getType() { - return TYPE_CHECKBOX; - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.java deleted file mode 100644 index afc3352cc0..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.features.settings.model.StringSetting; - -public final class DateTimeSetting extends SettingsItem { - private String mDefaultValue; - - public DateTimeSetting(String key, String section, int titleId, int descriptionId, - String defaultValue, Setting setting) { - super(key, section, setting, titleId, descriptionId); - mDefaultValue = defaultValue; - } - - public String getValue() { - if (getSetting() != null) { - StringSetting setting = (StringSetting) getSetting(); - return setting.getValue(); - } else { - return mDefaultValue; - } - } - - public StringSetting setSelectedValue(String datetime) { - if (getSetting() == null) { - StringSetting setting = new StringSetting(getKey(), getSection(), datetime); - setSetting(setting); - return setting; - } else { - StringSetting setting = (StringSetting) getSetting(); - setting.setValue(datetime); - return null; - } - } - - @Override - public int getType() { - return TYPE_DATETIME_SETTING; - } -} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.kt new file mode 100644 index 0000000000..6332e0e310 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.kt @@ -0,0 +1,32 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model.view + +import org.citra.citra_emu.features.settings.model.AbstractSetting +import org.citra.citra_emu.features.settings.model.AbstractStringSetting + +class DateTimeSetting( + setting: AbstractSetting?, + titleId: Int, + descriptionId: Int, + val key: String? = null, + private val defaultValue: String? = null +) : SettingsItem(setting, titleId, descriptionId) { + override val type = TYPE_DATETIME_SETTING + + val value: String + get() = if (setting != null) { + val setting = setting as AbstractStringSetting + setting.string + } else { + defaultValue!! + } + + fun setSelectedValue(datetime: String): AbstractStringSetting { + val stringSetting = setting as AbstractStringSetting + stringSetting.string = datetime + return stringSetting + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.java deleted file mode 100644 index bac8876cdf..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -import org.citra.citra_emu.features.settings.model.Setting; - -public final class HeaderSetting extends SettingsItem { - public HeaderSetting(String key, Setting setting, int titleId, int descriptionId) { - super(key, null, setting, titleId, descriptionId); - } - - @Override - public int getType() { - return SettingsItem.TYPE_HEADER; - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.kt new file mode 100644 index 0000000000..e99b842f9f --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.kt @@ -0,0 +1,9 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model.view + +class HeaderSetting(titleId: Int) : SettingsItem(null, titleId, 0) { + override val type = TYPE_HEADER +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.java deleted file mode 100644 index 6d4d954e8c..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.java +++ /dev/null @@ -1,382 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.view.InputDevice; -import android.view.KeyEvent; -import android.widget.Toast; - -import org.citra.citra_emu.CitraApplication; -import org.citra.citra_emu.NativeLibrary; -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.features.settings.model.StringSetting; -import org.citra.citra_emu.features.settings.utils.SettingsFile; - -public final class InputBindingSetting extends SettingsItem { - private static final String INPUT_MAPPING_PREFIX = "InputMapping"; - - public InputBindingSetting(String key, String section, int titleId, Setting setting) { - super(key, section, setting, titleId, 0); - } - - public String getValue() { - if (getSetting() == null) { - return ""; - } - - StringSetting setting = (StringSetting) getSetting(); - return setting.getValue(); - } - - /** - * Returns true if this key is for the 3DS Circle Pad - */ - private boolean IsCirclePad() { - switch (getKey()) { - case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL: - case SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL: - return true; - } - return false; - } - - /** - * Returns true if this key is for a horizontal axis for a 3DS analog stick or D-pad - */ - public boolean IsHorizontalOrientation() { - switch (getKey()) { - case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL: - case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL: - case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL: - return true; - } - return false; - } - - /** - * Returns true if this key is for the 3DS C-Stick - */ - private boolean IsCStick() { - switch (getKey()) { - case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL: - case SettingsFile.KEY_CSTICK_AXIS_VERTICAL: - return true; - } - return false; - } - - /** - * Returns true if this key is for the 3DS D-Pad - */ - private boolean IsDPad() { - switch (getKey()) { - case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL: - case SettingsFile.KEY_DPAD_AXIS_VERTICAL: - return true; - } - return false; - } - - /** - * Returns true if this key is for the 3DS L/R or ZL/ZR buttons. Note, these are not real - * triggers on the 3DS, but we support them as such on a physical gamepad. - */ - public boolean IsTrigger() { - switch (getKey()) { - case SettingsFile.KEY_BUTTON_L: - case SettingsFile.KEY_BUTTON_R: - case SettingsFile.KEY_BUTTON_ZL: - case SettingsFile.KEY_BUTTON_ZR: - return true; - } - return false; - } - - /** - * Returns true if a gamepad axis can be used to map this key. - */ - public boolean IsAxisMappingSupported() { - return IsCirclePad() || IsCStick() || IsDPad() || IsTrigger(); - } - - /** - * Returns true if a gamepad button can be used to map this key. - */ - private boolean IsButtonMappingSupported() { - return !IsAxisMappingSupported() || IsTrigger(); - } - - /** - * Returns the Citra button code for the settings key. - */ - private int getButtonCode() { - switch (getKey()) { - case SettingsFile.KEY_BUTTON_A: - return NativeLibrary.ButtonType.BUTTON_A; - case SettingsFile.KEY_BUTTON_B: - return NativeLibrary.ButtonType.BUTTON_B; - case SettingsFile.KEY_BUTTON_X: - return NativeLibrary.ButtonType.BUTTON_X; - case SettingsFile.KEY_BUTTON_Y: - return NativeLibrary.ButtonType.BUTTON_Y; - case SettingsFile.KEY_BUTTON_L: - return NativeLibrary.ButtonType.TRIGGER_L; - case SettingsFile.KEY_BUTTON_R: - return NativeLibrary.ButtonType.TRIGGER_R; - case SettingsFile.KEY_BUTTON_ZL: - return NativeLibrary.ButtonType.BUTTON_ZL; - case SettingsFile.KEY_BUTTON_ZR: - return NativeLibrary.ButtonType.BUTTON_ZR; - case SettingsFile.KEY_BUTTON_SELECT: - return NativeLibrary.ButtonType.BUTTON_SELECT; - case SettingsFile.KEY_BUTTON_START: - return NativeLibrary.ButtonType.BUTTON_START; - case SettingsFile.KEY_BUTTON_UP: - return NativeLibrary.ButtonType.DPAD_UP; - case SettingsFile.KEY_BUTTON_DOWN: - return NativeLibrary.ButtonType.DPAD_DOWN; - case SettingsFile.KEY_BUTTON_LEFT: - return NativeLibrary.ButtonType.DPAD_LEFT; - case SettingsFile.KEY_BUTTON_RIGHT: - return NativeLibrary.ButtonType.DPAD_RIGHT; - } - return -1; - } - - /** - * Returns the settings key for the specified Citra button code. - */ - private static String getButtonKey(int buttonCode) { - switch (buttonCode) { - case NativeLibrary.ButtonType.BUTTON_A: - return SettingsFile.KEY_BUTTON_A; - case NativeLibrary.ButtonType.BUTTON_B: - return SettingsFile.KEY_BUTTON_B; - case NativeLibrary.ButtonType.BUTTON_X: - return SettingsFile.KEY_BUTTON_X; - case NativeLibrary.ButtonType.BUTTON_Y: - return SettingsFile.KEY_BUTTON_Y; - case NativeLibrary.ButtonType.TRIGGER_L: - return SettingsFile.KEY_BUTTON_L; - case NativeLibrary.ButtonType.TRIGGER_R: - return SettingsFile.KEY_BUTTON_R; - case NativeLibrary.ButtonType.BUTTON_ZL: - return SettingsFile.KEY_BUTTON_ZL; - case NativeLibrary.ButtonType.BUTTON_ZR: - return SettingsFile.KEY_BUTTON_ZR; - case NativeLibrary.ButtonType.BUTTON_SELECT: - return SettingsFile.KEY_BUTTON_SELECT; - case NativeLibrary.ButtonType.BUTTON_START: - return SettingsFile.KEY_BUTTON_START; - case NativeLibrary.ButtonType.DPAD_UP: - return SettingsFile.KEY_BUTTON_UP; - case NativeLibrary.ButtonType.DPAD_DOWN: - return SettingsFile.KEY_BUTTON_DOWN; - case NativeLibrary.ButtonType.DPAD_LEFT: - return SettingsFile.KEY_BUTTON_LEFT; - case NativeLibrary.ButtonType.DPAD_RIGHT: - return SettingsFile.KEY_BUTTON_RIGHT; - } - return ""; - } - - /** - * Returns the key used to lookup the reverse mapping for this key, which is used to cleanup old - * settings on re-mapping or clearing of a setting. - */ - private String getReverseKey() { - String reverseKey = INPUT_MAPPING_PREFIX + "_ReverseMapping_" + getKey(); - - if (IsAxisMappingSupported() && !IsTrigger()) { - // Triggers are the only axis-supported mappings without orientation - reverseKey += "_" + (IsHorizontalOrientation() ? 0 : 1); - } - - return reverseKey; - } - - /** - * Removes the old mapping for this key from the settings, e.g. on user clearing the setting. - */ - public void removeOldMapping() { - // Get preferences editor - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext()); - SharedPreferences.Editor editor = preferences.edit(); - - // Try remove all possible keys we wrote for this setting - String oldKey = preferences.getString(getReverseKey(), ""); - if (!oldKey.equals("")) { - editor.remove(getKey()); // Used for ui text - editor.remove(oldKey); // Used for button mapping - editor.remove(oldKey + "_GuestOrientation"); // Used for axis orientation - editor.remove(oldKey + "_GuestButton"); // Used for axis button - } - - // Apply changes - editor.apply(); - } - - /** - * Helper function to get the settings key for an gamepad button. - */ - public static String getInputButtonKey(int keyCode) { - return INPUT_MAPPING_PREFIX + "_Button_" + keyCode; - } - - /** - * Helper function to get the settings key for an gamepad axis. - */ - public static String getInputAxisKey(int axis) { - return INPUT_MAPPING_PREFIX + "_HostAxis_" + axis; - } - - /** - * Helper function to get the settings key for an gamepad axis button (stick or trigger). - */ - public static String getInputAxisButtonKey(int axis) { - return getInputAxisKey(axis) + "_GuestButton"; - } - - /** - * Helper function to get the settings key for an gamepad axis orientation. - */ - public static String getInputAxisOrientationKey(int axis) { - return getInputAxisKey(axis) + "_GuestOrientation"; - } - - /** - * Helper function to write a gamepad button mapping for the setting. - */ - private void WriteButtonMapping(String key) { - // Get preferences editor - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext()); - SharedPreferences.Editor editor = preferences.edit(); - - // Remove mapping for another setting using this input - int oldButtonCode = preferences.getInt(key, -1); - if (oldButtonCode != -1) { - String oldKey = getButtonKey(oldButtonCode); - editor.remove(oldKey); // Only need to remove UI text setting, others will be overwritten - } - - // Cleanup old mapping for this setting - removeOldMapping(); - - // Write new mapping - editor.putInt(key, getButtonCode()); - - // Write next reverse mapping for future cleanup - editor.putString(getReverseKey(), key); - - // Apply changes - editor.apply(); - } - - /** - * Helper function to write a gamepad axis mapping for the setting. - */ - private void WriteAxisMapping(int axis, int value) { - // Get preferences editor - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext()); - SharedPreferences.Editor editor = preferences.edit(); - - // Cleanup old mapping - removeOldMapping(); - - // Write new mapping - editor.putInt(getInputAxisOrientationKey(axis), IsHorizontalOrientation() ? 0 : 1); - editor.putInt(getInputAxisButtonKey(axis), value); - - // Write next reverse mapping for future cleanup - editor.putString(getReverseKey(), getInputAxisKey(axis)); - - // Apply changes - editor.apply(); - } - - /** - * Saves the provided key input setting as an Android preference. - * - * @param keyEvent KeyEvent of this key press. - */ - public void onKeyInput(KeyEvent keyEvent) { - if (!IsButtonMappingSupported()) { - Toast.makeText(CitraApplication.Companion.getAppContext(), R.string.input_message_analog_only, Toast.LENGTH_LONG).show(); - return; - } - - InputDevice device = keyEvent.getDevice(); - - WriteButtonMapping(getInputButtonKey(keyEvent.getKeyCode())); - - String uiString = device.getName() + ": Button " + keyEvent.getKeyCode(); - setUiString(uiString); - } - - /** - * Saves the provided motion input setting as an Android preference. - * - * @param device InputDevice from which the input event originated. - * @param motionRange MotionRange of the movement - * @param axisDir Either '-' or '+' (currently unused) - */ - public void onMotionInput(InputDevice device, InputDevice.MotionRange motionRange, - char axisDir) { - if (!IsAxisMappingSupported()) { - Toast.makeText(CitraApplication.Companion.getAppContext(), R.string.input_message_button_only, Toast.LENGTH_LONG).show(); - return; - } - - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext()); - SharedPreferences.Editor editor = preferences.edit(); - - int button; - if (IsCirclePad()) { - button = NativeLibrary.ButtonType.STICK_LEFT; - } else if (IsCStick()) { - button = NativeLibrary.ButtonType.STICK_C; - } else if (IsDPad()) { - button = NativeLibrary.ButtonType.DPAD; - } else { - button = getButtonCode(); - } - - WriteAxisMapping(motionRange.getAxis(), button); - - String uiString = device.getName() + ": Axis " + motionRange.getAxis(); - setUiString(uiString); - - editor.apply(); - } - - /** - * Sets the string to use in the configuration UI for the gamepad input. - */ - private StringSetting setUiString(String ui) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext()); - SharedPreferences.Editor editor = preferences.edit(); - - if (getSetting() == null) { - StringSetting setting = new StringSetting(getKey(), getSection(), ""); - setSetting(setting); - - editor.putString(setting.getKey(), ui); - editor.apply(); - - return setting; - } else { - StringSetting setting = (StringSetting) getSetting(); - - editor.putString(setting.getKey(), ui); - editor.apply(); - - return null; - } - } - - @Override - public int getType() { - return TYPE_INPUT_BINDING; - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt new file mode 100644 index 0000000000..a6ae5e31c0 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt @@ -0,0 +1,299 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model.view + +import android.content.Context +import android.content.SharedPreferences +import androidx.preference.PreferenceManager +import android.view.InputDevice +import android.view.InputDevice.MotionRange +import android.view.KeyEvent +import android.widget.Toast +import org.citra.citra_emu.CitraApplication +import org.citra.citra_emu.NativeLibrary +import org.citra.citra_emu.R +import org.citra.citra_emu.features.settings.model.AbstractSetting +import org.citra.citra_emu.features.settings.model.Settings + +class InputBindingSetting( + val abstractSetting: AbstractSetting, + titleId: Int +) : SettingsItem(abstractSetting, titleId, 0) { + private val context: Context get() = CitraApplication.appContext + private val preferences: SharedPreferences + get() = PreferenceManager.getDefaultSharedPreferences(context) + + var value: String + get() = preferences.getString(abstractSetting.key, "")!! + set(string) { + preferences.edit() + .putString(abstractSetting.key, string) + .apply() + } + + /** + * Returns true if this key is for the 3DS Circle Pad + */ + fun isCirclePad(): Boolean = + when (abstractSetting.key) { + Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, + Settings.KEY_CIRCLEPAD_AXIS_VERTICAL -> true + + else -> false + } + + /** + * Returns true if this key is for a horizontal axis for a 3DS analog stick or D-pad + */ + fun isHorizontalOrientation(): Boolean = + when (abstractSetting.key) { + Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, + Settings.KEY_CSTICK_AXIS_HORIZONTAL, + Settings.KEY_DPAD_AXIS_HORIZONTAL -> true + + else -> false + } + + /** + * Returns true if this key is for the 3DS C-Stick + */ + fun isCStick(): Boolean = + when (abstractSetting.key) { + Settings.KEY_CSTICK_AXIS_HORIZONTAL, + Settings.KEY_CSTICK_AXIS_VERTICAL -> true + + else -> false + } + + /** + * Returns true if this key is for the 3DS D-Pad + */ + fun isDPad(): Boolean = + when (abstractSetting.key) { + Settings.KEY_DPAD_AXIS_HORIZONTAL, + Settings.KEY_DPAD_AXIS_VERTICAL -> true + + else -> false + } + + /** + * Returns true if this key is for the 3DS L/R or ZL/ZR buttons. Note, these are not real + * triggers on the 3DS, but we support them as such on a physical gamepad. + */ + fun isTrigger(): Boolean = + when (abstractSetting.key) { + Settings.KEY_BUTTON_L, + Settings.KEY_BUTTON_R, + Settings.KEY_BUTTON_ZL, + Settings.KEY_BUTTON_ZR -> true + + else -> false + } + + /** + * Returns true if a gamepad axis can be used to map this key. + */ + fun isAxisMappingSupported(): Boolean { + return isCirclePad() || isCStick() || isDPad() || isTrigger() + } + + /** + * Returns true if a gamepad button can be used to map this key. + */ + fun isButtonMappingSupported(): Boolean { + return !isAxisMappingSupported() || isTrigger() + } + + /** + * Returns the Citra button code for the settings key. + */ + private val buttonCode: Int + get() = + when (abstractSetting.key) { + Settings.KEY_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A + Settings.KEY_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B + Settings.KEY_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X + Settings.KEY_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y + Settings.KEY_BUTTON_L -> NativeLibrary.ButtonType.TRIGGER_L + Settings.KEY_BUTTON_R -> NativeLibrary.ButtonType.TRIGGER_R + Settings.KEY_BUTTON_ZL -> NativeLibrary.ButtonType.BUTTON_ZL + Settings.KEY_BUTTON_ZR -> NativeLibrary.ButtonType.BUTTON_ZR + Settings.KEY_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_SELECT + Settings.KEY_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_START + Settings.KEY_BUTTON_HOME -> NativeLibrary.ButtonType.BUTTON_HOME + Settings.KEY_BUTTON_UP -> NativeLibrary.ButtonType.DPAD_UP + Settings.KEY_BUTTON_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN + Settings.KEY_BUTTON_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT + Settings.KEY_BUTTON_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT + else -> -1 + } + + /** + * Returns the key used to lookup the reverse mapping for this key, which is used to cleanup old + * settings on re-mapping or clearing of a setting. + */ + private val reverseKey: String + get() { + var reverseKey = "${INPUT_MAPPING_PREFIX}_ReverseMapping_${abstractSetting.key}" + if (isAxisMappingSupported() && !isTrigger()) { + // Triggers are the only axis-supported mappings without orientation + reverseKey += "_" + if (isHorizontalOrientation()) { + 0 + } else { + 1 + } + } + return reverseKey + } + + /** + * Removes the old mapping for this key from the settings, e.g. on user clearing the setting. + */ + fun removeOldMapping() { + // Try remove all possible keys we wrote for this setting + val oldKey = preferences.getString(reverseKey, "") + if (oldKey != "") { + preferences.edit() + .remove(abstractSetting.key) // Used for ui text + .remove(oldKey) // Used for button mapping + .remove(oldKey + "_GuestOrientation") // Used for axis orientation + .remove(oldKey + "_GuestButton") // Used for axis button + .apply() + } + } + + /** + * Helper function to write a gamepad button mapping for the setting. + */ + private fun writeButtonMapping(key: String) { + val editor = preferences.edit() + + // Remove mapping for another setting using this input + val oldButtonCode = preferences.getInt(key, -1) + if (oldButtonCode != -1) { + val oldKey = getButtonKey(oldButtonCode) + editor.remove(oldKey) // Only need to remove UI text setting, others will be overwritten + } + + // Cleanup old mapping for this setting + removeOldMapping() + + // Write new mapping + editor.putInt(key, buttonCode) + + // Write next reverse mapping for future cleanup + editor.putString(reverseKey, key) + + // Apply changes + editor.apply() + } + + /** + * Helper function to write a gamepad axis mapping for the setting. + */ + private fun writeAxisMapping(axis: Int, value: Int) { + // Cleanup old mapping + removeOldMapping() + + // Write new mapping + preferences.edit() + .putInt(getInputAxisOrientationKey(axis), if (isHorizontalOrientation()) 0 else 1) + .putInt(getInputAxisButtonKey(axis), value) + // Write next reverse mapping for future cleanup + .putString(reverseKey, getInputAxisKey(axis)) + .apply() + } + + /** + * Saves the provided key input setting as an Android preference. + * + * @param keyEvent KeyEvent of this key press. + */ + fun onKeyInput(keyEvent: KeyEvent) { + if (!isButtonMappingSupported()) { + Toast.makeText(context, R.string.input_message_analog_only, Toast.LENGTH_LONG).show() + return + } + writeButtonMapping(getInputButtonKey(keyEvent.keyCode)) + val uiString = "${keyEvent.device.name}: Button ${keyEvent.keyCode}" + value = uiString + } + + /** + * Saves the provided motion input setting as an Android preference. + * + * @param device InputDevice from which the input event originated. + * @param motionRange MotionRange of the movement + * @param axisDir Either '-' or '+' (currently unused) + */ + fun onMotionInput(device: InputDevice, motionRange: MotionRange, axisDir: Char) { + if (!isAxisMappingSupported()) { + Toast.makeText(context, R.string.input_message_button_only, Toast.LENGTH_LONG).show() + return + } + val button = if (isCirclePad()) { + NativeLibrary.ButtonType.STICK_LEFT + } else if (isCStick()) { + NativeLibrary.ButtonType.STICK_C + } else if (isDPad()) { + NativeLibrary.ButtonType.DPAD + } else { + buttonCode + } + writeAxisMapping(motionRange.axis, button) + val uiString = "${device.name}: Axis ${motionRange.axis}" + value = uiString + } + + override val type = TYPE_INPUT_BINDING + + companion object { + private const val INPUT_MAPPING_PREFIX = "InputMapping" + + /** + * Returns the settings key for the specified Citra button code. + */ + private fun getButtonKey(buttonCode: Int): String = + when (buttonCode) { + NativeLibrary.ButtonType.BUTTON_A -> Settings.KEY_BUTTON_A + NativeLibrary.ButtonType.BUTTON_B -> Settings.KEY_BUTTON_B + NativeLibrary.ButtonType.BUTTON_X -> Settings.KEY_BUTTON_X + NativeLibrary.ButtonType.BUTTON_Y -> Settings.KEY_BUTTON_Y + NativeLibrary.ButtonType.TRIGGER_L -> Settings.KEY_BUTTON_L + NativeLibrary.ButtonType.TRIGGER_R -> Settings.KEY_BUTTON_R + NativeLibrary.ButtonType.BUTTON_ZL -> Settings.KEY_BUTTON_ZL + NativeLibrary.ButtonType.BUTTON_ZR -> Settings.KEY_BUTTON_ZR + NativeLibrary.ButtonType.BUTTON_SELECT -> Settings.KEY_BUTTON_SELECT + NativeLibrary.ButtonType.BUTTON_START -> Settings.KEY_BUTTON_START + NativeLibrary.ButtonType.BUTTON_HOME -> Settings.KEY_BUTTON_HOME + NativeLibrary.ButtonType.DPAD_UP -> Settings.KEY_BUTTON_UP + NativeLibrary.ButtonType.DPAD_DOWN -> Settings.KEY_BUTTON_DOWN + NativeLibrary.ButtonType.DPAD_LEFT -> Settings.KEY_BUTTON_LEFT + NativeLibrary.ButtonType.DPAD_RIGHT -> Settings.KEY_BUTTON_RIGHT + else -> "" + } + + /** + * Helper function to get the settings key for an gamepad button. + */ + fun getInputButtonKey(keyCode: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${keyCode}" + + /** + * Helper function to get the settings key for an gamepad axis. + */ + fun getInputAxisKey(axis: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${axis}" + + /** + * Helper function to get the settings key for an gamepad axis button (stick or trigger). + */ + fun getInputAxisButtonKey(axis: Int): String = "${getInputAxisKey(axis)}_GuestButton" + + /** + * Helper function to get the settings key for an gamepad axis orientation. + */ + fun getInputAxisOrientationKey(axis: Int): String = + "${getInputAxisKey(axis)}_GuestOrientation" + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt new file mode 100644 index 0000000000..8a237a14e9 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt @@ -0,0 +1,15 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model.view + +class RunnableSetting( + titleId: Int, + descriptionId: Int, + val isRuntimeRunnable: Boolean, + val runnable: () -> Unit, + val value: (() -> String)? = null +) : SettingsItem(null, titleId, descriptionId) { + override val type = TYPE_RUNNABLE +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.java deleted file mode 100644 index 8a56426965..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.features.settings.model.Settings; -import org.citra.citra_emu.features.settings.ui.SettingsAdapter; - -/** - * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments. - * Each one corresponds to a {@link Setting} object, so this class's subclasses - * should vaguely correspond to those subclasses. There are a few with multiple analogues - * and a few with none (Headers, for example, do not correspond to anything in the ini - * file.) - */ -public abstract class SettingsItem { - public static final int TYPE_HEADER = 0; - public static final int TYPE_CHECKBOX = 1; - public static final int TYPE_SINGLE_CHOICE = 2; - public static final int TYPE_SLIDER = 3; - public static final int TYPE_SUBMENU = 4; - public static final int TYPE_INPUT_BINDING = 5; - public static final int TYPE_STRING_SINGLE_CHOICE = 6; - public static final int TYPE_DATETIME_SETTING = 7; - - private String mKey; - private String mSection; - - private Setting mSetting; - - private int mNameId; - private int mDescriptionId; - - /** - * Base constructor. Takes a key / section name in case the third parameter, the Setting, - * is null; in which case, one can be constructed and saved using the key / section. - * - * @param key Identifier for the Setting represented by this Item. - * @param section Section to which the Setting belongs. - * @param setting A possibly-null backing Setting, to be modified on UI events. - * @param nameId Resource ID for a text string to be displayed as this setting's name. - * @param descriptionId Resource ID for a text string to be displayed as this setting's description. - */ - public SettingsItem(String key, String section, Setting setting, int nameId, - int descriptionId) { - mKey = key; - mSection = section; - mSetting = setting; - mNameId = nameId; - mDescriptionId = descriptionId; - } - - /** - * @return The identifier for the backing Setting. - */ - public String getKey() { - return mKey; - } - - /** - * @return The header under which the backing Setting belongs. - */ - public String getSection() { - return mSection; - } - - /** - * @return The backing Setting, possibly null. - */ - public Setting getSetting() { - return mSetting; - } - - /** - * Replace the backing setting with a new one. Generally used in cases where - * the backing setting is null. - * - * @param setting A non-null Setting. - */ - public void setSetting(Setting setting) { - mSetting = setting; - } - - /** - * @return A resource ID for a text string representing this Setting's name. - */ - public int getNameId() { - return mNameId; - } - - public int getDescriptionId() { - return mDescriptionId; - } - - /** - * Used by {@link SettingsAdapter}'s onCreateViewHolder() - * method to determine which type of ViewHolder should be created. - * - * @return An integer (ideally, one of the constants defined in this file) - */ - public abstract int getType(); -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt new file mode 100644 index 0000000000..5f6b4b70ce --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt @@ -0,0 +1,42 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model.view + +import org.citra.citra_emu.NativeLibrary +import org.citra.citra_emu.features.settings.model.AbstractSetting + +/** + * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments. + * Each one corresponds to a [AbstractSetting] object, so this class's subclasses + * should vaguely correspond to those subclasses. There are a few with multiple analogues + * and a few with none (Headers, for example, do not correspond to anything in the ini + * file.) + */ +abstract class SettingsItem( + var setting: AbstractSetting?, + val nameId: Int, + val descriptionId: Int +) { + abstract val type: Int + + val isEditable: Boolean + get() { + if (!NativeLibrary.isRunning()) return true + return setting?.isRuntimeEditable ?: false + } + + companion object { + const val TYPE_HEADER = 0 + const val TYPE_SWITCH = 1 + const val TYPE_SINGLE_CHOICE = 2 + const val TYPE_SLIDER = 3 + const val TYPE_SUBMENU = 4 + const val TYPE_STRING_SINGLE_CHOICE = 5 + const val TYPE_DATETIME_SETTING = 6 + const val TYPE_RUNNABLE = 7 + const val TYPE_INPUT_BINDING = 8 + const val TYPE_STRING_INPUT = 9 + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.java deleted file mode 100644 index ee9d225d60..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -import org.citra.citra_emu.features.settings.model.IntSetting; -import org.citra.citra_emu.features.settings.model.Setting; - -public final class SingleChoiceSetting extends SettingsItem { - private int mDefaultValue; - - private int mChoicesId; - private int mValuesId; - - public SingleChoiceSetting(String key, String section, int titleId, int descriptionId, - int choicesId, int valuesId, int defaultValue, Setting setting) { - super(key, section, setting, titleId, descriptionId); - mValuesId = valuesId; - mChoicesId = choicesId; - mDefaultValue = defaultValue; - } - - public int getChoicesId() { - return mChoicesId; - } - - public int getValuesId() { - return mValuesId; - } - - public int getSelectedValue() { - if (getSetting() != null) { - IntSetting setting = (IntSetting) getSetting(); - return setting.getValue(); - } else { - return mDefaultValue; - } - } - - /** - * Write a value to the backing int. If that int was previously null, - * initializes a new one and returns it, so it can be added to the Hashmap. - * - * @param selection New value of the int. - * @return null if overwritten successfully otherwise; a newly created IntSetting. - */ - public IntSetting setSelectedValue(int selection) { - if (getSetting() == null) { - IntSetting setting = new IntSetting(getKey(), getSection(), selection); - setSetting(setting); - return setting; - } else { - IntSetting setting = (IntSetting) getSetting(); - setting.setValue(selection); - return null; - } - } - - @Override - public int getType() { - return TYPE_SINGLE_CHOICE; - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.kt new file mode 100644 index 0000000000..75c6ec3317 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.kt @@ -0,0 +1,60 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model.view + +import org.citra.citra_emu.features.settings.model.AbstractIntSetting +import org.citra.citra_emu.features.settings.model.AbstractSetting + +class SingleChoiceSetting( + setting: AbstractSetting?, + titleId: Int, + descriptionId: Int, + val choicesId: Int, + val valuesId: Int, + val key: String? = null, + val defaultValue: Int? = null +) : SettingsItem(setting, titleId, descriptionId) { + override val type = TYPE_SINGLE_CHOICE + + val selectedValue: Int + get() { + if (setting == null) { + return defaultValue!! + } + + try { + val setting = setting as AbstractIntSetting + return setting.int + } catch (_: ClassCastException) { + } + + try { + val setting = setting as AbstractShortSetting + return setting.short.toInt() + } catch (_: ClassCastException) { + } + + return defaultValue!! + } + + /** + * Write a value to the backing int. If that int was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param selection New value of the int. + * @return the existing setting with the new value applied. + */ + fun setSelectedValue(selection: Int): AbstractIntSetting { + val intSetting = setting as AbstractIntSetting + intSetting.int = selection + return intSetting + } + + fun setSelectedValue(selection: Short): AbstractShortSetting { + val shortSetting = setting as AbstractShortSetting + shortSetting.short = selection + return shortSetting + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.java deleted file mode 100644 index 551b13f999..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -import org.citra.citra_emu.features.settings.model.FloatSetting; -import org.citra.citra_emu.features.settings.model.IntSetting; -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.utils.Log; - -public final class SliderSetting extends SettingsItem { - private int mMin; - private int mMax; - private int mDefaultValue; - - private String mUnits; - - public SliderSetting(String key, String section, int titleId, int descriptionId, - int min, int max, String units, int defaultValue, Setting setting) { - super(key, section, setting, titleId, descriptionId); - mMin = min; - mMax = max; - mUnits = units; - mDefaultValue = defaultValue; - } - - public int getMin() { - return mMin; - } - - public int getMax() { - return mMax; - } - - public int getDefaultValue() { - return mDefaultValue; - } - - public int getSelectedValue() { - Setting setting = getSetting(); - - if (setting == null) { - return mDefaultValue; - } - - if (setting instanceof IntSetting) { - IntSetting intSetting = (IntSetting) setting; - return intSetting.getValue(); - } else if (setting instanceof FloatSetting) { - FloatSetting floatSetting = (FloatSetting) setting; - return Math.round(floatSetting.getValue()); - } else { - Log.error("[SliderSetting] Error casting setting type."); - return -1; - } - } - - /** - * Write a value to the backing int. If that int was previously null, - * initializes a new one and returns it, so it can be added to the Hashmap. - * - * @param selection New value of the int. - * @return null if overwritten successfully otherwise; a newly created IntSetting. - */ - public IntSetting setSelectedValue(int selection) { - if (getSetting() == null) { - IntSetting setting = new IntSetting(getKey(), getSection(), selection); - setSetting(setting); - return setting; - } else { - IntSetting setting = (IntSetting) getSetting(); - setting.setValue(selection); - return null; - } - } - - /** - * Write a value to the backing float. If that float was previously null, - * initializes a new one and returns it, so it can be added to the Hashmap. - * - * @param selection New value of the float. - * @return null if overwritten successfully otherwise; a newly created FloatSetting. - */ - public FloatSetting setSelectedValue(float selection) { - if (getSetting() == null) { - FloatSetting setting = new FloatSetting(getKey(), getSection(), selection); - setSetting(setting); - return setting; - } else { - FloatSetting setting = (FloatSetting) getSetting(); - setting.setValue(selection); - return null; - } - } - - public String getUnits() { - return mUnits; - } - - @Override - public int getType() { - return TYPE_SLIDER; - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.kt new file mode 100644 index 0000000000..1227bf46d1 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.kt @@ -0,0 +1,70 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model.view + +import org.citra.citra_emu.features.settings.model.AbstractFloatSetting +import org.citra.citra_emu.features.settings.model.AbstractIntSetting +import org.citra.citra_emu.features.settings.model.AbstractSetting +import org.citra.citra_emu.features.settings.model.FloatSetting +import org.citra.citra_emu.features.settings.model.ScaledFloatSetting +import org.citra.citra_emu.utils.Log +import kotlin.math.roundToInt + +class SliderSetting( + setting: AbstractSetting?, + titleId: Int, + descriptionId: Int, + val min: Int, + val max: Int, + val units: String, + val key: String? = null, + val defaultValue: Float? = null +) : SettingsItem(setting, titleId, descriptionId) { + override val type = TYPE_SLIDER + + val selectedValue: Int + get() { + val setting = setting ?: return defaultValue!!.toInt() + return when (setting) { + is AbstractIntSetting -> setting.int + is FloatSetting -> setting.float.roundToInt() + is ScaledFloatSetting -> setting.float.roundToInt() + else -> { + Log.error("[SliderSetting] Error casting setting type.") + -1 + } + } + } + + /** + * Write a value to the backing int. If that int was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param selection New value of the int. + * @return the existing setting with the new value applied. + */ + fun setSelectedValue(selection: Int): AbstractIntSetting { + val intSetting = setting as AbstractIntSetting + intSetting.int = selection + return intSetting + } + + /** + * Write a value to the backing float. If that float was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param selection New value of the float. + * @return the existing setting with the new value applied. + */ + fun setSelectedValue(selection: Float): AbstractFloatSetting { + val floatSetting = setting as AbstractFloatSetting + if (floatSetting is ScaledFloatSetting) { + floatSetting.float = selection + } else { + floatSetting.float = selection + } + return floatSetting + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringInputSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringInputSetting.kt new file mode 100644 index 0000000000..ebbe1a675a --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringInputSetting.kt @@ -0,0 +1,27 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model.view + +import org.citra.citra_emu.features.settings.model.AbstractSetting +import org.citra.citra_emu.features.settings.model.AbstractStringSetting + +class StringInputSetting( + setting: AbstractSetting?, + titleId: Int, + descriptionId: Int, + val defaultValue: String, + val characterLimit: Int = 0 +) : SettingsItem(setting, titleId, descriptionId) { + override val type = TYPE_STRING_INPUT + + val selectedValue: String + get() = setting?.valueAsString ?: defaultValue + + fun setSelectedValue(selection: String): AbstractStringSetting { + val stringSetting = setting as AbstractStringSetting + stringSetting.string = selection + return stringSetting + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.java deleted file mode 100644 index 057145d9da..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.features.settings.model.StringSetting; - -public class StringSingleChoiceSetting extends SettingsItem { - private String mDefaultValue; - - private String[] mChoicesId; - private String[] mValuesId; - - public StringSingleChoiceSetting(String key, String section, int titleId, int descriptionId, - String[] choicesId, String[] valuesId, String defaultValue, Setting setting) { - super(key, section, setting, titleId, descriptionId); - mValuesId = valuesId; - mChoicesId = choicesId; - mDefaultValue = defaultValue; - } - - public String[] getChoicesId() { - return mChoicesId; - } - - public String[] getValuesId() { - return mValuesId; - } - - public String getValueAt(int index) { - if (mValuesId == null) - return null; - - if (index >= 0 && index < mValuesId.length) { - return mValuesId[index]; - } - - return ""; - } - - public String getSelectedValue() { - if (getSetting() != null) { - StringSetting setting = (StringSetting) getSetting(); - return setting.getValue(); - } else { - return mDefaultValue; - } - } - - public int getSelectValueIndex() { - String selectedValue = getSelectedValue(); - for (int i = 0; i < mValuesId.length; i++) { - if (mValuesId[i].equals(selectedValue)) { - return i; - } - } - - return -1; - } - - /** - * Write a value to the backing int. If that int was previously null, - * initializes a new one and returns it, so it can be added to the Hashmap. - * - * @param selection New value of the int. - * @return null if overwritten successfully otherwise; a newly created IntSetting. - */ - public StringSetting setSelectedValue(String selection) { - if (getSetting() == null) { - StringSetting setting = new StringSetting(getKey(), getSection(), selection); - setSetting(setting); - return setting; - } else { - StringSetting setting = (StringSetting) getSetting(); - setting.setValue(selection); - return null; - } - } - - @Override - public int getType() { - return TYPE_STRING_SINGLE_CHOICE; - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.kt new file mode 100644 index 0000000000..c763e29f50 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.kt @@ -0,0 +1,78 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model.view + +import org.citra.citra_emu.features.settings.model.AbstractSetting +import org.citra.citra_emu.features.settings.model.AbstractStringSetting + +class StringSingleChoiceSetting( + setting: AbstractSetting?, + titleId: Int, + descriptionId: Int, + val choices: Array<String>, + val values: Array<String>?, + val key: String? = null, + private val defaultValue: String? = null +) : SettingsItem(setting, titleId, descriptionId) { + override val type = TYPE_STRING_SINGLE_CHOICE + + fun getValueAt(index: Int): String? { + if (values == null) return null + return if (index >= 0 && index < values.size) { + values[index] + } else { + "" + } + } + + val selectedValue: String + get() { + if (setting == null) { + return defaultValue!! + } + + try { + val setting = setting as AbstractStringSetting + return setting.string + } catch (_: ClassCastException) { + } + + try { + val setting = setting as AbstractShortSetting + return setting.short.toString() + } catch (_: ClassCastException) { + } + return defaultValue!! + } + val selectValueIndex: Int + get() { + val selectedValue = selectedValue + for (i in values!!.indices) { + if (values[i] == selectedValue) { + return i + } + } + return -1 + } + + /** + * Write a value to the backing int. If that int was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param selection New value of the int. + * @return the existing setting with the new value applied. + */ + fun setSelectedValue(selection: String): AbstractStringSetting { + val stringSetting = setting as AbstractStringSetting + stringSetting.string = selection + return stringSetting + } + + fun setSelectedValue(selection: Short): AbstractShortSetting { + val shortSetting = setting as AbstractShortSetting + shortSetting.short = selection + return shortSetting + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.java deleted file mode 100644 index 9d44a923fd..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.citra.citra_emu.features.settings.model.view; - -import org.citra.citra_emu.features.settings.model.Setting; - -public final class SubmenuSetting extends SettingsItem { - private String mMenuKey; - - public SubmenuSetting(String key, Setting setting, int titleId, int descriptionId, String menuKey) { - super(key, null, setting, titleId, descriptionId); - mMenuKey = menuKey; - } - - public String getMenuKey() { - return mMenuKey; - } - - @Override - public int getType() { - return TYPE_SUBMENU; - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.kt new file mode 100644 index 0000000000..08e1c60477 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.kt @@ -0,0 +1,13 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model.view + +class SubmenuSetting( + titleId: Int, + descriptionId: Int, + val menuKey: String +) : SettingsItem(null, titleId, descriptionId) { + override val type = TYPE_SUBMENU +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SwitchSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SwitchSetting.kt new file mode 100644 index 0000000000..a1d27849d3 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SwitchSetting.kt @@ -0,0 +1,63 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model.view + +import org.citra.citra_emu.features.settings.model.AbstractBooleanSetting +import org.citra.citra_emu.features.settings.model.AbstractIntSetting +import org.citra.citra_emu.features.settings.model.AbstractSetting + +class SwitchSetting( + setting: AbstractSetting, + titleId: Int, + descriptionId: Int, + val key: String? = null, + val defaultValue: Any? = null +) : SettingsItem(setting, titleId, descriptionId) { + override val type = TYPE_SWITCH + + val isChecked: Boolean + get() { + if (setting == null) { + return defaultValue as Boolean + } + + // Try integer setting + try { + val setting = setting as AbstractIntSetting + return setting.int == 1 + } catch (_: ClassCastException) { + } + + // Try boolean setting + try { + val setting = setting as AbstractBooleanSetting + return setting.boolean + } catch (_: ClassCastException) { + } + return defaultValue as Boolean + } + + /** + * Write a value to the backing boolean. If that boolean was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param checked Pretty self explanatory. + * @return the existing setting with the new value applied. + */ + fun setChecked(checked: Boolean): AbstractSetting { + // Try integer setting + try { + val setting = setting as AbstractIntSetting + setting.int = if (checked) 1 else 0 + return setting + } catch (_: ClassCastException) { + } + + // Try boolean setting + val setting = setting as AbstractBooleanSetting + setting.boolean = checked + return setting + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.java deleted file mode 100644 index 58ffbbfea8..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.java +++ /dev/null @@ -1,227 +0,0 @@ -package org.citra.citra_emu.features.settings.ui; - -import android.app.ProgressDialog; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Bundle; -import android.provider.Settings; -import android.view.Menu; -import android.view.MenuInflater; -import android.widget.FrameLayout; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.graphics.Insets; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowCompat; -import androidx.core.view.WindowInsetsCompat; -import androidx.fragment.app.FragmentTransaction; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import com.google.android.material.appbar.AppBarLayout; -import com.google.android.material.appbar.MaterialToolbar; - -import org.citra.citra_emu.NativeLibrary; -import org.citra.citra_emu.R; -import org.citra.citra_emu.utils.DirectoryInitialization; -import org.citra.citra_emu.utils.EmulationMenuSettings; -import org.citra.citra_emu.utils.InsetsHelper; -import org.citra.citra_emu.utils.ThemeUtil; - -public final class SettingsActivity extends AppCompatActivity implements SettingsActivityView { - private static final String ARG_MENU_TAG = "menu_tag"; - private static final String ARG_GAME_ID = "game_id"; - private static final String FRAGMENT_TAG = "settings"; - private SettingsActivityPresenter mPresenter = new SettingsActivityPresenter(this); - - private ProgressDialog dialog; - - public static void launch(Context context, String menuTag, String gameId) { - Intent settings = new Intent(context, SettingsActivity.class); - settings.putExtra(ARG_MENU_TAG, menuTag); - settings.putExtra(ARG_GAME_ID, gameId); - context.startActivity(settings); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - ThemeUtil.INSTANCE.setTheme(this); - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_settings); - - WindowCompat.setDecorFitsSystemWindows(getWindow(), false); - - Intent launcher = getIntent(); - String gameID = launcher.getStringExtra(ARG_GAME_ID); - String menuTag = launcher.getStringExtra(ARG_MENU_TAG); - - mPresenter.onCreate(savedInstanceState, menuTag, gameID); - - // Show "Back" button in the action bar for navigation - MaterialToolbar toolbar = findViewById(R.id.toolbar_settings); - setSupportActionBar(toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - setInsets(); - } - - @Override - public boolean onSupportNavigateUp() { - onBackPressed(); - - return true; - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.menu_settings, menu); - - return true; - } - - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - // Critical: If super method is not called, rotations will be busted. - super.onSaveInstanceState(outState); - mPresenter.saveState(outState); - } - - @Override - protected void onStart() { - super.onStart(); - mPresenter.onStart(); - } - - /** - * If this is called, the user has left the settings screen (potentially through the - * home button) and will expect their changes to be persisted. So we kick off an - * IntentService which will do so on a background thread. - */ - @Override - protected void onStop() { - super.onStop(); - - mPresenter.onStop(isFinishing()); - - // Update framebuffer layout when closing the settings - NativeLibrary.INSTANCE.notifyOrientationChange(EmulationMenuSettings.getLandscapeScreenLayout(), - getWindowManager().getDefaultDisplay().getRotation()); - } - - @Override - public void showSettingsFragment(String menuTag, boolean addToStack, String gameID) { - if (!addToStack && getFragment() != null) { - return; - } - - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - - if (addToStack) { - if (areSystemAnimationsEnabled()) { - transaction.setCustomAnimations( - R.anim.anim_settings_fragment_in, - R.anim.anim_settings_fragment_out, - 0, - R.anim.anim_pop_settings_fragment_out); - } - - transaction.addToBackStack(null); - } - transaction.replace(R.id.frame_content, SettingsFragment.newInstance(menuTag, gameID), FRAGMENT_TAG); - - transaction.commit(); - } - - private boolean areSystemAnimationsEnabled() { - float duration = Settings.Global.getFloat( - getContentResolver(), - Settings.Global.ANIMATOR_DURATION_SCALE, 1); - float transition = Settings.Global.getFloat( - getContentResolver(), - Settings.Global.TRANSITION_ANIMATION_SCALE, 1); - return duration != 0 && transition != 0; - } - - @Override - public void showLoading() { - if (dialog == null) { - dialog = new ProgressDialog(this); - dialog.setMessage(getString(R.string.load_settings)); - dialog.setIndeterminate(true); - } - - dialog.show(); - } - - @Override - public void hideLoading() { - dialog.dismiss(); - } - - @Override - public void showPermissionNeededHint() { - Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT) - .show(); - } - - @Override - public void showExternalStorageNotMountedHint() { - Toast.makeText(this, R.string.external_storage_not_mounted, Toast.LENGTH_SHORT) - .show(); - } - - @Override - public org.citra.citra_emu.features.settings.model.Settings getSettings() { - return mPresenter.getSettings(); - } - - @Override - public void setSettings(org.citra.citra_emu.features.settings.model.Settings settings) { - mPresenter.setSettings(settings); - } - - @Override - public void onSettingsFileLoaded(org.citra.citra_emu.features.settings.model.Settings settings) { - SettingsFragmentView fragment = getFragment(); - - if (fragment != null) { - fragment.onSettingsFileLoaded(settings); - } - } - - @Override - public void onSettingsFileNotFound() { - SettingsFragmentView fragment = getFragment(); - - if (fragment != null) { - fragment.loadDefaultSettings(); - } - } - - @Override - public void showToastMessage(String message, boolean is_long) { - Toast.makeText(this, message, is_long ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show(); - } - - @Override - public void onSettingChanged() { - mPresenter.onSettingChanged(); - } - - private SettingsFragment getFragment() { - return (SettingsFragment) getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG); - } - - private void setInsets() { - AppBarLayout appBar = findViewById(R.id.appbar_settings); - FrameLayout frame = findViewById(R.id.frame_content); - ViewCompat.setOnApplyWindowInsetsListener(frame, (v, windowInsets) -> { - Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); - InsetsHelper.insetAppBar(insets, appBar); - return windowInsets; - }); - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt new file mode 100644 index 0000000000..796f577b1e --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt @@ -0,0 +1,292 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.view.ViewGroup.MarginLayoutParams +import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.preference.PreferenceManager +import com.google.android.material.color.MaterialColors +import org.citra.citra_emu.CitraApplication +import org.citra.citra_emu.NativeLibrary +import org.citra.citra_emu.R +import org.citra.citra_emu.databinding.ActivitySettingsBinding +import java.io.IOException +import org.citra.citra_emu.features.settings.model.BooleanSetting +import org.citra.citra_emu.features.settings.model.FloatSetting +import org.citra.citra_emu.features.settings.model.IntSetting +import org.citra.citra_emu.features.settings.model.ScaledFloatSetting +import org.citra.citra_emu.features.settings.model.Settings +import org.citra.citra_emu.features.settings.model.SettingsViewModel +import org.citra.citra_emu.features.settings.model.StringSetting +import org.citra.citra_emu.features.settings.utils.SettingsFile +import org.citra.citra_emu.utils.SystemSaveGame +import org.citra.citra_emu.utils.DirectoryInitialization +import org.citra.citra_emu.utils.InsetsHelper +import org.citra.citra_emu.utils.ThemeUtil + +class SettingsActivity : AppCompatActivity(), SettingsActivityView { + private val presenter = SettingsActivityPresenter(this) + + private lateinit var binding: ActivitySettingsBinding + + private val settingsViewModel: SettingsViewModel by viewModels() + + override val settings: Settings get() = settingsViewModel.settings + + override fun onCreate(savedInstanceState: Bundle?) { + ThemeUtil.setTheme(this) + + super.onCreate(savedInstanceState) + + binding = ActivitySettingsBinding.inflate(layoutInflater) + setContentView(binding.root) + + WindowCompat.setDecorFitsSystemWindows(window, false) + + val launcher = intent + val gameID = launcher.getStringExtra(ARG_GAME_ID) + val menuTag = launcher.getStringExtra(ARG_MENU_TAG) + presenter.onCreate(savedInstanceState, menuTag!!, gameID!!) + + // Show "Back" button in the action bar for navigation + setSupportActionBar(binding.toolbarSettings) + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + + if (InsetsHelper.getSystemGestureType(applicationContext) != + InsetsHelper.GESTURE_NAVIGATION + ) { + binding.navigationBarShade.setBackgroundColor( + ThemeUtil.getColorWithOpacity( + MaterialColors.getColor( + binding.navigationBarShade, + com.google.android.material.R.attr.colorSurface + ), + ThemeUtil.SYSTEM_BAR_ALPHA + ) + ) + } + + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() = navigateBack() + } + ) + + setInsets() + } + + override fun onSupportNavigateUp(): Boolean { + navigateBack() + return true + } + + private fun navigateBack() { + if (supportFragmentManager.backStackEntryCount > 0) { + supportFragmentManager.popBackStack() + } else { + finish() + } + } + + override fun onSaveInstanceState(outState: Bundle) { + // Critical: If super method is not called, rotations will be busted. + super.onSaveInstanceState(outState) + presenter.saveState(outState) + } + + override fun onStart() { + super.onStart() + presenter.onStart() + } + + /** + * If this is called, the user has left the settings screen (potentially through the + * home button) and will expect their changes to be persisted. So we kick off an + * IntentService which will do so on a background thread. + */ + override fun onStop() { + super.onStop() + presenter.onStop(isFinishing) + } + + override fun showSettingsFragment(menuTag: String, addToStack: Boolean, gameId: String) { + if (!addToStack && settingsFragment != null) { + return + } + + val transaction = supportFragmentManager.beginTransaction() + if (addToStack) { + if (areSystemAnimationsEnabled()) { + transaction.setCustomAnimations( + R.anim.anim_settings_fragment_in, + R.anim.anim_settings_fragment_out, + 0, + R.anim.anim_pop_settings_fragment_out + ) + } + transaction.addToBackStack(null) + } + transaction.replace( + R.id.frame_content, + SettingsFragment.newInstance(menuTag, gameId), + FRAGMENT_TAG + ) + transaction.commit() + } + + private fun areSystemAnimationsEnabled(): Boolean { + val duration = android.provider.Settings.Global.getFloat( + contentResolver, + android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, + 1f + ) + val transition = android.provider.Settings.Global.getFloat( + contentResolver, + android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE, + 1f + ) + return duration != 0f && transition != 0f + } + + override fun onSettingsFileLoaded() { + val fragment: SettingsFragmentView? = settingsFragment + fragment?.loadSettingsList() + } + + override fun onSettingsFileNotFound() { + val fragment: SettingsFragmentView? = settingsFragment + fragment?.loadSettingsList() + } + + override fun showToastMessage(message: String, isLong: Boolean) { + Toast.makeText( + this, + message, + if (isLong) Toast.LENGTH_LONG else Toast.LENGTH_SHORT + ).show() + } + + override fun onSettingChanged() { + presenter.onSettingChanged() + } + + fun onSettingsReset() { + // Prevents saving to a non-existent settings file + presenter.onSettingsReset() + + val controllerKeys = Settings.buttonKeys + Settings.circlePadKeys + Settings.cStickKeys + + Settings.dPadKeys + Settings.triggerKeys + val editor = + PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext).edit() + controllerKeys.forEach { editor.remove(it) } + editor.apply() + + // Reset the static memory representation of each setting + BooleanSetting.clear() + FloatSetting.clear() + ScaledFloatSetting.clear() + IntSetting.clear() + StringSetting.clear() + + // Delete settings file because the user may have changed values that do not exist in the UI + val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG) + if (!settingsFile.delete()) { + throw IOException("Failed to delete $settingsFile") + } + + // Set the root of the document tree before we create a new config file or the native code + // will fail when creating the file. + if (DirectoryInitialization.setCitraUserDirectory()) { + CitraApplication.documentsTree.setRoot(Uri.parse(DirectoryInitialization.userPath)) + NativeLibrary.createConfigFile() + } else { + throw IllegalStateException("Citra directory unavailable when accessing config file!") + } + + // Set default values for system config file + SystemSaveGame.apply { + setUsername("CITRA") + setBirthday(3, 25) + setSystemLanguage(1) + setSoundOutputMode(2) + setCountryCode(49) + setPlayCoins(42) + } + + showToastMessage(getString(R.string.settings_reset), true) + finish() + } + + fun setToolbarTitle(title: String) { + binding.toolbarSettingsLayout.title = title + } + + private val settingsFragment: SettingsFragment? + get() = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as SettingsFragment? + + private fun setInsets() { + ViewCompat.setOnApplyWindowInsetsListener( + binding.frameContent + ) { view: View, windowInsets: WindowInsetsCompat -> + val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) + view.updatePadding( + left = barInsets.left + cutoutInsets.left, + right = barInsets.right + cutoutInsets.right + ) + + val mlpAppBar = binding.appbarSettings.layoutParams as MarginLayoutParams + mlpAppBar.leftMargin = barInsets.left + cutoutInsets.left + mlpAppBar.rightMargin = barInsets.right + cutoutInsets.right + binding.appbarSettings.layoutParams = mlpAppBar + + val mlpShade = binding.navigationBarShade.layoutParams as MarginLayoutParams + mlpShade.height = barInsets.bottom + binding.navigationBarShade.layoutParams = mlpShade + + windowInsets + } + } + + companion object { + private const val ARG_MENU_TAG = "menu_tag" + private const val ARG_GAME_ID = "game_id" + private const val FRAGMENT_TAG = "settings" + + @JvmStatic + fun launch(context: Context, menuTag: String?, gameId: String?) { + val settings = Intent(context, SettingsActivity::class.java) + settings.putExtra(ARG_MENU_TAG, menuTag) + settings.putExtra(ARG_GAME_ID, gameId) + context.startActivity(settings) + } + + fun launch( + context: Context, + launcher: ActivityResultLauncher<Intent>, + menuTag: String?, + gameId: String? + ) { + val settings = Intent(context, SettingsActivity::class.java) + settings.putExtra(ARG_MENU_TAG, menuTag) + settings.putExtra(ARG_GAME_ID, gameId) + launcher.launch(settings) + } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.java deleted file mode 100644 index 84a7d9d646..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.citra.citra_emu.features.settings.ui; - -import android.content.IntentFilter; -import android.os.Bundle; -import android.text.TextUtils; -import androidx.appcompat.app.AppCompatActivity; -import androidx.documentfile.provider.DocumentFile; -import java.io.File; -import org.citra.citra_emu.NativeLibrary; -import org.citra.citra_emu.features.settings.model.Settings; -import org.citra.citra_emu.features.settings.utils.SettingsFile; -import org.citra.citra_emu.utils.DirectoryInitialization; -import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState; -import org.citra.citra_emu.utils.Log; -import org.citra.citra_emu.utils.ThemeUtil; - -public final class SettingsActivityPresenter { - private static final String KEY_SHOULD_SAVE = "should_save"; - - private SettingsActivityView mView; - - private Settings mSettings = new Settings(); - - private boolean mShouldSave; - - private String menuTag; - private String gameId; - - public SettingsActivityPresenter(SettingsActivityView view) { - mView = view; - } - - public void onCreate(Bundle savedInstanceState, String menuTag, String gameId) { - if (savedInstanceState == null) { - this.menuTag = menuTag; - this.gameId = gameId; - } else { - mShouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE); - } - } - - public void onStart() { - prepareCitraDirectoriesIfNeeded(); - } - - void loadSettingsUI() { - if (mSettings.isEmpty()) { - if (!TextUtils.isEmpty(gameId)) { - mSettings.loadSettings(gameId, mView); - } else { - mSettings.loadSettings(mView); - } - } - - mView.showSettingsFragment(menuTag, false, gameId); - mView.onSettingsFileLoaded(mSettings); - } - - private void prepareCitraDirectoriesIfNeeded() { - DocumentFile configFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG); - if (configFile == null || !configFile.exists()) { - Log.error("Citra config file could not be found!"); - } - loadSettingsUI(); - } - - public void setSettings(Settings settings) { - mSettings = settings; - } - - public Settings getSettings() { - return mSettings; - } - - public void onStop(boolean finishing) { - if (mSettings != null && finishing && mShouldSave) { - Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI..."); - mSettings.saveSettings(mView); - } - - NativeLibrary.INSTANCE.reloadSettings(); - } - - public void onSettingChanged() { - mShouldSave = true; - } - - public void saveState(Bundle outState) { - outState.putBoolean(KEY_SHOULD_SAVE, mShouldSave); - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt new file mode 100644 index 0000000000..ae36531092 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt @@ -0,0 +1,78 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui + +import android.os.Bundle +import android.text.TextUtils +import org.citra.citra_emu.NativeLibrary +import org.citra.citra_emu.features.settings.model.Settings +import org.citra.citra_emu.utils.SystemSaveGame +import org.citra.citra_emu.utils.DirectoryInitialization +import org.citra.citra_emu.utils.Log + +class SettingsActivityPresenter(private val activityView: SettingsActivityView) { + val settings: Settings get() = activityView.settings + + private var shouldSave = false + private lateinit var menuTag: String + private lateinit var gameId: String + + fun onCreate(savedInstanceState: Bundle?, menuTag: String, gameId: String) { + this.menuTag = menuTag + this.gameId = gameId + if (savedInstanceState != null) { + shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE) + } + } + + fun onStart() { + SystemSaveGame.load() + prepareDirectoriesIfNeeded() + } + + private fun loadSettingsUI() { + if (!settings.isLoaded) { + if (!TextUtils.isEmpty(gameId)) { + settings.loadSettings(gameId, activityView) + } else { + settings.loadSettings(activityView) + } + } + activityView.showSettingsFragment(menuTag, false, gameId) + activityView.onSettingsFileLoaded() + } + + private fun prepareDirectoriesIfNeeded() { + if (!DirectoryInitialization.areCitraDirectoriesReady()) { + DirectoryInitialization.start() + } + loadSettingsUI() + } + + fun onStop(finishing: Boolean) { + if (finishing && shouldSave) { + Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...") + settings.saveSettings(activityView) + SystemSaveGame.save() + } + NativeLibrary.reloadSettings() + } + + fun onSettingChanged() { + shouldSave = true + } + + fun onSettingsReset() { + shouldSave = false + } + + fun saveState(outState: Bundle) { + outState.putBoolean(KEY_SHOULD_SAVE, shouldSave) + } + + companion object { + private const val KEY_SHOULD_SAVE = "should_save" + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.java deleted file mode 100644 index bd2f5f5aa6..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.citra.citra_emu.features.settings.ui; - -import android.content.IntentFilter; - -import org.citra.citra_emu.features.settings.model.Settings; - -/** - * Abstraction for the Activity that manages SettingsFragments. - */ -public interface SettingsActivityView { - /** - * Show a new SettingsFragment. - * - * @param menuTag Identifier for the settings group that should be displayed. - * @param addToStack Whether or not this fragment should replace a previous one. - */ - void showSettingsFragment(String menuTag, boolean addToStack, String gameId); - - /** - * Called by a contained Fragment to get access to the Setting HashMap - * loaded from disk, so that each Fragment doesn't need to perform its own - * read operation. - * - * @return A possibly null HashMap of Settings. - */ - Settings getSettings(); - - /** - * Used to provide the Activity with Settings HashMaps if a Fragment already - * has one; for example, if a rotation occurs, the Fragment will not be killed, - * but the Activity will, so the Activity needs to have its HashMaps resupplied. - * - * @param settings The ArrayList of all the Settings HashMaps. - */ - void setSettings(Settings settings); - - /** - * Called when an asynchronous load operation completes. - * - * @param settings The (possibly null) result of the ini load operation. - */ - void onSettingsFileLoaded(Settings settings); - - /** - * Called when an asynchronous load operation fails. - */ - void onSettingsFileNotFound(); - - /** - * Display a popup text message on screen. - * - * @param message The contents of the onscreen message. - * @param is_long Whether this should be a long Toast or short one. - */ - void showToastMessage(String message, boolean is_long); - - /** - * End the activity. - */ - void finish(); - - /** - * Called by a containing Fragment to tell the Activity that a setting was changed; - * unless this has been called, the Activity will not save to disk. - */ - void onSettingChanged(); - - /** - * Show loading dialog while loading the settings - */ - void showLoading(); - - /** - * Hide the loading the dialog - */ - void hideLoading(); - - /** - * Show a hint to the user that the app needs write to external storage access - */ - void showPermissionNeededHint(); - - /** - * Show a hint to the user that the app needs the external storage to be mounted - */ - void showExternalStorageNotMountedHint(); -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.kt new file mode 100644 index 0000000000..126a010f01 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.kt @@ -0,0 +1,58 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui + +import org.citra.citra_emu.features.settings.model.Settings + +/** + * Abstraction for the Activity that manages SettingsFragments. + */ +interface SettingsActivityView { + /** + * Show a new SettingsFragment. + * + * @param menuTag Identifier for the settings group that should be displayed. + * @param addToStack Whether or not this fragment should replace a previous one. + */ + fun showSettingsFragment(menuTag: String, addToStack: Boolean, gameId: String) + + /** + * Called by a contained Fragment to get access to the Setting HashMap + * loaded from disk, so that each Fragment doesn't need to perform its own + * read operation. + * + * @return A HashMap of Settings. + */ + val settings: Settings + + /** + * Called when a load operation completes. + */ + fun onSettingsFileLoaded() + + /** + * Called when a load operation fails. + */ + fun onSettingsFileNotFound() + + /** + * Display a popup text message on screen. + * + * @param message The contents of the onscreen message. + * @param isLong Whether this should be a long Toast or short one. + */ + fun showToastMessage(message: String, isLong: Boolean) + + /** + * End the activity. + */ + fun finish() + + /** + * Called by a containing Fragment to tell the Activity that a setting was changed; + * unless this has been called, the Activity will not save to disk. + */ + fun onSettingChanged() +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.java deleted file mode 100644 index b03ec11b4a..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.java +++ /dev/null @@ -1,393 +0,0 @@ -package org.citra.citra_emu.features.settings.ui; - -import android.content.Context; -import android.content.DialogInterface; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.DatePicker; -import android.widget.TextView; -import android.widget.TimePicker; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.slider.Slider; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.dialogs.MotionAlertDialog; -import org.citra.citra_emu.features.settings.model.FloatSetting; -import org.citra.citra_emu.features.settings.model.IntSetting; -import org.citra.citra_emu.features.settings.model.StringSetting; -import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting; -import org.citra.citra_emu.features.settings.model.view.DateTimeSetting; -import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting; -import org.citra.citra_emu.features.settings.model.view.SliderSetting; -import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting; -import org.citra.citra_emu.features.settings.model.view.SubmenuSetting; -import org.citra.citra_emu.features.settings.ui.viewholder.CheckBoxSettingViewHolder; -import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder; -import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder; -import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder; -import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder; -import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder; -import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder; -import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder; -import org.citra.citra_emu.utils.Log; - -import java.util.ArrayList; - -public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolder> implements DialogInterface.OnClickListener, Slider.OnChangeListener { - private SettingsFragmentView mView; - private Context mContext; - private ArrayList<SettingsItem> mSettings; - - private SettingsItem mClickedItem; - private int mClickedPosition; - private int mSliderProgress; - - private AlertDialog mDialog; - private TextView mTextSliderValue; - - public SettingsAdapter(SettingsFragmentView view, Context context) { - mView = view; - mContext = context; - mClickedPosition = -1; - } - - @Override - public SettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view; - LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - - switch (viewType) { - case SettingsItem.TYPE_HEADER: - view = inflater.inflate(R.layout.list_item_settings_header, parent, false); - return new HeaderViewHolder(view, this); - - case SettingsItem.TYPE_CHECKBOX: - view = inflater.inflate(R.layout.list_item_setting_checkbox, parent, false); - return new CheckBoxSettingViewHolder(view, this); - - case SettingsItem.TYPE_SINGLE_CHOICE: - case SettingsItem.TYPE_STRING_SINGLE_CHOICE: - view = inflater.inflate(R.layout.list_item_setting, parent, false); - return new SingleChoiceViewHolder(view, this); - - case SettingsItem.TYPE_SLIDER: - view = inflater.inflate(R.layout.list_item_setting, parent, false); - return new SliderViewHolder(view, this); - - case SettingsItem.TYPE_SUBMENU: - view = inflater.inflate(R.layout.list_item_setting, parent, false); - return new SubmenuViewHolder(view, this); - - case SettingsItem.TYPE_INPUT_BINDING: - view = inflater.inflate(R.layout.list_item_setting, parent, false); - return new InputBindingSettingViewHolder(view, this, mContext); - - case SettingsItem.TYPE_DATETIME_SETTING: - view = inflater.inflate(R.layout.list_item_setting, parent, false); - return new DateTimeViewHolder(view, this); - - default: - Log.error("[SettingsAdapter] Invalid view type: " + viewType); - return null; - } - } - - @Override - public void onBindViewHolder(SettingViewHolder holder, int position) { - holder.bind(getItem(position)); - } - - private SettingsItem getItem(int position) { - return mSettings.get(position); - } - - @Override - public int getItemCount() { - if (mSettings != null) { - return mSettings.size(); - } else { - return 0; - } - } - - @Override - public int getItemViewType(int position) { - return getItem(position).getType(); - } - - public void setSettings(ArrayList<SettingsItem> settings) { - mSettings = settings; - notifyDataSetChanged(); - } - - public void onBooleanClick(CheckBoxSetting item, int position, boolean checked) { - IntSetting setting = item.setChecked(checked); - notifyItemChanged(position); - - if (setting != null) { - mView.putSetting(setting); - } - - mView.onSettingChanged(); - } - - public void onSingleChoiceClick(SingleChoiceSetting item) { - mClickedItem = item; - - int value = getSelectionForSingleChoiceValue(item); - - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity()) - .setTitle(item.getNameId()) - .setSingleChoiceItems(item.getChoicesId(), value, this); - mDialog = builder.show(); - } - - public void onSingleChoiceClick(SingleChoiceSetting item, int position) { - mClickedPosition = position; - onSingleChoiceClick(item); - } - - public void onStringSingleChoiceClick(StringSingleChoiceSetting item) { - mClickedItem = item; - - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity()) - .setTitle(item.getNameId()) - .setSingleChoiceItems(item.getChoicesId(), item.getSelectValueIndex(), this); - mDialog = builder.show(); - } - - public void onStringSingleChoiceClick(StringSingleChoiceSetting item, int position) { - mClickedPosition = position; - onStringSingleChoiceClick(item); - } - - DialogInterface.OnClickListener defaultCancelListener = (dialog, which) -> closeDialog(); - - public void onDateTimeClick(DateTimeSetting item, int position) { - mClickedItem = item; - mClickedPosition = position; - - LayoutInflater inflater = LayoutInflater.from(mView.getActivity()); - View view = inflater.inflate(R.layout.sysclock_datetime_picker, null); - - DatePicker dp = view.findViewById(R.id.date_picker); - TimePicker tp = view.findViewById(R.id.time_picker); - - //set date and time to substrings of settingValue; format = 2018-12-24 04:20:69 (alright maybe not that 69) - String settingValue = item.getValue(); - dp.updateDate(Integer.parseInt(settingValue.substring(0, 4)), Integer.parseInt(settingValue.substring(5, 7)) - 1, Integer.parseInt(settingValue.substring(8, 10))); - - tp.setIs24HourView(true); - tp.setHour(Integer.parseInt(settingValue.substring(11, 13))); - tp.setMinute(Integer.parseInt(settingValue.substring(14, 16))); - - DialogInterface.OnClickListener ok = (dialog, which) -> { - //set it - int year = dp.getYear(); - if (year < 2000) { - year = 2000; - } - String month = ("00" + (dp.getMonth() + 1)).substring(String.valueOf(dp.getMonth() + 1).length()); - String day = ("00" + dp.getDayOfMonth()).substring(String.valueOf(dp.getDayOfMonth()).length()); - String hr = ("00" + tp.getHour()).substring(String.valueOf(tp.getHour()).length()); - String min = ("00" + tp.getMinute()).substring(String.valueOf(tp.getMinute()).length()); - String datetime = year + "-" + month + "-" + day + " " + hr + ":" + min + ":01"; - - StringSetting setting = item.setSelectedValue(datetime); - if (setting != null) { - mView.putSetting(setting); - } - - mView.onSettingChanged(); - - mClickedItem = null; - closeDialog(); - }; - - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity()) - .setView(view) - .setPositiveButton(android.R.string.ok, ok) - .setNegativeButton(android.R.string.cancel, defaultCancelListener); - mDialog = builder.show(); - } - - public void onSliderClick(SliderSetting item, int position) { - mClickedItem = item; - mClickedPosition = position; - mSliderProgress = item.getSelectedValue(); - - LayoutInflater inflater = LayoutInflater.from(mView.getActivity()); - View view = inflater.inflate(R.layout.dialog_slider, null); - - Slider slider = view.findViewById(R.id.slider); - - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity()) - .setTitle(item.getNameId()) - .setView(view) - .setPositiveButton(android.R.string.ok, this) - .setNegativeButton(android.R.string.cancel, defaultCancelListener) - .setNeutralButton(R.string.slider_default, (DialogInterface dialog, int which) -> { - slider.setValue(item.getDefaultValue()); - onClick(dialog, which); - }); - mDialog = builder.show(); - - mTextSliderValue = view.findViewById(R.id.text_value); - mTextSliderValue.setText(String.valueOf(mSliderProgress)); - - TextView units = view.findViewById(R.id.text_units); - units.setText(item.getUnits()); - - slider.setValueFrom(item.getMin()); - slider.setValueTo(item.getMax()); - slider.setValue(mSliderProgress); - - slider.addOnChangeListener(this); - } - - public void onSubmenuClick(SubmenuSetting item) { - mView.loadSubMenu(item.getMenuKey()); - } - - public void onInputBindingClick(final InputBindingSetting item, final int position) { - final MotionAlertDialog dialog = new MotionAlertDialog(mContext, item); - dialog.setTitle(R.string.input_binding); - - int messageResId = R.string.input_binding_description; - if (item.IsAxisMappingSupported() && !item.IsTrigger()) { - // Use specialized message for axis left/right or up/down - if (item.IsHorizontalOrientation()) { - messageResId = R.string.input_binding_description_horizontal_axis; - } else { - messageResId = R.string.input_binding_description_vertical_axis; - } - } - - dialog.setMessage(String.format(mContext.getString(messageResId), mContext.getString(item.getNameId()))); - dialog.setButton(AlertDialog.BUTTON_NEGATIVE, mContext.getString(android.R.string.cancel), this); - dialog.setButton(AlertDialog.BUTTON_NEUTRAL, mContext.getString(R.string.clear), (dialogInterface, i) -> - item.removeOldMapping()); - dialog.setOnDismissListener(dialog1 -> - { - StringSetting setting = new StringSetting(item.getKey(), item.getSection(), item.getValue()); - notifyItemChanged(position); - - mView.putSetting(setting); - - mView.onSettingChanged(); - }); - dialog.setCanceledOnTouchOutside(false); - dialog.show(); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - if (mClickedItem instanceof SingleChoiceSetting) { - SingleChoiceSetting scSetting = (SingleChoiceSetting) mClickedItem; - - int value = getValueForSingleChoiceSelection(scSetting, which); - if (scSetting.getSelectedValue() != value) { - mView.onSettingChanged(); - } - - // Get the backing Setting, which may be null (if for example it was missing from the file) - IntSetting setting = scSetting.setSelectedValue(value); - if (setting != null) { - mView.putSetting(setting); - } - - closeDialog(); - } else if (mClickedItem instanceof StringSingleChoiceSetting) { - StringSingleChoiceSetting scSetting = (StringSingleChoiceSetting) mClickedItem; - String value = scSetting.getValueAt(which); - if (!scSetting.getSelectedValue().equals(value)) - mView.onSettingChanged(); - - StringSetting setting = scSetting.setSelectedValue(value); - if (setting != null) { - mView.putSetting(setting); - } - - closeDialog(); - } else if (mClickedItem instanceof SliderSetting) { - SliderSetting sliderSetting = (SliderSetting) mClickedItem; - if (sliderSetting.getSelectedValue() != mSliderProgress) { - mView.onSettingChanged(); - } - - if (sliderSetting.getSetting() instanceof FloatSetting) { - float value = (float) mSliderProgress; - - FloatSetting setting = sliderSetting.setSelectedValue(value); - if (setting != null) { - mView.putSetting(setting); - } - } else { - IntSetting setting = sliderSetting.setSelectedValue(mSliderProgress); - if (setting != null) { - mView.putSetting(setting); - } - } - - closeDialog(); - } - - mClickedItem = null; - mSliderProgress = -1; - } - - public void closeDialog() { - if (mDialog != null) { - if (mClickedPosition != -1) { - notifyItemChanged(mClickedPosition); - mClickedPosition = -1; - } - mDialog.dismiss(); - mDialog = null; - } - } - - private int getValueForSingleChoiceSelection(SingleChoiceSetting item, int which) { - int valuesId = item.getValuesId(); - - if (valuesId > 0) { - int[] valuesArray = mContext.getResources().getIntArray(valuesId); - return valuesArray[which]; - } else { - return which; - } - } - - private int getSelectionForSingleChoiceValue(SingleChoiceSetting item) { - int value = item.getSelectedValue(); - int valuesId = item.getValuesId(); - - if (valuesId > 0) { - int[] valuesArray = mContext.getResources().getIntArray(valuesId); - for (int index = 0; index < valuesArray.length; index++) { - int current = valuesArray[index]; - if (current == value) { - return index; - } - } - } else { - return value; - } - - return -1; - } - - @Override - public void onValueChange(@NonNull Slider slider, float value, boolean fromUser) { - mSliderProgress = (int) value; - mTextSliderValue.setText(String.valueOf(mSliderProgress)); - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt new file mode 100644 index 0000000000..6d9cb97985 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt @@ -0,0 +1,503 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui + +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface +import android.icu.util.Calendar +import android.icu.util.TimeZone +import android.text.InputFilter +import android.text.format.DateFormat +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.FragmentActivity +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.datepicker.MaterialDatePicker +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.slider.Slider +import com.google.android.material.timepicker.MaterialTimePicker +import com.google.android.material.timepicker.TimeFormat +import org.citra.citra_emu.R +import org.citra.citra_emu.databinding.DialogSliderBinding +import org.citra.citra_emu.databinding.DialogSoftwareKeyboardBinding +import org.citra.citra_emu.databinding.ListItemSettingBinding +import org.citra.citra_emu.databinding.ListItemSettingSwitchBinding +import org.citra.citra_emu.databinding.ListItemSettingsHeaderBinding +import org.citra.citra_emu.features.settings.model.AbstractBooleanSetting +import org.citra.citra_emu.features.settings.model.AbstractFloatSetting +import org.citra.citra_emu.features.settings.model.AbstractIntSetting +import org.citra.citra_emu.features.settings.model.AbstractSetting +import org.citra.citra_emu.features.settings.model.AbstractStringSetting +import org.citra.citra_emu.features.settings.model.FloatSetting +import org.citra.citra_emu.features.settings.model.ScaledFloatSetting +import org.citra.citra_emu.features.settings.model.view.AbstractShortSetting +import org.citra.citra_emu.features.settings.model.view.DateTimeSetting +import org.citra.citra_emu.features.settings.model.view.InputBindingSetting +import org.citra.citra_emu.features.settings.model.view.SettingsItem +import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting +import org.citra.citra_emu.features.settings.model.view.SliderSetting +import org.citra.citra_emu.features.settings.model.view.StringInputSetting +import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting +import org.citra.citra_emu.features.settings.model.view.SubmenuSetting +import org.citra.citra_emu.features.settings.model.view.SwitchSetting +import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder +import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder +import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder +import org.citra.citra_emu.features.settings.ui.viewholder.RunnableViewHolder +import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder +import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder +import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder +import org.citra.citra_emu.features.settings.ui.viewholder.StringInputViewHolder +import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder +import org.citra.citra_emu.features.settings.ui.viewholder.SwitchSettingViewHolder +import org.citra.citra_emu.fragments.MessageDialogFragment +import org.citra.citra_emu.fragments.MotionBottomSheetDialogFragment +import org.citra.citra_emu.utils.SystemSaveGame +import java.lang.IllegalStateException +import java.lang.NumberFormatException +import java.text.SimpleDateFormat + +class SettingsAdapter( + private val fragmentView: SettingsFragmentView, + private val context: Context +) : RecyclerView.Adapter<SettingViewHolder?>(), DialogInterface.OnClickListener { + private var settings: ArrayList<SettingsItem>? = null + private var clickedItem: SettingsItem? = null + private var clickedPosition: Int + private var dialog: AlertDialog? = null + private var sliderProgress = 0 + private var textSliderValue: TextView? = null + private var textInputValue: String = "" + + private var defaultCancelListener = + DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> closeDialog() } + + init { + clickedPosition = -1 + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + SettingsItem.TYPE_HEADER -> { + HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this) + } + + SettingsItem.TYPE_SWITCH -> { + SwitchSettingViewHolder(ListItemSettingSwitchBinding.inflate(inflater), this) + } + + SettingsItem.TYPE_SINGLE_CHOICE, SettingsItem.TYPE_STRING_SINGLE_CHOICE -> { + SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this) + } + + SettingsItem.TYPE_SLIDER -> { + SliderViewHolder(ListItemSettingBinding.inflate(inflater), this) + } + + SettingsItem.TYPE_SUBMENU -> { + SubmenuViewHolder(ListItemSettingBinding.inflate(inflater), this) + } + + SettingsItem.TYPE_DATETIME_SETTING -> { + DateTimeViewHolder(ListItemSettingBinding.inflate(inflater), this) + } + + SettingsItem.TYPE_RUNNABLE -> { + RunnableViewHolder(ListItemSettingBinding.inflate(inflater), this) + } + + SettingsItem.TYPE_INPUT_BINDING -> { + InputBindingSettingViewHolder(ListItemSettingBinding.inflate(inflater), this) + } + + SettingsItem.TYPE_STRING_INPUT -> { + StringInputViewHolder(ListItemSettingBinding.inflate(inflater), this) + } + + else -> { + // TODO: Create an error view since we can't return null now + HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this) + } + } + } + + override fun onBindViewHolder(holder: SettingViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + private fun getItem(position: Int): SettingsItem { + return settings!![position] + } + + override fun getItemCount(): Int { + return if (settings != null) { + settings!!.size + } else { + 0 + } + } + + override fun getItemViewType(position: Int): Int { + return getItem(position).type + } + + fun setSettingsList(settings: ArrayList<SettingsItem>?) { + this.settings = settings + notifyDataSetChanged() + } + + fun onBooleanClick(item: SwitchSetting, position: Int, checked: Boolean) { + val setting = item.setChecked(checked) + fragmentView.putSetting(setting) + fragmentView.onSettingChanged() + } + + private fun onSingleChoiceClick(item: SingleChoiceSetting) { + clickedItem = item + val value = getSelectionForSingleChoiceValue(item) + dialog = MaterialAlertDialogBuilder(context) + .setTitle(item.nameId) + .setSingleChoiceItems(item.choicesId, value, this) + .show() + } + + fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) { + clickedPosition = position + onSingleChoiceClick(item) + } + + private fun onStringSingleChoiceClick(item: StringSingleChoiceSetting) { + clickedItem = item + dialog = MaterialAlertDialogBuilder(context) + .setTitle(item.nameId) + .setSingleChoiceItems(item.choices, item.selectValueIndex, this) + .show() + } + + fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) { + clickedPosition = position + onStringSingleChoiceClick(item) + } + + @SuppressLint("SimpleDateFormat") + fun onDateTimeClick(item: DateTimeSetting, position: Int) { + clickedItem = item + clickedPosition = position + + val storedTime: Long = try { + java.lang.Long.decode(item.value) * 1000 + } catch (e: NumberFormatException) { + val date = item.value.substringBefore(" ") + val time = item.value.substringAfter(" ") + + val formatter = SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssZZZZ") + val gmt = formatter.parse("${date}T${time}+0000") + gmt!!.time + } + + // Helper to extract hour and minute from epoch time + val calendar: Calendar = Calendar.getInstance() + calendar.timeInMillis = storedTime + calendar.timeZone = TimeZone.getTimeZone("UTC") + + var timeFormat: Int = TimeFormat.CLOCK_12H + if (DateFormat.is24HourFormat(fragmentView.activityView as AppCompatActivity)) { + timeFormat = TimeFormat.CLOCK_24H + } + + val datePicker: MaterialDatePicker<Long> = MaterialDatePicker.Builder.datePicker() + .setSelection(storedTime) + .setTitleText(R.string.select_rtc_date) + .build() + val timePicker: MaterialTimePicker = MaterialTimePicker.Builder() + .setTimeFormat(timeFormat) + .setHour(calendar.get(Calendar.HOUR_OF_DAY)) + .setMinute(calendar.get(Calendar.MINUTE)) + .setTitleText(R.string.select_rtc_time) + .build() + + datePicker.addOnPositiveButtonClickListener { + timePicker.show( + (fragmentView.activityView as AppCompatActivity).supportFragmentManager, + "TimePicker" + ) + } + timePicker.addOnPositiveButtonClickListener { + var epochTime: Long = datePicker.selection!! / 1000 + epochTime += timePicker.hour.toLong() * 60 * 60 + epochTime += timePicker.minute.toLong() * 60 + val rtcString = epochTime.toString() + if (item.value != rtcString) { + fragmentView.onSettingChanged() + } + notifyItemChanged(clickedPosition) + val setting = item.setSelectedValue(rtcString) + fragmentView.putSetting(setting) + clickedItem = null + } + datePicker.show( + (fragmentView.activityView as AppCompatActivity).supportFragmentManager, + "DatePicker" + ) + } + + fun onSliderClick(item: SliderSetting, position: Int) { + clickedItem = item + clickedPosition = position + sliderProgress = item.selectedValue + + val inflater = LayoutInflater.from(context) + val sliderBinding = DialogSliderBinding.inflate(inflater) + + textSliderValue = sliderBinding.textValue + textSliderValue!!.text = sliderProgress.toString() + sliderBinding.textUnits.text = item.units + + sliderBinding.slider.apply { + valueFrom = item.min.toFloat() + valueTo = item.max.toFloat() + value = sliderProgress.toFloat() + addOnChangeListener { _: Slider, value: Float, _: Boolean -> + sliderProgress = value.toInt() + textSliderValue!!.text = sliderProgress.toString() + } + } + + dialog = MaterialAlertDialogBuilder(context) + .setTitle(item.nameId) + .setView(sliderBinding.root) + .setPositiveButton(android.R.string.ok, this) + .setNegativeButton(android.R.string.cancel, defaultCancelListener) + .setNeutralButton(R.string.slider_default) { dialog: DialogInterface, which: Int -> + sliderBinding.slider.value = when (item.setting) { + is ScaledFloatSetting -> { + val scaledSetting = item.setting as ScaledFloatSetting + scaledSetting.defaultValue * scaledSetting.scale + } + + is FloatSetting -> (item.setting as FloatSetting).defaultValue + else -> item.defaultValue!! + } + onClick(dialog, which) + } + .show() + } + + fun onSubmenuClick(item: SubmenuSetting) { + fragmentView.loadSubMenu(item.menuKey) + } + + fun onInputBindingClick(item: InputBindingSetting, position: Int) { + val activity = fragmentView.activityView as FragmentActivity + MotionBottomSheetDialogFragment.newInstance( + item, + { closeDialog() }, + { + notifyItemChanged(position) + fragmentView.onSettingChanged() + } + ).show(activity.supportFragmentManager, MotionBottomSheetDialogFragment.TAG) + } + + fun onStringInputClick(item: StringInputSetting, position: Int) { + clickedItem = item + clickedPosition = position + textInputValue = item.selectedValue + + val inflater = LayoutInflater.from(context) + val inputBinding = DialogSoftwareKeyboardBinding.inflate(inflater) + + inputBinding.editTextInput.setText(textInputValue) + inputBinding.editTextInput.doOnTextChanged { text, _, _, _ -> + textInputValue = text.toString() + } + if (item.characterLimit != 0) { + inputBinding.editTextInput.filters = + arrayOf(InputFilter.LengthFilter(item.characterLimit)) + } + + dialog = MaterialAlertDialogBuilder(context) + .setView(inputBinding.root) + .setTitle(item.nameId) + .setPositiveButton(android.R.string.ok, this) + .setNegativeButton(android.R.string.cancel, defaultCancelListener) + .show() + } + + override fun onClick(dialog: DialogInterface, which: Int) { + when (clickedItem) { + is SingleChoiceSetting -> { + val scSetting = clickedItem as SingleChoiceSetting + val setting = when (scSetting.setting) { + is AbstractIntSetting -> { + val value = getValueForSingleChoiceSelection(scSetting, which) + if (scSetting.selectedValue != value) { + fragmentView.onSettingChanged() + } + scSetting.setSelectedValue(value) + } + + is AbstractShortSetting -> { + val value = getValueForSingleChoiceSelection(scSetting, which).toShort() + if (scSetting.selectedValue.toShort() != value) { + fragmentView.onSettingChanged() + } + scSetting.setSelectedValue(value) + } + + else -> throw IllegalStateException("Unrecognized type used for SingleChoiceSetting!") + } + + fragmentView.putSetting(setting) + closeDialog() + } + + is StringSingleChoiceSetting -> { + val scSetting = clickedItem as StringSingleChoiceSetting + val setting = when (scSetting.setting) { + is AbstractStringSetting -> { + val value = scSetting.getValueAt(which) + if (scSetting.selectedValue != value) fragmentView.onSettingChanged() + scSetting.setSelectedValue(value!!) + } + + is AbstractShortSetting -> { + if (scSetting.selectValueIndex != which) fragmentView.onSettingChanged() + scSetting.setSelectedValue(scSetting.getValueAt(which)?.toShort() ?: 1) + } + + else -> throw IllegalStateException("Unrecognized type used for StringSingleChoiceSetting!") + } + + fragmentView.putSetting(setting) + closeDialog() + } + + is SliderSetting -> { + val sliderSetting = clickedItem as SliderSetting + if (sliderSetting.selectedValue != sliderProgress) { + fragmentView.onSettingChanged() + } + when (sliderSetting.setting) { + is FloatSetting, + is ScaledFloatSetting -> { + val value = sliderProgress.toFloat() + val setting = sliderSetting.setSelectedValue(value) + fragmentView.putSetting(setting) + } + + else -> { + val setting = sliderSetting.setSelectedValue(sliderProgress) + fragmentView.putSetting(setting) + } + } + closeDialog() + } + + is StringInputSetting -> { + val inputSetting = clickedItem as StringInputSetting + if (inputSetting.selectedValue != textInputValue) { + fragmentView.onSettingChanged() + } + val setting = inputSetting.setSelectedValue(textInputValue) + fragmentView.putSetting(setting) + closeDialog() + } + } + clickedItem = null + sliderProgress = -1 + textInputValue = "" + } + + fun onLongClick(setting: AbstractSetting, position: Int): Boolean { + MaterialAlertDialogBuilder(context) + .setMessage(R.string.reset_setting_confirmation) + .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> + when (setting) { + is AbstractBooleanSetting -> setting.boolean = setting.defaultValue as Boolean + is AbstractFloatSetting -> { + if (setting is ScaledFloatSetting) { + setting.float = setting.defaultValue * setting.scale + } else { + setting.float = setting.defaultValue as Float + } + } + + is AbstractIntSetting -> setting.int = setting.defaultValue as Int + is AbstractStringSetting -> setting.string = setting.defaultValue as String + is AbstractShortSetting -> setting.short = setting.defaultValue as Short + } + notifyItemChanged(position) + fragmentView.onSettingChanged() + } + .setNegativeButton(android.R.string.cancel, null) + .show() + + return true + } + + fun onClickDisabledSetting() { + MessageDialogFragment.newInstance( + R.string.setting_not_editable, + R.string.setting_not_editable_description + ).show((fragmentView as SettingsFragment).childFragmentManager, MessageDialogFragment.TAG) + } + + fun onClickRegenerateConsoleId() { + MaterialAlertDialogBuilder(context) + .setTitle(R.string.regenerate_console_id) + .setMessage(R.string.regenerate_console_id_description) + .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> + SystemSaveGame.regenerateConsoleId() + notifyDataSetChanged() + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + fun closeDialog() { + if (dialog != null) { + if (clickedPosition != -1) { + notifyItemChanged(clickedPosition) + clickedPosition = -1 + } + dialog!!.dismiss() + dialog = null + } + } + + private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int { + val valuesId = item.valuesId + return if (valuesId > 0) { + val valuesArray = context.resources.getIntArray(valuesId) + valuesArray[which] + } else { + which + } + } + + private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int { + val value = item.selectedValue + val valuesId = item.valuesId + if (valuesId > 0) { + val valuesArray = context.resources.getIntArray(valuesId) + for (index in valuesArray.indices) { + val current = valuesArray[index] + if (current == value) { + return index + } + } + } else { + return value + } + return -1 + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.java deleted file mode 100644 index 76d4223f5c..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.java +++ /dev/null @@ -1,151 +0,0 @@ -package org.citra.citra_emu.features.settings.ui; - -import android.content.Context; -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.core.graphics.Insets; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowInsetsCompat; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.features.settings.model.Settings; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.ui.DividerItemDecoration; - -import java.util.ArrayList; - -public final class SettingsFragment extends Fragment implements SettingsFragmentView { - private static final String ARGUMENT_MENU_TAG = "menu_tag"; - private static final String ARGUMENT_GAME_ID = "game_id"; - - private SettingsFragmentPresenter mPresenter = new SettingsFragmentPresenter(this); - private SettingsActivityView mActivity; - - private SettingsAdapter mAdapter; - - private RecyclerView mRecyclerView; - - public static Fragment newInstance(String menuTag, String gameId) { - SettingsFragment fragment = new SettingsFragment(); - - Bundle arguments = new Bundle(); - arguments.putString(ARGUMENT_MENU_TAG, menuTag); - arguments.putString(ARGUMENT_GAME_ID, gameId); - - fragment.setArguments(arguments); - return fragment; - } - - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - - mActivity = (SettingsActivityView) context; - mPresenter.onAttach(); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setRetainInstance(true); - String menuTag = getArguments().getString(ARGUMENT_MENU_TAG); - String gameId = getArguments().getString(ARGUMENT_GAME_ID); - - mAdapter = new SettingsAdapter(this, getActivity()); - - mPresenter.onCreate(menuTag, gameId); - } - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_settings, container, false); - } - - @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { - LinearLayoutManager manager = new LinearLayoutManager(getActivity()); - - mRecyclerView = view.findViewById(R.id.list_settings); - - mRecyclerView.setAdapter(mAdapter); - mRecyclerView.setLayoutManager(manager); - mRecyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), null)); - - SettingsActivityView activity = (SettingsActivityView) getActivity(); - - mPresenter.onViewCreated(activity.getSettings()); - - setInsets(); - } - - @Override - public void onDetach() { - super.onDetach(); - mActivity = null; - - if (mAdapter != null) { - mAdapter.closeDialog(); - } - } - - @Override - public void onSettingsFileLoaded(Settings settings) { - mPresenter.setSettings(settings); - } - - @Override - public void passSettingsToActivity(Settings settings) { - if (mActivity != null) { - mActivity.setSettings(settings); - } - } - - @Override - public void showSettingsList(ArrayList<SettingsItem> settingsList) { - mAdapter.setSettings(settingsList); - } - - @Override - public void loadDefaultSettings() { - mPresenter.loadDefaultSettings(); - } - - @Override - public void loadSubMenu(String menuKey) { - mActivity.showSettingsFragment(menuKey, true, getArguments().getString(ARGUMENT_GAME_ID)); - } - - @Override - public void showToastMessage(String message, boolean is_long) { - mActivity.showToastMessage(message, is_long); - } - - @Override - public void putSetting(Setting setting) { - mPresenter.putSetting(setting); - } - - @Override - public void onSettingChanged() { - mActivity.onSettingChanged(); - } - - private void setInsets() { - ViewCompat.setOnApplyWindowInsetsListener(mRecyclerView, (v, windowInsets) -> { - Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); - v.setPadding(insets.left, 0, insets.right, insets.bottom); - return windowInsets; - }); - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt new file mode 100644 index 0000000000..77458cd545 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt @@ -0,0 +1,128 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.divider.MaterialDividerItemDecoration +import org.citra.citra_emu.databinding.FragmentSettingsBinding +import org.citra.citra_emu.features.settings.model.AbstractSetting +import org.citra.citra_emu.features.settings.model.view.SettingsItem + +class SettingsFragment : Fragment(), SettingsFragmentView { + override var activityView: SettingsActivityView? = null + + private val fragmentPresenter = SettingsFragmentPresenter(this) + private var settingsAdapter: SettingsAdapter? = null + + private var _binding: FragmentSettingsBinding? = null + private val binding get() = _binding!! + + override fun onAttach(context: Context) { + super.onAttach(context) + activityView = requireActivity() as SettingsActivityView + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val menuTag = requireArguments().getString(ARGUMENT_MENU_TAG) + val gameId = requireArguments().getString(ARGUMENT_GAME_ID) + fragmentPresenter.onCreate(menuTag!!, gameId!!) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentSettingsBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + settingsAdapter = SettingsAdapter(this, requireActivity()) + val dividerDecoration = MaterialDividerItemDecoration( + requireContext(), + LinearLayoutManager.VERTICAL + ) + dividerDecoration.isLastItemDecorated = false + binding.listSettings.apply { + adapter = settingsAdapter + layoutManager = LinearLayoutManager(activity) + addItemDecoration(dividerDecoration) + } + fragmentPresenter.onViewCreated(settingsAdapter!!) + + setInsets() + } + + override fun onDetach() { + super.onDetach() + activityView = null + if (settingsAdapter != null) { + settingsAdapter!!.closeDialog() + } + } + + override fun showSettingsList(settingsList: ArrayList<SettingsItem>) { + settingsAdapter!!.setSettingsList(settingsList) + } + + override fun loadSettingsList() { + fragmentPresenter.loadSettingsList() + } + + override fun loadSubMenu(menuKey: String) { + activityView!!.showSettingsFragment( + menuKey, + true, + requireArguments().getString(ARGUMENT_GAME_ID)!! + ) + } + + override fun showToastMessage(message: String?, is_long: Boolean) { + activityView!!.showToastMessage(message!!, is_long) + } + + override fun putSetting(setting: AbstractSetting) { + fragmentPresenter.putSetting(setting) + } + + override fun onSettingChanged() { + activityView!!.onSettingChanged() + } + + private fun setInsets() { + ViewCompat.setOnApplyWindowInsetsListener( + binding.listSettings + ) { view: View, windowInsets: WindowInsetsCompat -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + view.updatePadding(bottom = insets.bottom) + windowInsets + } + } + + companion object { + private const val ARGUMENT_MENU_TAG = "menu_tag" + private const val ARGUMENT_GAME_ID = "game_id" + + fun newInstance(menuTag: String?, gameId: String?): Fragment { + val fragment = SettingsFragment() + val arguments = Bundle() + arguments.putString(ARGUMENT_MENU_TAG, menuTag) + arguments.putString(ARGUMENT_GAME_ID, gameId) + fragment.arguments = arguments + return fragment + } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java deleted file mode 100644 index 3e53cf4658..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java +++ /dev/null @@ -1,410 +0,0 @@ -package org.citra.citra_emu.features.settings.ui; - -import android.app.Activity; -import android.content.Context; -import android.hardware.camera2.CameraAccessException; -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CameraManager; -import android.text.TextUtils; - -import org.citra.citra_emu.NativeLibrary; -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.features.settings.model.SettingSection; -import org.citra.citra_emu.features.settings.model.Settings; -import org.citra.citra_emu.features.settings.model.StringSetting; -import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting; -import org.citra.citra_emu.features.settings.model.view.DateTimeSetting; -import org.citra.citra_emu.features.settings.model.view.HeaderSetting; -import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting; -import org.citra.citra_emu.features.settings.model.view.SliderSetting; -import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting; -import org.citra.citra_emu.features.settings.model.view.SubmenuSetting; -import org.citra.citra_emu.features.settings.utils.SettingsFile; -import org.citra.citra_emu.utils.Log; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Objects; - -public final class SettingsFragmentPresenter { - private SettingsFragmentView mView; - - private String mMenuTag; - private String mGameID; - - private Settings mSettings; - private ArrayList<SettingsItem> mSettingsList; - - public SettingsFragmentPresenter(SettingsFragmentView view) { - mView = view; - } - - public void onCreate(String menuTag, String gameId) { - mGameID = gameId; - mMenuTag = menuTag; - } - - public void onViewCreated(Settings settings) { - setSettings(settings); - } - - /** - * If the screen is rotated, the Activity will forget the settings map. This fragment - * won't, though; so rather than have the Activity reload from disk, have the fragment pass - * the settings map back to the Activity. - */ - public void onAttach() { - if (mSettings != null) { - mView.passSettingsToActivity(mSettings); - } - } - - public void putSetting(Setting setting) { - mSettings.getSection(setting.getSection()).putSetting(setting); - } - - private StringSetting asStringSetting(Setting setting) { - if (setting == null) { - return null; - } - - StringSetting stringSetting = new StringSetting(setting.getKey(), setting.getSection(), setting.getValueAsString()); - putSetting(stringSetting); - return stringSetting; - } - - public void loadDefaultSettings() { - loadSettingsList(); - } - - public void setSettings(Settings settings) { - if (mSettingsList == null && settings != null) { - mSettings = settings; - - loadSettingsList(); - } else { - mView.getActivity().setTitle(R.string.preferences_settings); - mView.showSettingsList(mSettingsList); - } - } - - private void loadSettingsList() { - if (!TextUtils.isEmpty(mGameID)) { - mView.getActivity().setTitle("Game Settings: " + mGameID); - } - ArrayList<SettingsItem> sl = new ArrayList<>(); - - if (mMenuTag == null) { - return; - } - - switch (mMenuTag) { - case SettingsFile.FILE_NAME_CONFIG: - addConfigSettings(sl); - break; - case Settings.SECTION_CORE: - addGeneralSettings(sl); - break; - case Settings.SECTION_SYSTEM: - addSystemSettings(sl); - break; - case Settings.SECTION_CAMERA: - addCameraSettings(sl); - break; - case Settings.SECTION_CONTROLS: - addInputSettings(sl); - break; - case Settings.SECTION_RENDERER: - addGraphicsSettings(sl); - break; - case Settings.SECTION_AUDIO: - addAudioSettings(sl); - break; - case Settings.SECTION_DEBUG: - addDebugSettings(sl); - break; - default: - mView.showToastMessage("Unimplemented menu", false); - return; - } - - mSettingsList = sl; - mView.showSettingsList(mSettingsList); - } - - private void addConfigSettings(ArrayList<SettingsItem> sl) { - mView.getActivity().setTitle(R.string.preferences_settings); - - sl.add(new SubmenuSetting(null, null, R.string.preferences_general, 0, Settings.SECTION_CORE)); - sl.add(new SubmenuSetting(null, null, R.string.preferences_system, 0, Settings.SECTION_SYSTEM)); - sl.add(new SubmenuSetting(null, null, R.string.preferences_camera, 0, Settings.SECTION_CAMERA)); - sl.add(new SubmenuSetting(null, null, R.string.preferences_controls, 0, Settings.SECTION_CONTROLS)); - sl.add(new SubmenuSetting(null, null, R.string.preferences_graphics, 0, Settings.SECTION_RENDERER)); - sl.add(new SubmenuSetting(null, null, R.string.preferences_audio, 0, Settings.SECTION_AUDIO)); - sl.add(new SubmenuSetting(null, null, R.string.preferences_debug, 0, Settings.SECTION_DEBUG)); - } - - private void addGeneralSettings(ArrayList<SettingsItem> sl) { - mView.getActivity().setTitle(R.string.preferences_general); - - SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER); - Setting frameLimitEnable = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED); - Setting frameLimitValue = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT); - - sl.add(new CheckBoxSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED, Settings.SECTION_RENDERER, R.string.frame_limit_enable, R.string.frame_limit_enable_description, true, frameLimitEnable)); - sl.add(new SliderSetting(SettingsFile.KEY_FRAME_LIMIT, Settings.SECTION_RENDERER, R.string.frame_limit_slider, R.string.frame_limit_slider_description, 1, 200, "%", 100, frameLimitValue)); - } - - private void addSystemSettings(ArrayList<SettingsItem> sl) { - mView.getActivity().setTitle(R.string.preferences_system); - - SettingSection systemSection = mSettings.getSection(Settings.SECTION_SYSTEM); - Setting region = systemSection.getSetting(SettingsFile.KEY_REGION_VALUE); - Setting language = systemSection.getSetting(SettingsFile.KEY_LANGUAGE); - Setting systemClock = systemSection.getSetting(SettingsFile.KEY_INIT_CLOCK); - Setting dateTime = systemSection.getSetting(SettingsFile.KEY_INIT_TIME); - Setting pluginLoader = systemSection.getSetting(SettingsFile.KEY_PLUGIN_LOADER); - Setting allowPluginLoader = systemSection.getSetting(SettingsFile.KEY_ALLOW_PLUGIN_LOADER); - - sl.add(new SingleChoiceSetting(SettingsFile.KEY_REGION_VALUE, Settings.SECTION_SYSTEM, R.string.emulated_region, 0, R.array.regionNames, R.array.regionValues, -1, region)); - sl.add(new SingleChoiceSetting(SettingsFile.KEY_LANGUAGE, Settings.SECTION_SYSTEM, R.string.emulated_language, 0, R.array.languageNames, R.array.languageValues, 1, language)); - - sl.add(new HeaderSetting(null, null, R.string.clock, 0)); - sl.add(new SingleChoiceSetting(SettingsFile.KEY_INIT_CLOCK, Settings.SECTION_SYSTEM, R.string.init_clock, R.string.init_clock_description, R.array.systemClockNames, R.array.systemClockValues, 0, systemClock)); - sl.add(new DateTimeSetting(SettingsFile.KEY_INIT_TIME, Settings.SECTION_SYSTEM, R.string.init_time, R.string.init_time_description, "2000-01-01 00:00:01", dateTime)); - - sl.add(new HeaderSetting(null, null, R.string.plugin_loader, 0)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_PLUGIN_LOADER, Settings.SECTION_SYSTEM, R.string.plugin_loader, R.string.plugin_loader_description, false, pluginLoader)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_ALLOW_PLUGIN_LOADER, Settings.SECTION_SYSTEM, R.string.allow_plugin_loader, R.string.allow_plugin_loader_description, true, allowPluginLoader)); - } - - private void addCameraSettings(ArrayList<SettingsItem> sl) { - final Activity activity = mView.getActivity(); - activity.setTitle(R.string.preferences_camera); - - // Get the camera IDs - CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); - ArrayList<String> supportedCameraNameList = new ArrayList<>(); - ArrayList<String> supportedCameraIdList = new ArrayList<>(); - if (cameraManager != null) { - try { - for (String id : cameraManager.getCameraIdList()) { - final CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); - if (Objects.requireNonNull(characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)) == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) { - continue; // Legacy cameras cannot be used with the NDK - } - - supportedCameraIdList.add(id); - - final int facing = Objects.requireNonNull(characteristics.get(CameraCharacteristics.LENS_FACING)); - int stringId = R.string.camera_facing_external; - switch (facing) { - case CameraCharacteristics.LENS_FACING_FRONT: - stringId = R.string.camera_facing_front; - break; - case CameraCharacteristics.LENS_FACING_BACK: - stringId = R.string.camera_facing_back; - break; - case CameraCharacteristics.LENS_FACING_EXTERNAL: - stringId = R.string.camera_facing_external; - break; - } - supportedCameraNameList.add(String.format("%1$s (%2$s)", id, activity.getString(stringId))); - } - } catch (CameraAccessException e) { - Log.error("Couldn't retrieve camera list"); - e.printStackTrace(); - } - } - - // Create the names and values for display - ArrayList<String> cameraDeviceNameList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceNames))); - cameraDeviceNameList.addAll(supportedCameraNameList); - ArrayList<String> cameraDeviceValueList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceValues))); - cameraDeviceValueList.addAll(supportedCameraIdList); - - final String[] cameraDeviceNames = cameraDeviceNameList.toArray(new String[]{}); - final String[] cameraDeviceValues = cameraDeviceValueList.toArray(new String[]{}); - - final boolean haveCameraDevices = !supportedCameraIdList.isEmpty(); - - String[] imageSourceNames = activity.getResources().getStringArray(R.array.cameraImageSourceNames); - String[] imageSourceValues = activity.getResources().getStringArray(R.array.cameraImageSourceValues); - if (!haveCameraDevices) { - // Remove the last entry (ndk / Device Camera) - imageSourceNames = Arrays.copyOfRange(imageSourceNames, 0, imageSourceNames.length - 1); - imageSourceValues = Arrays.copyOfRange(imageSourceValues, 0, imageSourceValues.length - 1); - } - - final String defaultImageSource = haveCameraDevices ? "ndk" : "image"; - - SettingSection cameraSection = mSettings.getSection(Settings.SECTION_CAMERA); - - Setting innerCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_NAME); - Setting innerCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG)); - Setting innerCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_FLIP); - sl.add(new HeaderSetting(null, null, R.string.inner_camera, 0)); - sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, innerCameraImageSource)); - if (haveCameraDevices) - sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_front", innerCameraConfig)); - sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, innerCameraFlip)); - - Setting outerLeftCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME); - Setting outerLeftCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG)); - Setting outerLeftCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP); - sl.add(new HeaderSetting(null, null, R.string.outer_left_camera, 0)); - sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerLeftCameraImageSource)); - if (haveCameraDevices) - sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerLeftCameraConfig)); - sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerLeftCameraFlip)); - - Setting outerRightCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME); - Setting outerRightCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG)); - Setting outerRightCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP); - sl.add(new HeaderSetting(null, null, R.string.outer_right_camera, 0)); - sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerRightCameraImageSource)); - if (haveCameraDevices) - sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerRightCameraConfig)); - sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerRightCameraFlip)); - } - - private void addInputSettings(ArrayList<SettingsItem> sl) { - mView.getActivity().setTitle(R.string.preferences_controls); - - SettingSection controlsSection = mSettings.getSection(Settings.SECTION_CONTROLS); - Setting buttonA = controlsSection.getSetting(SettingsFile.KEY_BUTTON_A); - Setting buttonB = controlsSection.getSetting(SettingsFile.KEY_BUTTON_B); - Setting buttonX = controlsSection.getSetting(SettingsFile.KEY_BUTTON_X); - Setting buttonY = controlsSection.getSetting(SettingsFile.KEY_BUTTON_Y); - Setting buttonSelect = controlsSection.getSetting(SettingsFile.KEY_BUTTON_SELECT); - Setting buttonStart = controlsSection.getSetting(SettingsFile.KEY_BUTTON_START); - Setting circlepadAxisVert = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL); - Setting circlepadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL); - Setting cstickAxisVert = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL); - Setting cstickAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL); - Setting dpadAxisVert = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL); - Setting dpadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL); - // Setting buttonUp = controlsSection.getSetting(SettingsFile.KEY_BUTTON_UP); - // Setting buttonDown = controlsSection.getSetting(SettingsFile.KEY_BUTTON_DOWN); - // Setting buttonLeft = controlsSection.getSetting(SettingsFile.KEY_BUTTON_LEFT); - // Setting buttonRight = controlsSection.getSetting(SettingsFile.KEY_BUTTON_RIGHT); - Setting buttonL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_L); - Setting buttonR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_R); - Setting buttonZL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZL); - Setting buttonZR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZR); - - sl.add(new HeaderSetting(null, null, R.string.generic_buttons, 0)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_A, Settings.SECTION_CONTROLS, R.string.button_a, buttonA)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_B, Settings.SECTION_CONTROLS, R.string.button_b, buttonB)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_X, Settings.SECTION_CONTROLS, R.string.button_x, buttonX)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_Y, Settings.SECTION_CONTROLS, R.string.button_y, buttonY)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_SELECT, Settings.SECTION_CONTROLS, R.string.button_select, buttonSelect)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_START, Settings.SECTION_CONTROLS, R.string.button_start, buttonStart)); - - sl.add(new HeaderSetting(null, null, R.string.controller_circlepad, 0)); - sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, circlepadAxisVert)); - sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, circlepadAxisHoriz)); - - sl.add(new HeaderSetting(null, null, R.string.controller_c, 0)); - sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, cstickAxisVert)); - sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, cstickAxisHoriz)); - - sl.add(new HeaderSetting(null, null, R.string.controller_dpad, 0)); - sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, dpadAxisVert)); - sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, dpadAxisHoriz)); - - // TODO(bunnei): Figure out what to do with these. Configuring is functional, but removing for MVP because they are confusing. - // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_UP, Settings.SECTION_CONTROLS, R.string.generic_up, buttonUp)); - // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_DOWN, Settings.SECTION_CONTROLS, R.string.generic_down, buttonDown)); - // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_LEFT, Settings.SECTION_CONTROLS, R.string.generic_left, buttonLeft)); - // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_RIGHT, Settings.SECTION_CONTROLS, R.string.generic_right, buttonRight)); - - sl.add(new HeaderSetting(null, null, R.string.controller_triggers, 0)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_L, Settings.SECTION_CONTROLS, R.string.button_l, buttonL)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_R, Settings.SECTION_CONTROLS, R.string.button_r, buttonR)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZL, Settings.SECTION_CONTROLS, R.string.button_zl, buttonZL)); - sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZR, Settings.SECTION_CONTROLS, R.string.button_zr, buttonZR)); - } - - private void addGraphicsSettings(ArrayList<SettingsItem> sl) { - mView.getActivity().setTitle(R.string.preferences_graphics); - - SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER); - Setting graphicsApi = rendererSection.getSetting(SettingsFile.KEY_GRAPHICS_API); - Setting spirvShaderGen = rendererSection.getSetting(SettingsFile.KEY_SPIRV_SHADER_GEN); - Setting asyncShaders = rendererSection.getSetting(SettingsFile.KEY_ASYNC_SHADERS); - Setting resolutionFactor = rendererSection.getSetting(SettingsFile.KEY_RESOLUTION_FACTOR); - Setting filterMode = rendererSection.getSetting(SettingsFile.KEY_FILTER_MODE); - Setting shadersAccurateMul = rendererSection.getSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL); - Setting render3dMode = rendererSection.getSetting(SettingsFile.KEY_RENDER_3D); - Setting factor3d = rendererSection.getSetting(SettingsFile.KEY_FACTOR_3D); - Setting useDiskShaderCache = rendererSection.getSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE); - Setting textureFilterName = rendererSection.getSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME); - SettingSection layoutSection = mSettings.getSection(Settings.SECTION_LAYOUT); - Setting cardboardScreenSize = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE); - Setting cardboardXShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT); - Setting cardboardYShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_Y_SHIFT); - SettingSection utilitySection = mSettings.getSection(Settings.SECTION_UTILITY); - Setting dumpTextures = utilitySection.getSetting(SettingsFile.KEY_DUMP_TEXTURES); - Setting customTextures = utilitySection.getSetting(SettingsFile.KEY_CUSTOM_TEXTURES); - Setting asyncCustomLoading = utilitySection.getSetting(SettingsFile.KEY_ASYNC_CUSTOM_LOADING); - //Setting preloadTextures = utilitySection.getSetting(SettingsFile.KEY_PRELOAD_TEXTURES); - - sl.add(new HeaderSetting(null, null, R.string.renderer, 0)); - sl.add(new SingleChoiceSetting(SettingsFile.KEY_GRAPHICS_API, Settings.SECTION_RENDERER, R.string.graphics_api, 0, R.array.graphicsApiNames, R.array.graphicsApiValues, 0, graphicsApi)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_SPIRV_SHADER_GEN, Settings.SECTION_RENDERER, R.string.spirv_shader_gen, R.string.spirv_shader_gen_description, true, spirvShaderGen)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_ASYNC_SHADERS, Settings.SECTION_RENDERER, R.string.async_shaders, R.string.async_shaders_description, false, asyncShaders)); - sl.add(new SliderSetting(SettingsFile.KEY_RESOLUTION_FACTOR, Settings.SECTION_RENDERER, R.string.internal_resolution, R.string.internal_resolution_description, 1, 4, "x", 1, resolutionFactor)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_FILTER_MODE, Settings.SECTION_RENDERER, R.string.linear_filtering, R.string.linear_filtering_description, true, filterMode)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL, Settings.SECTION_RENDERER, R.string.shaders_accurate_mul, R.string.shaders_accurate_mul_description, false, shadersAccurateMul)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE, Settings.SECTION_RENDERER, R.string.use_disk_shader_cache, R.string.use_disk_shader_cache_description, true, useDiskShaderCache)); - sl.add(new SingleChoiceSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME, Settings.SECTION_RENDERER, R.string.texture_filter_name, 0, R.array.textureFilterNames, R.array.textureFilterValues, 0, textureFilterName)); - - sl.add(new HeaderSetting(null, null, R.string.stereoscopy, 0)); - sl.add(new SingleChoiceSetting(SettingsFile.KEY_RENDER_3D, Settings.SECTION_RENDERER, R.string.render3d, 0, R.array.render3dModes, R.array.render3dValues, 0, render3dMode)); - sl.add(new SliderSetting(SettingsFile.KEY_FACTOR_3D, Settings.SECTION_RENDERER, R.string.factor3d, R.string.factor3d_description, 0, 100, "%", 0, factor3d)); - - sl.add(new HeaderSetting(null, null, R.string.cardboard_vr, 0)); - sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE, Settings.SECTION_LAYOUT, R.string.cardboard_screen_size, R.string.cardboard_screen_size_description, 30, 100, "%", 85, cardboardScreenSize)); - sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT, Settings.SECTION_LAYOUT, R.string.cardboard_x_shift, R.string.cardboard_x_shift_description, -100, 100, "%", 0, cardboardXShift)); - sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_Y_SHIFT, Settings.SECTION_LAYOUT, R.string.cardboard_y_shift, R.string.cardboard_y_shift_description, -100, 100, "%", 0, cardboardYShift)); - - sl.add(new HeaderSetting(null, null, R.string.utility, 0)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_DUMP_TEXTURES, Settings.SECTION_UTILITY, R.string.dump_textures, R.string.dump_textures_description, false, dumpTextures)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_CUSTOM_TEXTURES, Settings.SECTION_UTILITY, R.string.custom_textures, R.string.custom_textures_description, false, customTextures)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_ASYNC_CUSTOM_LOADING, Settings.SECTION_UTILITY, R.string.async_custom_loading, R.string.async_custom_loading_description, true, asyncCustomLoading)); - //Disabled until custom texture implementation gets rewrite, current one overloads RAM and crashes Citra. - //sl.add(new CheckBoxSetting(SettingsFile.KEY_PRELOAD_TEXTURES, Settings.SECTION_UTILITY, R.string.preload_textures, R.string.preload_textures_description, false, preloadTextures)); - } - - private void addAudioSettings(ArrayList<SettingsItem> sl) { - mView.getActivity().setTitle(R.string.preferences_audio); - - SettingSection audioSection = mSettings.getSection(Settings.SECTION_AUDIO); - Setting audioStretch = audioSection.getSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING); - Setting audioInputType = audioSection.getSetting(SettingsFile.KEY_AUDIO_INPUT_TYPE); - - sl.add(new CheckBoxSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING, Settings.SECTION_AUDIO, R.string.audio_stretch, R.string.audio_stretch_description, true, audioStretch)); - sl.add(new SingleChoiceSetting(SettingsFile.KEY_AUDIO_INPUT_TYPE, Settings.SECTION_AUDIO, R.string.audio_input_type, 0, R.array.audioInputTypeNames, R.array.audioInputTypeValues, 0, audioInputType)); - } - - private void addDebugSettings(ArrayList<SettingsItem> sl) { - mView.getActivity().setTitle(R.string.preferences_debug); - - SettingSection coreSection = mSettings.getSection(Settings.SECTION_CORE); - SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER); - Setting useCpuJit = coreSection.getSetting(SettingsFile.KEY_CPU_JIT); - Setting hardwareShader = rendererSection.getSetting(SettingsFile.KEY_HW_SHADER); - Setting vsyncEnable = rendererSection.getSetting(SettingsFile.KEY_USE_VSYNC); - Setting rendererDebug = rendererSection.getSetting(SettingsFile.KEY_RENDERER_DEBUG); - - sl.add(new HeaderSetting(null, null, R.string.debug_warning, 0)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_CPU_JIT, Settings.SECTION_CORE, R.string.cpu_jit, R.string.cpu_jit_description, true, useCpuJit, true, mView)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_HW_SHADER, Settings.SECTION_RENDERER, R.string.hw_shaders, R.string.hw_shaders_description, true, hardwareShader, true, mView)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_VSYNC, Settings.SECTION_RENDERER, R.string.vsync, R.string.vsync_description, true, vsyncEnable)); - sl.add(new CheckBoxSetting(SettingsFile.KEY_RENDERER_DEBUG, Settings.SECTION_DEBUG, R.string.renderer_debug, R.string.renderer_debug_description, false, rendererDebug)); - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt new file mode 100644 index 0000000000..de0cc18266 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -0,0 +1,1040 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui + +import android.content.Context +import android.content.SharedPreferences +import android.hardware.camera2.CameraAccessException +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraManager +import android.os.Build +import android.text.TextUtils +import androidx.preference.PreferenceManager +import org.citra.citra_emu.CitraApplication +import org.citra.citra_emu.R +import org.citra.citra_emu.features.settings.model.AbstractBooleanSetting +import org.citra.citra_emu.features.settings.model.AbstractIntSetting +import org.citra.citra_emu.features.settings.model.AbstractSetting +import org.citra.citra_emu.features.settings.model.AbstractStringSetting +import org.citra.citra_emu.features.settings.model.BooleanSetting +import org.citra.citra_emu.features.settings.model.IntSetting +import org.citra.citra_emu.features.settings.model.ScaledFloatSetting +import org.citra.citra_emu.features.settings.model.Settings +import org.citra.citra_emu.features.settings.model.StringSetting +import org.citra.citra_emu.features.settings.model.view.AbstractShortSetting +import org.citra.citra_emu.features.settings.model.view.DateTimeSetting +import org.citra.citra_emu.features.settings.model.view.HeaderSetting +import org.citra.citra_emu.features.settings.model.view.InputBindingSetting +import org.citra.citra_emu.features.settings.model.view.RunnableSetting +import org.citra.citra_emu.features.settings.model.view.SettingsItem +import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting +import org.citra.citra_emu.features.settings.model.view.SliderSetting +import org.citra.citra_emu.features.settings.model.view.StringInputSetting +import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting +import org.citra.citra_emu.features.settings.model.view.SubmenuSetting +import org.citra.citra_emu.features.settings.model.view.SwitchSetting +import org.citra.citra_emu.features.settings.utils.SettingsFile +import org.citra.citra_emu.fragments.ResetSettingsDialogFragment +import org.citra.citra_emu.utils.BirthdayMonth +import org.citra.citra_emu.utils.SystemSaveGame +import org.citra.citra_emu.utils.Log +import org.citra.citra_emu.utils.ThemeUtil + +class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) { + private var menuTag: String? = null + private lateinit var gameId: String + private var settingsList: ArrayList<SettingsItem>? = null + + private val settingsActivity get() = fragmentView.activityView as SettingsActivity + private val settings get() = fragmentView.activityView!!.settings + private lateinit var settingsAdapter: SettingsAdapter + + private lateinit var preferences: SharedPreferences + + fun onCreate(menuTag: String, gameId: String) { + this.gameId = gameId + this.menuTag = menuTag + } + + fun onViewCreated(settingsAdapter: SettingsAdapter) { + this.settingsAdapter = settingsAdapter + preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) + loadSettingsList() + } + + fun putSetting(setting: AbstractSetting) { + if (setting.section == null || setting.key == null) { + return + } + + val section = settings.getSection(setting.section!!)!! + if (section.getSetting(setting.key!!) == null) { + section.putSetting(setting) + } + } + + fun loadSettingsList() { + if (!TextUtils.isEmpty(gameId)) { + settingsActivity.setToolbarTitle("Game Settings: $gameId") + } + val sl = ArrayList<SettingsItem>() + if (menuTag == null) { + return + } + when (menuTag) { + SettingsFile.FILE_NAME_CONFIG -> addConfigSettings(sl) + Settings.SECTION_CORE -> addGeneralSettings(sl) + Settings.SECTION_SYSTEM -> addSystemSettings(sl) + Settings.SECTION_CAMERA -> addCameraSettings(sl) + Settings.SECTION_CONTROLS -> addControlsSettings(sl) + Settings.SECTION_RENDERER -> addGraphicsSettings(sl) + Settings.SECTION_AUDIO -> addAudioSettings(sl) + Settings.SECTION_DEBUG -> addDebugSettings(sl) + Settings.SECTION_THEME -> addThemeSettings(sl) + else -> { + fragmentView.showToastMessage("Unimplemented menu", false) + return + } + } + settingsList = sl + fragmentView.showSettingsList(settingsList!!) + } + + private fun addConfigSettings(sl: ArrayList<SettingsItem>) { + settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_settings)) + sl.apply { + add( + SubmenuSetting( + R.string.preferences_general, + 0, + Settings.SECTION_CORE + ) + ) + add( + SubmenuSetting( + R.string.preferences_system, + 0, + Settings.SECTION_SYSTEM + ) + ) + add( + SubmenuSetting( + R.string.preferences_camera, + 0, + Settings.SECTION_CAMERA + ) + ) + add( + SubmenuSetting( + R.string.preferences_controls, + 0, + Settings.SECTION_CONTROLS + ) + ) + add( + SubmenuSetting( + R.string.preferences_graphics, + 0, + Settings.SECTION_RENDERER + ) + ) + add( + SubmenuSetting( + R.string.preferences_audio, + 0, + Settings.SECTION_AUDIO + ) + ) + add( + SubmenuSetting( + R.string.preferences_debug, + 0, + Settings.SECTION_DEBUG + ) + ) + add( + RunnableSetting( + R.string.reset_to_default, + 0, + false, + { + ResetSettingsDialogFragment().show( + settingsActivity.supportFragmentManager, + ResetSettingsDialogFragment.TAG + ) + } + ) + ) + } + } + + private fun addGeneralSettings(sl: ArrayList<SettingsItem>) { + settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_general)) + sl.apply { + add( + SwitchSetting( + IntSetting.USE_FRAME_LIMIT, + R.string.frame_limit_enable, + R.string.frame_limit_enable_description, + IntSetting.USE_FRAME_LIMIT.key, + IntSetting.USE_FRAME_LIMIT.defaultValue + ) + ) + add( + SliderSetting( + IntSetting.FRAME_LIMIT, + R.string.frame_limit_slider, + R.string.frame_limit_slider_description, + 1, + 200, + "%", + IntSetting.FRAME_LIMIT.key, + IntSetting.FRAME_LIMIT.defaultValue.toFloat() + ) + ) + } + } + + @OptIn(ExperimentalStdlibApi::class) + private fun addSystemSettings(sl: ArrayList<SettingsItem>) { + settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_system)) + sl.apply { + val usernameSetting = object : AbstractStringSetting { + override var string: String + get() = SystemSaveGame.getUsername() + set(value) = SystemSaveGame.setUsername(value) + override val key = null + override val section = null + override val isRuntimeEditable = false + override val valueAsString get() = string + override val defaultValue = "CITRA" + } + add( + StringInputSetting( + usernameSetting, + R.string.username, + 0, + "CITRA", + 10 + ) + ) + add( + SingleChoiceSetting( + IntSetting.EMULATED_REGION, + R.string.emulated_region, + 0, + R.array.regionNames, + R.array.regionValues, + IntSetting.EMULATED_REGION.key, + IntSetting.EMULATED_REGION.defaultValue + ) + ) + + val systemLanguageSetting = object : AbstractIntSetting { + override var int: Int + get() = SystemSaveGame.getSystemLanguage() + set(value) = SystemSaveGame.setSystemLanguage(value) + override val key = null + override val section = null + override val isRuntimeEditable = false + override val valueAsString get() = int.toString() + override val defaultValue = 1 + } + add( + SingleChoiceSetting( + systemLanguageSetting, + R.string.emulated_language, + 0, + R.array.languageNames, + R.array.languageValues + ) + ) + + val systemCountrySetting = object : AbstractShortSetting { + override var short: Short + get() = SystemSaveGame.getCountryCode() + set(value) = SystemSaveGame.setCountryCode(value) + override val key = null + override val section = null + override val isRuntimeEditable = false + override val valueAsString = short.toString() + override val defaultValue: Short = 49 + } + var index = -1 + val countries = settingsActivity.resources.getStringArray(R.array.countries) + .mapNotNull { + index++ + if (it.isNotEmpty()) it to index.toString() else null + } + add( + StringSingleChoiceSetting( + systemCountrySetting, + R.string.country, + 0, + countries.map { it.first }.toTypedArray(), + countries.map { it.second }.toTypedArray() + ) + ) + + val playCoinSettings = object : AbstractIntSetting { + override var int: Int + get() = SystemSaveGame.getPlayCoins() + set(value) = SystemSaveGame.setPlayCoins(value) + override val key = null + override val section = null + override val isRuntimeEditable = false + override val valueAsString = int.toString() + override val defaultValue = 42 + } + add( + SliderSetting( + playCoinSettings, + R.string.play_coins, + 0, + 0, + 300, + "" + ) + ) + add( + RunnableSetting( + R.string.console_id, + 0, + false, + { settingsAdapter.onClickRegenerateConsoleId() }, + { "0x${SystemSaveGame.getConsoleId().toHexString().uppercase()}" } + ) + ) + + add(HeaderSetting(R.string.birthday)) + val systemBirthdayMonthSetting = object : AbstractShortSetting { + override var short: Short + get() = SystemSaveGame.getBirthday()[0] + set(value) { + val birthdayDay = SystemSaveGame.getBirthday()[1] + val daysInNewMonth = BirthdayMonth.getMonthFromCode(value)?.days ?: 31 + if (daysInNewMonth < birthdayDay) { + SystemSaveGame.setBirthday(value, 1) + settingsAdapter.notifyDataSetChanged() + } else { + SystemSaveGame.setBirthday(value, birthdayDay) + } + } + override val key = null + override val section = null + override val isRuntimeEditable = false + override val valueAsString get() = short.toString() + override val defaultValue: Short = 3 + } + add( + SingleChoiceSetting( + systemBirthdayMonthSetting, + R.string.birthday_month, + 0, + R.array.months, + R.array.monthValues + ) + ) + + val systemBirthdayDaySetting = object : AbstractShortSetting { + override var short: Short + get() = SystemSaveGame.getBirthday()[1] + set(value) { + val birthdayMonth = SystemSaveGame.getBirthday()[0] + val daysInNewMonth = + BirthdayMonth.getMonthFromCode(birthdayMonth)?.days ?: 31 + if (value > daysInNewMonth) { + SystemSaveGame.setBirthday(birthdayMonth, 1) + } else { + SystemSaveGame.setBirthday(birthdayMonth, value) + } + } + override val key = null + override val section = null + override val isRuntimeEditable = false + override val valueAsString get() = short.toString() + override val defaultValue: Short = 25 + } + val birthdayMonth = SystemSaveGame.getBirthday()[0] + val daysInMonth = BirthdayMonth.getMonthFromCode(birthdayMonth)?.days ?: 31 + val dayArray = Array(daysInMonth) { "${it + 1}" } + add( + StringSingleChoiceSetting( + systemBirthdayDaySetting, + R.string.birthday_day, + 0, + dayArray, + dayArray + ) + ) + + add(HeaderSetting(R.string.clock)) + add( + SingleChoiceSetting( + IntSetting.INIT_CLOCK, + R.string.init_clock, + R.string.init_clock_description, + R.array.systemClockNames, + R.array.systemClockValues, + IntSetting.INIT_CLOCK.key, + IntSetting.INIT_CLOCK.defaultValue + ) + ) + add( + DateTimeSetting( + StringSetting.INIT_TIME, + R.string.simulated_clock, + R.string.init_time_description, + StringSetting.INIT_TIME.key, + StringSetting.INIT_TIME.defaultValue + ) + ) + + add(HeaderSetting(R.string.plugin_loader)) + add( + SwitchSetting( + BooleanSetting.PLUGIN_LOADER, + R.string.plugin_loader, + R.string.plugin_loader_description, + BooleanSetting.PLUGIN_LOADER.key, + BooleanSetting.PLUGIN_LOADER.defaultValue + ) + ) + add( + SwitchSetting( + BooleanSetting.ALLOW_PLUGIN_LOADER, + R.string.allow_plugin_loader, + R.string.allow_plugin_loader_description, + BooleanSetting.ALLOW_PLUGIN_LOADER.key, + BooleanSetting.ALLOW_PLUGIN_LOADER.defaultValue + ) + ) + } + } + + private fun addCameraSettings(sl: ArrayList<SettingsItem>) { + settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.camera)) + + // Get the camera IDs + val cameraManager = + settingsActivity.getSystemService(Context.CAMERA_SERVICE) as CameraManager? + val supportedCameraNameList = ArrayList<String>() + val supportedCameraIdList = ArrayList<String>() + if (cameraManager != null) { + try { + for (id in cameraManager.cameraIdList) { + val characteristics = cameraManager.getCameraCharacteristics(id) + if (characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL) == + CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY + ) { + continue // Legacy cameras cannot be used with the NDK + } + supportedCameraIdList.add(id) + val facing = characteristics.get(CameraCharacteristics.LENS_FACING) + var stringId: Int = R.string.camera_facing_external + when (facing) { + CameraCharacteristics.LENS_FACING_FRONT -> stringId = + R.string.camera_facing_front + + CameraCharacteristics.LENS_FACING_BACK -> stringId = + R.string.camera_facing_back + + CameraCharacteristics.LENS_FACING_EXTERNAL -> stringId = + R.string.camera_facing_external + } + supportedCameraNameList.add( + String.format("%1\$s (%2\$s)", id, settingsActivity.getString(stringId)) + ) + } + } catch (e: CameraAccessException) { + Log.error("Couldn't retrieve camera list") + e.printStackTrace() + } + } + + // Create the names and values for display + val cameraDeviceNameList = + settingsActivity.resources.getStringArray(R.array.cameraDeviceNames).toMutableList() + cameraDeviceNameList.addAll(supportedCameraNameList) + val cameraDeviceValueList = + settingsActivity.resources.getStringArray(R.array.cameraDeviceValues).toMutableList() + cameraDeviceValueList.addAll(supportedCameraIdList) + + val haveCameraDevices = supportedCameraIdList.isNotEmpty() + + val imageSourceNames = + settingsActivity.resources.getStringArray(R.array.cameraImageSourceNames) + val imageSourceValues = + settingsActivity.resources.getStringArray(R.array.cameraImageSourceValues) + if (!haveCameraDevices) { + // Remove the last entry (ndk / Device Camera) + imageSourceNames.copyOfRange(0, imageSourceNames.size - 1) + imageSourceValues.copyOfRange(0, imageSourceValues.size - 1) + } + + sl.apply { + add(HeaderSetting(R.string.inner_camera)) + add( + StringSingleChoiceSetting( + StringSetting.CAMERA_INNER_NAME, + R.string.image_source, + R.string.image_source_description, + imageSourceNames, + imageSourceValues, + StringSetting.CAMERA_INNER_NAME.key, + StringSetting.CAMERA_INNER_NAME.defaultValue + ) + ) + if (haveCameraDevices) { + add( + StringSingleChoiceSetting( + StringSetting.CAMERA_INNER_CONFIG, + R.string.camera_device, + R.string.camera_device_description, + cameraDeviceNameList.toTypedArray(), + cameraDeviceValueList.toTypedArray(), + StringSetting.CAMERA_INNER_CONFIG.key, + StringSetting.CAMERA_INNER_CONFIG.defaultValue + ) + ) + } + add( + SingleChoiceSetting( + IntSetting.CAMERA_INNER_FLIP, + R.string.image_flip, + 0, + R.array.cameraFlipNames, + R.array.cameraDeviceValues, + IntSetting.CAMERA_INNER_FLIP.key, + IntSetting.CAMERA_INNER_FLIP.defaultValue + ) + ) + + add(HeaderSetting(R.string.outer_left_camera)) + add( + StringSingleChoiceSetting( + StringSetting.CAMERA_OUTER_LEFT_NAME, + R.string.image_source, + R.string.image_source_description, + imageSourceNames, + imageSourceValues, + StringSetting.CAMERA_OUTER_LEFT_NAME.key, + StringSetting.CAMERA_OUTER_LEFT_NAME.defaultValue + ) + ) + if (haveCameraDevices) { + add( + StringSingleChoiceSetting( + StringSetting.CAMERA_OUTER_LEFT_CONFIG, + R.string.camera_device, + R.string.camera_device_description, + cameraDeviceNameList.toTypedArray(), + cameraDeviceValueList.toTypedArray(), + StringSetting.CAMERA_OUTER_LEFT_CONFIG.key, + StringSetting.CAMERA_OUTER_LEFT_CONFIG.defaultValue + ) + ) + } + add( + SingleChoiceSetting( + IntSetting.CAMERA_OUTER_LEFT_FLIP, + R.string.image_flip, + 0, + R.array.cameraFlipNames, + R.array.cameraDeviceValues, + IntSetting.CAMERA_OUTER_LEFT_FLIP.key, + IntSetting.CAMERA_OUTER_LEFT_FLIP.defaultValue + ) + ) + + add(HeaderSetting(R.string.outer_right_camera)) + add( + StringSingleChoiceSetting( + StringSetting.CAMERA_OUTER_RIGHT_NAME, + R.string.image_source, + R.string.image_source_description, + imageSourceNames, + imageSourceValues, + StringSetting.CAMERA_OUTER_RIGHT_NAME.key, + StringSetting.CAMERA_OUTER_RIGHT_NAME.defaultValue + ) + ) + if (haveCameraDevices) { + add( + StringSingleChoiceSetting( + StringSetting.CAMERA_OUTER_RIGHT_CONFIG, + R.string.camera_device, + R.string.camera_device_description, + cameraDeviceNameList.toTypedArray(), + cameraDeviceValueList.toTypedArray(), + StringSetting.CAMERA_OUTER_RIGHT_CONFIG.key, + StringSetting.CAMERA_OUTER_RIGHT_CONFIG.defaultValue + ) + ) + } + add( + SingleChoiceSetting( + IntSetting.CAMERA_OUTER_RIGHT_FLIP, + R.string.image_flip, + 0, + R.array.cameraFlipNames, + R.array.cameraDeviceValues, + IntSetting.CAMERA_OUTER_RIGHT_FLIP.key, + IntSetting.CAMERA_OUTER_RIGHT_FLIP.defaultValue + ) + ) + } + } + + private fun addControlsSettings(sl: ArrayList<SettingsItem>) { + settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_controls)) + sl.apply { + add(HeaderSetting(R.string.generic_buttons)) + Settings.buttonKeys.forEachIndexed { i: Int, key: String -> + val button = getInputObject(key) + add(InputBindingSetting(button, Settings.buttonTitles[i])) + } + + add(HeaderSetting(R.string.controller_circlepad)) + Settings.circlePadKeys.forEachIndexed { i: Int, key: String -> + val button = getInputObject(key) + add(InputBindingSetting(button, Settings.axisTitles[i])) + } + + add(HeaderSetting(R.string.controller_c)) + Settings.cStickKeys.forEachIndexed { i: Int, key: String -> + val button = getInputObject(key) + add(InputBindingSetting(button, Settings.axisTitles[i])) + } + + add(HeaderSetting(R.string.controller_dpad)) + Settings.dPadKeys.forEachIndexed { i: Int, key: String -> + val button = getInputObject(key) + add(InputBindingSetting(button, Settings.axisTitles[i])) + } + + add(HeaderSetting(R.string.controller_triggers)) + Settings.triggerKeys.forEachIndexed { i: Int, key: String -> + val button = getInputObject(key) + add(InputBindingSetting(button, Settings.triggerTitles[i])) + } + } + } + + private fun getInputObject(key: String): AbstractStringSetting { + return object : AbstractStringSetting { + override var string: String + get() = preferences.getString(key, "")!! + set(value) { + preferences.edit() + .putString(key, value) + .apply() + } + override val key = key + override val section = Settings.SECTION_CONTROLS + override val isRuntimeEditable = true + override val valueAsString = preferences.getString(key, "")!! + override val defaultValue = "" + } + } + + private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) { + settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_graphics)) + sl.apply { + add(HeaderSetting(R.string.renderer)) + add( + SingleChoiceSetting( + IntSetting.GRAPHICS_API, + R.string.graphics_api, + 0, + R.array.graphicsApiNames, + R.array.graphicsApiValues, + IntSetting.GRAPHICS_API.key, + IntSetting.GRAPHICS_API.defaultValue + ) + ) + add( + SwitchSetting( + BooleanSetting.SPIRV_SHADER_GEN, + R.string.spirv_shader_gen, + R.string.spirv_shader_gen_description, + BooleanSetting.SPIRV_SHADER_GEN.key, + BooleanSetting.SPIRV_SHADER_GEN.defaultValue, + ) + ) + add( + SwitchSetting( + BooleanSetting.ASYNC_SHADERS, + R.string.async_shaders, + R.string.async_shaders_description, + BooleanSetting.ASYNC_SHADERS.key, + BooleanSetting.ASYNC_SHADERS.defaultValue + ) + ) + add( + SliderSetting( + IntSetting.RESOLUTION_FACTOR, + R.string.internal_resolution, + R.string.internal_resolution_description, + 1, + 10, + "x", + IntSetting.GRAPHICS_API.key, + IntSetting.GRAPHICS_API.defaultValue.toFloat() + ) + ) + add( + SwitchSetting( + IntSetting.LINEAR_FILTERING, + R.string.linear_filtering, + R.string.linear_filtering_description, + IntSetting.LINEAR_FILTERING.key, + IntSetting.LINEAR_FILTERING.defaultValue + ) + ) + add( + SwitchSetting( + IntSetting.SHADERS_ACCURATE_MUL, + R.string.shaders_accurate_mul, + R.string.shaders_accurate_mul_description, + IntSetting.SHADERS_ACCURATE_MUL.key, + IntSetting.SHADERS_ACCURATE_MUL.defaultValue + ) + ) + add( + SwitchSetting( + IntSetting.DISK_SHADER_CACHE, + R.string.use_disk_shader_cache, + R.string.use_disk_shader_cache_description, + IntSetting.DISK_SHADER_CACHE.key, + IntSetting.DISK_SHADER_CACHE.defaultValue + ) + ) + add( + SingleChoiceSetting( + IntSetting.TEXTURE_FILTER, + R.string.texture_filter_name, + R.string.texture_filter_description, + R.array.textureFilterNames, + R.array.textureFilterValues, + IntSetting.TEXTURE_FILTER.key, + IntSetting.TEXTURE_FILTER.defaultValue + ) + ) + + add(HeaderSetting(R.string.stereoscopy)) + add( + SingleChoiceSetting( + IntSetting.STEREOSCOPIC_3D_MODE, + R.string.render3d, + 0, + R.array.render3dModes, + R.array.render3dValues, + IntSetting.STEREOSCOPIC_3D_MODE.key, + IntSetting.STEREOSCOPIC_3D_MODE.defaultValue + ) + ) + add( + SliderSetting( + IntSetting.STEREOSCOPIC_3D_DEPTH, + R.string.factor3d, + R.string.factor3d_description, + 0, + 100, + "%", + IntSetting.STEREOSCOPIC_3D_DEPTH.key, + IntSetting.STEREOSCOPIC_3D_DEPTH.defaultValue.toFloat() + ) + ) + + add(HeaderSetting(R.string.cardboard_vr)) + add( + SliderSetting( + IntSetting.CARDBOARD_SCREEN_SIZE, + R.string.cardboard_screen_size, + R.string.cardboard_screen_size_description, + 30, + 100, + "%", + IntSetting.CARDBOARD_SCREEN_SIZE.key, + IntSetting.CARDBOARD_SCREEN_SIZE.defaultValue.toFloat() + ) + ) + add( + SliderSetting( + IntSetting.CARDBOARD_X_SHIFT, + R.string.cardboard_x_shift, + R.string.cardboard_x_shift_description, + -100, + 100, + "%", + IntSetting.CARDBOARD_X_SHIFT.key, + IntSetting.CARDBOARD_X_SHIFT.defaultValue.toFloat() + ) + ) + add( + SliderSetting( + IntSetting.CARDBOARD_Y_SHIFT, + R.string.cardboard_y_shift, + R.string.cardboard_y_shift_description, + -100, + 100, + "%", + IntSetting.CARDBOARD_Y_SHIFT.key, + IntSetting.CARDBOARD_Y_SHIFT.defaultValue.toFloat() + ) + ) + + add(HeaderSetting(R.string.utility)) + add( + SwitchSetting( + IntSetting.DUMP_TEXTURES, + R.string.dump_textures, + R.string.dump_textures_description, + IntSetting.DUMP_TEXTURES.key, + IntSetting.DUMP_TEXTURES.defaultValue + ) + ) + add( + SwitchSetting( + IntSetting.CUSTOM_TEXTURES, + R.string.custom_textures, + R.string.custom_textures_description, + IntSetting.CUSTOM_TEXTURES.key, + IntSetting.CUSTOM_TEXTURES.defaultValue + ) + ) + add( + SwitchSetting( + IntSetting.ASYNC_CUSTOM_LOADING, + R.string.async_custom_loading, + R.string.async_custom_loading_description, + IntSetting.ASYNC_CUSTOM_LOADING.key, + IntSetting.ASYNC_CUSTOM_LOADING.defaultValue + ) + ) + + // Disabled until custom texture implementation gets rewrite, current one overloads RAM + // and crashes Citra. + // add( + // SwitchSetting( + // BooleanSetting.PRELOAD_TEXTURES, + // R.string.preload_textures, + // R.string.preload_textures_description, + // BooleanSetting.PRELOAD_TEXTURES.key, + // BooleanSetting.PRELOAD_TEXTURES.defaultValue + // ) + // ) + } + } + + private fun addAudioSettings(sl: ArrayList<SettingsItem>) { + settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_audio)) + sl.apply { + add( + SliderSetting( + ScaledFloatSetting.AUDIO_VOLUME, + R.string.audio_volume, + 0, + 0, + 100, + "%", + ScaledFloatSetting.AUDIO_VOLUME.key, + ScaledFloatSetting.AUDIO_VOLUME.defaultValue + ) + ) + add( + SwitchSetting( + IntSetting.ENABLE_AUDIO_STRETCHING, + R.string.audio_stretch, + R.string.audio_stretch_description, + IntSetting.ENABLE_AUDIO_STRETCHING.key, + IntSetting.ENABLE_AUDIO_STRETCHING.defaultValue + ) + ) + add( + SingleChoiceSetting( + IntSetting.AUDIO_INPUT_TYPE, + R.string.audio_input_type, + 0, + R.array.audioInputTypeNames, + R.array.audioInputTypeValues, + IntSetting.AUDIO_INPUT_TYPE.key, + IntSetting.AUDIO_INPUT_TYPE.defaultValue + ) + ) + + val soundOutputModeSetting = object : AbstractIntSetting { + override var int: Int + get() = SystemSaveGame.getSoundOutputMode() + set(value) = SystemSaveGame.setSoundOutputMode(value) + override val key = null + override val section = null + override val isRuntimeEditable = false + override val valueAsString = int.toString() + override val defaultValue = 2 + } + add( + SingleChoiceSetting( + soundOutputModeSetting, + R.string.sound_output_mode, + 0, + R.array.soundOutputModes, + R.array.soundOutputModeValues + ) + ) + } + } + + private fun addDebugSettings(sl: ArrayList<SettingsItem>) { + settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_debug)) + sl.apply { + add(HeaderSetting(R.string.debug_warning)) + add( + SwitchSetting( + IntSetting.NEW_3DS, + R.string.new_3ds, + 0, + IntSetting.NEW_3DS.key, + IntSetting.NEW_3DS.defaultValue + ) + ) + add( + SliderSetting( + IntSetting.CPU_CLOCK_SPEED, + R.string.cpu_clock_speed, + 0, + 25, + 400, + "%", + IntSetting.CPU_CLOCK_SPEED.key, + IntSetting.CPU_CLOCK_SPEED.defaultValue.toFloat() + ) + ) + add( + SwitchSetting( + IntSetting.CPU_JIT, + R.string.cpu_jit, + R.string.cpu_jit_description, + IntSetting.CPU_JIT.key, + IntSetting.CPU_JIT.defaultValue + ) + ) + add( + SwitchSetting( + IntSetting.HW_SHADER, + R.string.hw_shaders, + R.string.hw_shaders_description, + IntSetting.HW_SHADER.key, + IntSetting.HW_SHADER.defaultValue + ) + ) + add( + SwitchSetting( + IntSetting.VSYNC, + R.string.vsync, + R.string.vsync_description, + IntSetting.VSYNC.key, + IntSetting.VSYNC.defaultValue + ) + ) + add( + SwitchSetting( + IntSetting.DEBUG_RENDERER, + R.string.renderer_debug, + R.string.renderer_debug_description, + IntSetting.DEBUG_RENDERER.key, + IntSetting.DEBUG_RENDERER.defaultValue + ) + ) + } + } + + private fun addThemeSettings(sl: ArrayList<SettingsItem>) { + settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_theme)) + sl.apply { + val theme: AbstractBooleanSetting = object : AbstractBooleanSetting { + override var boolean: Boolean + get() = preferences.getBoolean(Settings.PREF_MATERIAL_YOU, false) + set(value) { + preferences.edit() + .putBoolean(Settings.PREF_MATERIAL_YOU, value) + .apply() + settingsActivity.recreate() + } + override val key: String? = null + override val section: String? = null + override val isRuntimeEditable: Boolean = false + override val valueAsString: String + get() = preferences.getBoolean(Settings.PREF_MATERIAL_YOU, false).toString() + override val defaultValue = false + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + add( + SwitchSetting( + theme, + R.string.material_you, + R.string.material_you_description + ) + ) + } + + val themeMode: AbstractIntSetting = object : AbstractIntSetting { + override var int: Int + get() = preferences.getInt(Settings.PREF_THEME_MODE, -1) + set(value) { + preferences.edit() + .putInt(Settings.PREF_THEME_MODE, value) + .apply() + ThemeUtil.setThemeMode(settingsActivity) + settingsActivity.recreate() + } + override val key: String? = null + override val section: String? = null + override val isRuntimeEditable: Boolean = false + override val valueAsString: String + get() = preferences.getInt(Settings.PREF_THEME_MODE, -1).toString() + override val defaultValue: Any = -1 + } + + add( + SingleChoiceSetting( + themeMode, + R.string.change_theme_mode, + 0, + R.array.themeModeEntries, + R.array.themeModeValues + ) + ) + + val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting { + override var boolean: Boolean + get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) + set(value) { + preferences.edit() + .putBoolean(Settings.PREF_BLACK_BACKGROUNDS, value) + .apply() + settingsActivity.recreate() + } + override val key: String? = null + override val section: String? = null + override val isRuntimeEditable: Boolean = false + override val valueAsString: String + get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) + .toString() + override val defaultValue: Any = false + } + + add( + SwitchSetting( + blackBackgrounds, + R.string.use_black_backgrounds, + R.string.use_black_backgrounds_description + ) + ) + } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.java deleted file mode 100644 index c36eb55a7c..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.citra.citra_emu.features.settings.ui; - -import androidx.fragment.app.FragmentActivity; - -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.features.settings.model.Settings; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; - -import java.util.ArrayList; - -/** - * Abstraction for a screen showing a list of settings. Instances of - * this type of view will each display a layer of the setting hierarchy. - */ -public interface SettingsFragmentView { - /** - * Called by the containing Activity to notify the Fragment that an - * asynchronous load operation completed. - * - * @param settings The (possibly null) result of the ini load operation. - */ - void onSettingsFileLoaded(Settings settings); - - /** - * Pass a settings HashMap to the containing activity, so that it can - * share the HashMap with other SettingsFragments; useful so that rotations - * do not require an additional load operation. - * - * @param settings An ArrayList containing all the settings HashMaps. - */ - void passSettingsToActivity(Settings settings); - - /** - * Pass an ArrayList to the View so that it can be displayed on screen. - * - * @param settingsList The result of converting the HashMap to an ArrayList - */ - void showSettingsList(ArrayList<SettingsItem> settingsList); - - /** - * Called by the containing Activity when an asynchronous load operation fails. - * Instructs the Fragment to load the settings screen with defaults selected. - */ - void loadDefaultSettings(); - - /** - * @return The Fragment's containing activity. - */ - FragmentActivity getActivity(); - - /** - * Tell the Fragment to tell the containing Activity to show a new - * Fragment containing a submenu of settings. - * - * @param menuKey Identifier for the settings group that should be shown. - */ - void loadSubMenu(String menuKey); - - /** - * Tell the Fragment to tell the containing activity to display a toast message. - * - * @param message Text to be shown in the Toast - * @param is_long Whether this should be a long Toast or short one. - */ - void showToastMessage(String message, boolean is_long); - - /** - * Have the fragment add a setting to the HashMap. - * - * @param setting The (possibly previously missing) new setting. - */ - void putSetting(Setting setting); - - /** - * Have the fragment tell the containing Activity that a setting was modified. - */ - void onSettingChanged(); -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.kt new file mode 100644 index 0000000000..e1bb25230d --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.kt @@ -0,0 +1,59 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui + +import org.citra.citra_emu.features.settings.model.AbstractSetting +import org.citra.citra_emu.features.settings.model.view.SettingsItem + +/** + * Abstraction for a screen showing a list of settings. Instances of + * this type of view will each display a layer of the setting hierarchy. + */ +interface SettingsFragmentView { + /** + * Pass an ArrayList to the View so that it can be displayed on screen. + * + * @param settingsList The result of converting the HashMap to an ArrayList + */ + fun showSettingsList(settingsList: ArrayList<SettingsItem>) + + /** + * Instructs the Fragment to load the settings screen. + */ + fun loadSettingsList() + + /** + * @return The Fragment's containing activity. + */ + val activityView: SettingsActivityView? + + /** + * Tell the Fragment to tell the containing Activity to show a new + * Fragment containing a submenu of settings. + * + * @param menuKey Identifier for the settings group that should be shown. + */ + fun loadSubMenu(menuKey: String) + + /** + * Tell the Fragment to tell the containing activity to display a toast message. + * + * @param message Text to be shown in the Toast + * @param is_long Whether this should be a long Toast or short one. + */ + fun showToastMessage(message: String?, is_long: Boolean) + + /** + * Have the fragment add a setting to the HashMap. + * + * @param setting The (possibly previously missing) new setting. + */ + fun putSetting(setting: AbstractSetting) + + /** + * Have the fragment tell the containing Activity that a setting was modified. + */ + fun onSettingChanged() +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java deleted file mode 100644 index d914f7d0bc..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.citra.citra_emu.features.settings.ui.viewholder; - -import android.view.View; -import android.widget.CheckBox; -import android.widget.TextView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.ui.SettingsAdapter; - -public final class CheckBoxSettingViewHolder extends SettingViewHolder { - private CheckBoxSetting mItem; - - private TextView mTextSettingName; - private TextView mTextSettingDescription; - - private CheckBox mCheckbox; - - public CheckBoxSettingViewHolder(View itemView, SettingsAdapter adapter) { - super(itemView, adapter); - } - - @Override - protected void findViews(View root) { - mTextSettingName = root.findViewById(R.id.text_setting_name); - mTextSettingDescription = root.findViewById(R.id.text_setting_description); - mCheckbox = root.findViewById(R.id.checkbox); - } - - @Override - public void bind(SettingsItem item) { - mItem = (CheckBoxSetting) item; - - mTextSettingName.setText(item.getNameId()); - - if (item.getDescriptionId() > 0) { - mTextSettingDescription.setText(item.getDescriptionId()); - mTextSettingDescription.setVisibility(View.VISIBLE); - } else { - mTextSettingDescription.setText(""); - mTextSettingDescription.setVisibility(View.GONE); - } - - mCheckbox.setChecked(mItem.isChecked()); - } - - @Override - public void onClick(View clicked) { - mCheckbox.toggle(); - - getAdapter().onBooleanClick(mItem, getAdapterPosition(), mCheckbox.isChecked()); - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.java deleted file mode 100644 index 09ea93010e..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.citra.citra_emu.features.settings.ui.viewholder; - -import android.view.View; -import android.widget.TextView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.view.DateTimeSetting; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.ui.SettingsAdapter; -import org.citra.citra_emu.utils.Log; - -public final class DateTimeViewHolder extends SettingViewHolder { - private DateTimeSetting mItem; - - private TextView mTextSettingName; - private TextView mTextSettingDescription; - - public DateTimeViewHolder(View itemView, SettingsAdapter adapter) { - super(itemView, adapter); - } - - @Override - protected void findViews(View root) { - mTextSettingName = root.findViewById(R.id.text_setting_name); - Log.error("test " + mTextSettingName); - mTextSettingDescription = root.findViewById(R.id.text_setting_description); - Log.error("test " + mTextSettingDescription); - } - - @Override - public void bind(SettingsItem item) { - mItem = (DateTimeSetting) item; - mTextSettingName.setText(item.getNameId()); - - if (item.getDescriptionId() > 0) { - mTextSettingDescription.setText(item.getDescriptionId()); - mTextSettingDescription.setVisibility(View.VISIBLE); - } else { - mTextSettingDescription.setVisibility(View.GONE); - } - } - - @Override - public void onClick(View clicked) { - getAdapter().onDateTimeClick(mItem, getAdapterPosition()); - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt new file mode 100644 index 0000000000..6e06e7a01b --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt @@ -0,0 +1,77 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui.viewholder + +import android.annotation.SuppressLint +import android.view.View +import org.citra.citra_emu.databinding.ListItemSettingBinding +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import org.citra.citra_emu.features.settings.model.view.DateTimeSetting +import org.citra.citra_emu.features.settings.model.view.SettingsItem +import org.citra.citra_emu.features.settings.ui.SettingsAdapter +import java.text.SimpleDateFormat + +class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : + SettingViewHolder(binding.root, adapter) { + private lateinit var setting: DateTimeSetting + + @SuppressLint("SimpleDateFormat") + override fun bind(item: SettingsItem) { + setting = item as DateTimeSetting + binding.textSettingName.setText(item.nameId) + if (item.descriptionId != 0) { + binding.textSettingDescription.visibility = View.VISIBLE + binding.textSettingDescription.setText(item.descriptionId) + } else { + binding.textSettingDescription.visibility = View.GONE + } + binding.textSettingValue.visibility = View.VISIBLE + val epochTime = try { + setting.value.toLong() + } catch (e: NumberFormatException) { + val date = setting.value.substringBefore(" ") + val time = setting.value.substringAfter(" ") + + val formatter = SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssZZZZ") + val gmt = formatter.parse("${date}T${time}+0000") + gmt!!.time / 1000 + } + val instant = Instant.ofEpochMilli(epochTime * 1000) + val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")) + val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) + binding.textSettingValue.text = dateFormatter.format(zonedTime) + + if (setting.isEditable) { + binding.textSettingName.alpha = 1f + binding.textSettingDescription.alpha = 1f + binding.textSettingValue.alpha = 1f + } else { + binding.textSettingName.alpha = 0.5f + binding.textSettingDescription.alpha = 0.5f + binding.textSettingValue.alpha = 0.5f + } + } + + override fun onClick(clicked: View) { + if (setting.isEditable) { + adapter.onDateTimeClick(setting, bindingAdapterPosition) + } else { + adapter.onClickDisabledSetting() + } + } + + override fun onLongClick(clicked: View): Boolean { + if (setting.isEditable) { + return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) + } else { + adapter.onClickDisabledSetting() + } + return false + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.java deleted file mode 100644 index baf80ed76d..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.citra.citra_emu.features.settings.ui.viewholder; - -import android.view.View; -import android.widget.TextView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.ui.SettingsAdapter; - -public final class HeaderViewHolder extends SettingViewHolder { - private TextView mHeaderName; - - public HeaderViewHolder(View itemView, SettingsAdapter adapter) { - super(itemView, adapter); - itemView.setOnClickListener(null); - } - - @Override - protected void findViews(View root) { - mHeaderName = root.findViewById(R.id.text_header_name); - } - - @Override - public void bind(SettingsItem item) { - mHeaderName.setText(item.getNameId()); - } - - @Override - public void onClick(View clicked) { - // no-op - } -} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.kt new file mode 100644 index 0000000000..617348c895 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.kt @@ -0,0 +1,31 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui.viewholder + +import android.view.View +import org.citra.citra_emu.databinding.ListItemSettingsHeaderBinding +import org.citra.citra_emu.features.settings.model.view.SettingsItem +import org.citra.citra_emu.features.settings.ui.SettingsAdapter + +class HeaderViewHolder(val binding: ListItemSettingsHeaderBinding, adapter: SettingsAdapter) : + SettingViewHolder(binding.root, adapter) { + + init { + itemView.setOnClickListener(null) + } + + override fun bind(item: SettingsItem) { + binding.textHeaderName.setText(item.nameId) + } + + override fun onClick(clicked: View) { + // no-op + } + + override fun onLongClick(clicked: View): Boolean { + // no-op + return true + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java deleted file mode 100644 index 7d95c250a1..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.citra.citra_emu.features.settings.ui.viewholder; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.view.View; -import android.widget.TextView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.ui.SettingsAdapter; - -public final class InputBindingSettingViewHolder extends SettingViewHolder { - private InputBindingSetting mItem; - - private TextView mTextSettingName; - private TextView mTextSettingDescription; - - private Context mContext; - - public InputBindingSettingViewHolder(View itemView, SettingsAdapter adapter, Context context) { - super(itemView, adapter); - - mContext = context; - } - - @Override - protected void findViews(View root) { - mTextSettingName = root.findViewById(R.id.text_setting_name); - mTextSettingDescription = root.findViewById(R.id.text_setting_description); - } - - @Override - public void bind(SettingsItem item) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); - - mItem = (InputBindingSetting) item; - - mTextSettingName.setText(item.getNameId()); - - String key = sharedPreferences.getString(mItem.getKey(), ""); - if (key != null && !key.isEmpty()) { - mTextSettingDescription.setText(key); - mTextSettingDescription.setVisibility(View.VISIBLE); - } else { - mTextSettingDescription.setVisibility(View.GONE); - } - } - - @Override - public void onClick(View clicked) { - getAdapter().onInputBindingClick(mItem, getAdapterPosition()); - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt new file mode 100644 index 0000000000..9d2dc15af3 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt @@ -0,0 +1,60 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui.viewholder + +import android.view.View +import androidx.preference.PreferenceManager +import org.citra.citra_emu.CitraApplication +import org.citra.citra_emu.databinding.ListItemSettingBinding +import org.citra.citra_emu.features.settings.model.view.InputBindingSetting +import org.citra.citra_emu.features.settings.model.view.SettingsItem +import org.citra.citra_emu.features.settings.ui.SettingsAdapter + +class InputBindingSettingViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : + SettingViewHolder(binding.root, adapter) { + private lateinit var setting: InputBindingSetting + + override fun bind(item: SettingsItem) { + val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) + setting = item as InputBindingSetting + binding.textSettingName.setText(item.nameId) + val uiString = preferences.getString(setting.abstractSetting.key, "")!! + if (uiString.isNotEmpty()) { + binding.textSettingDescription.visibility = View.GONE + binding.textSettingValue.visibility = View.VISIBLE + binding.textSettingValue.text = uiString + } else { + binding.textSettingDescription.visibility = View.GONE + binding.textSettingValue.visibility = View.GONE + } + + if (setting.isEditable) { + binding.textSettingName.alpha = 1f + binding.textSettingDescription.alpha = 1f + binding.textSettingValue.alpha = 1f + } else { + binding.textSettingName.alpha = 0.5f + binding.textSettingDescription.alpha = 0.5f + binding.textSettingValue.alpha = 0.5f + } + } + + override fun onClick(clicked: View) { + if (setting.isEditable) { + adapter.onInputBindingClick(setting, bindingAdapterPosition) + } else { + adapter.onClickDisabledSetting() + } + } + + override fun onLongClick(clicked: View): Boolean { + if (setting.isEditable) { + adapter.onLongClick(setting.setting!!, bindingAdapterPosition) + } else { + adapter.onClickDisabledSetting() + } + return false + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt new file mode 100644 index 0000000000..ac0c60a906 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt @@ -0,0 +1,58 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui.viewholder + +import android.view.View +import org.citra.citra_emu.NativeLibrary +import org.citra.citra_emu.databinding.ListItemSettingBinding +import org.citra.citra_emu.features.settings.model.view.RunnableSetting +import org.citra.citra_emu.features.settings.model.view.SettingsItem +import org.citra.citra_emu.features.settings.ui.SettingsAdapter + +class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : + SettingViewHolder(binding.root, adapter) { + private lateinit var setting: RunnableSetting + + override fun bind(item: SettingsItem) { + setting = item as RunnableSetting + binding.textSettingName.setText(item.nameId) + if (item.descriptionId != 0) { + binding.textSettingDescription.setText(item.descriptionId) + binding.textSettingDescription.visibility = View.VISIBLE + } else { + binding.textSettingDescription.visibility = View.GONE + } + + if (setting.value != null) { + binding.textSettingValue.visibility = View.VISIBLE + binding.textSettingValue.text = setting.value!!.invoke() + } else { + binding.textSettingValue.visibility = View.GONE + } + + if (setting.isEditable) { + binding.textSettingName.alpha = 1f + binding.textSettingDescription.alpha = 1f + binding.textSettingValue.alpha = 1f + } else { + binding.textSettingName.alpha = 0.5f + binding.textSettingDescription.alpha = 0.5f + binding.textSettingValue.alpha = 0.5f + } + } + + override fun onClick(clicked: View) { + if (!setting.isRuntimeRunnable && !NativeLibrary.isRunning()) { + setting.runnable.invoke() + } else { + adapter.onClickDisabledSetting() + } + } + + override fun onLongClick(clicked: View): Boolean { + // no-op + return true + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.java deleted file mode 100644 index 2643ea1214..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.citra.citra_emu.features.settings.ui.viewholder; - -import android.view.View; - -import androidx.recyclerview.widget.RecyclerView; - -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.ui.SettingsAdapter; - -public abstract class SettingViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { - private SettingsAdapter mAdapter; - - public SettingViewHolder(View itemView, SettingsAdapter adapter) { - super(itemView); - - mAdapter = adapter; - - itemView.setOnClickListener(this); - - findViews(itemView); - } - - protected SettingsAdapter getAdapter() { - return mAdapter; - } - - /** - * Gets handles to all this ViewHolder's child views using their XML-defined identifiers. - * - * @param root The newly inflated top-level view. - */ - protected abstract void findViews(View root); - - /** - * Called by the adapter to set this ViewHolder's child views to display the list item - * it must now represent. - * - * @param item The list item that should be represented by this ViewHolder. - */ - public abstract void bind(SettingsItem item); - - /** - * Called when this ViewHolder's view is clicked on. Implementations should usually pass - * this event up to the adapter. - * - * @param clicked The view that was clicked on. - */ - public abstract void onClick(View clicked); -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt new file mode 100644 index 0000000000..5b4d39cf43 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt @@ -0,0 +1,37 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui.viewholder + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import org.citra.citra_emu.features.settings.model.view.SettingsItem +import org.citra.citra_emu.features.settings.ui.SettingsAdapter + +abstract class SettingViewHolder(itemView: View, protected val adapter: SettingsAdapter) : + RecyclerView.ViewHolder(itemView), View.OnClickListener, View.OnLongClickListener { + + init { + itemView.setOnClickListener(this) + itemView.setOnLongClickListener(this) + } + + /** + * Called by the adapter to set this ViewHolder's child views to display the list item + * it must now represent. + * + * @param item The list item that should be represented by this ViewHolder. + */ + abstract fun bind(item: SettingsItem) + + /** + * Called when this ViewHolder's view is clicked on. Implementations should usually pass + * this event up to the adapter. + * + * @param clicked The view that was clicked on. + */ + abstract override fun onClick(clicked: View) + + abstract override fun onLongClick(clicked: View): Boolean +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java deleted file mode 100644 index f735b77523..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.citra.citra_emu.features.settings.ui.viewholder; - -import android.content.res.Resources; -import android.view.View; -import android.widget.TextView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting; -import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting; -import org.citra.citra_emu.features.settings.ui.SettingsAdapter; - -public final class SingleChoiceViewHolder extends SettingViewHolder { - private SettingsItem mItem; - - private TextView mTextSettingName; - private TextView mTextSettingDescription; - - public SingleChoiceViewHolder(View itemView, SettingsAdapter adapter) { - super(itemView, adapter); - } - - @Override - protected void findViews(View root) { - mTextSettingName = root.findViewById(R.id.text_setting_name); - mTextSettingDescription = root.findViewById(R.id.text_setting_description); - } - - @Override - public void bind(SettingsItem item) { - mItem = item; - - mTextSettingName.setText(item.getNameId()); - mTextSettingDescription.setVisibility(View.VISIBLE); - if (item.getDescriptionId() > 0) { - mTextSettingDescription.setText(item.getDescriptionId()); - } else if (item instanceof SingleChoiceSetting) { - SingleChoiceSetting setting = (SingleChoiceSetting) item; - int selected = setting.getSelectedValue(); - Resources resMgr = mTextSettingDescription.getContext().getResources(); - String[] choices = resMgr.getStringArray(setting.getChoicesId()); - int[] values = resMgr.getIntArray(setting.getValuesId()); - for (int i = 0; i < values.length; ++i) { - if (values[i] == selected) { - mTextSettingDescription.setText(choices[i]); - } - } - } else { - mTextSettingDescription.setVisibility(View.GONE); - } - } - - @Override - public void onClick(View clicked) { - int position = getAdapterPosition(); - if (mItem instanceof SingleChoiceSetting) { - getAdapter().onSingleChoiceClick((SingleChoiceSetting) mItem, position); - } else if (mItem instanceof StringSingleChoiceSetting) { - getAdapter().onStringSingleChoiceClick((StringSingleChoiceSetting) mItem, position); - } - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt new file mode 100644 index 0000000000..e5e6d5df6d --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt @@ -0,0 +1,94 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui.viewholder + +import android.view.View +import org.citra.citra_emu.databinding.ListItemSettingBinding +import org.citra.citra_emu.features.settings.model.view.SettingsItem +import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting +import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting +import org.citra.citra_emu.features.settings.ui.SettingsAdapter + +class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : + SettingViewHolder(binding.root, adapter) { + private lateinit var setting: SettingsItem + + override fun bind(item: SettingsItem) { + setting = item + binding.textSettingName.setText(item.nameId) + if (item.descriptionId != 0) { + binding.textSettingDescription.visibility = View.VISIBLE + binding.textSettingDescription.setText(item.descriptionId) + } else { + binding.textSettingDescription.visibility = View.GONE + } + binding.textSettingValue.visibility = View.VISIBLE + binding.textSettingValue.text = getTextSetting() + + if (setting.isEditable) { + binding.textSettingName.alpha = 1f + binding.textSettingDescription.alpha = 1f + binding.textSettingValue.alpha = 1f + } else { + binding.textSettingName.alpha = 0.5f + binding.textSettingDescription.alpha = 0.5f + binding.textSettingValue.alpha = 0.5f + } + } + + private fun getTextSetting(): String { + when (val item = setting) { + is SingleChoiceSetting -> { + val resMgr = binding.textSettingDescription.context.resources + val values = resMgr.getIntArray(item.valuesId) + values.forEachIndexed { i: Int, value: Int -> + if (value == (setting as SingleChoiceSetting).selectedValue) { + return resMgr.getStringArray(item.choicesId)[i] + } + } + return "" + } + + is StringSingleChoiceSetting -> { + item.values?.forEachIndexed { i: Int, value: String -> + if (value == item.selectedValue) { + return item.choices[i] + } + } + return "" + } + + else -> return "" + } + } + + override fun onClick(clicked: View) { + if (!setting.isEditable) { + adapter.onClickDisabledSetting() + return + } + + if (setting is SingleChoiceSetting) { + adapter.onSingleChoiceClick( + (setting as SingleChoiceSetting), + bindingAdapterPosition + ) + } else if (setting is StringSingleChoiceSetting) { + adapter.onStringSingleChoiceClick( + (setting as StringSingleChoiceSetting), + bindingAdapterPosition + ) + } + } + + override fun onLongClick(clicked: View): Boolean { + if (setting.isEditable) { + return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) + } else { + adapter.onClickDisabledSetting() + } + return false + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.java deleted file mode 100644 index ce503bc543..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.citra.citra_emu.features.settings.ui.viewholder; - -import android.view.View; -import android.widget.TextView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.model.view.SliderSetting; -import org.citra.citra_emu.features.settings.ui.SettingsAdapter; - -public final class SliderViewHolder extends SettingViewHolder { - private SliderSetting mItem; - - private TextView mTextSettingName; - private TextView mTextSettingDescription; - - public SliderViewHolder(View itemView, SettingsAdapter adapter) { - super(itemView, adapter); - } - - @Override - protected void findViews(View root) { - mTextSettingName = root.findViewById(R.id.text_setting_name); - mTextSettingDescription = root.findViewById(R.id.text_setting_description); - } - - @Override - public void bind(SettingsItem item) { - mItem = (SliderSetting) item; - - mTextSettingName.setText(item.getNameId()); - - if (item.getDescriptionId() > 0) { - mTextSettingDescription.setText(item.getDescriptionId()); - mTextSettingDescription.setVisibility(View.VISIBLE); - } else { - mTextSettingDescription.setVisibility(View.GONE); - } - } - - @Override - public void onClick(View clicked) { - getAdapter().onSliderClick(mItem, getAdapterPosition()); - } -} - diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt new file mode 100644 index 0000000000..e5817adc78 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt @@ -0,0 +1,65 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui.viewholder + +import android.view.View +import org.citra.citra_emu.databinding.ListItemSettingBinding +import org.citra.citra_emu.features.settings.model.AbstractFloatSetting +import org.citra.citra_emu.features.settings.model.AbstractIntSetting +import org.citra.citra_emu.features.settings.model.FloatSetting +import org.citra.citra_emu.features.settings.model.ScaledFloatSetting +import org.citra.citra_emu.features.settings.model.view.SettingsItem +import org.citra.citra_emu.features.settings.model.view.SliderSetting +import org.citra.citra_emu.features.settings.ui.SettingsAdapter + +class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : + SettingViewHolder(binding.root, adapter) { + private lateinit var setting: SliderSetting + + override fun bind(item: SettingsItem) { + setting = item as SliderSetting + binding.textSettingName.setText(item.nameId) + if (item.descriptionId != 0) { + binding.textSettingDescription.visibility = View.VISIBLE + binding.textSettingDescription.setText(item.descriptionId) + } else { + binding.textSettingDescription.visibility = View.GONE + } + binding.textSettingValue.visibility = View.VISIBLE + binding.textSettingValue.text = when (setting.setting) { + is ScaledFloatSetting -> + "${(setting.setting as ScaledFloatSetting).float.toInt()}${setting.units}" + is FloatSetting -> "${(setting.setting as AbstractFloatSetting).float}${setting.units}" + else -> "${(setting.setting as AbstractIntSetting).int}${setting.units}" + } + + if (setting.isEditable) { + binding.textSettingName.alpha = 1f + binding.textSettingDescription.alpha = 1f + binding.textSettingValue.alpha = 1f + } else { + binding.textSettingName.alpha = 0.5f + binding.textSettingDescription.alpha = 0.5f + binding.textSettingValue.alpha = 0.5f + } + } + + override fun onClick(clicked: View) { + if (setting.isEditable) { + adapter.onSliderClick(setting, bindingAdapterPosition) + } else { + adapter.onClickDisabledSetting() + } + } + + override fun onLongClick(clicked: View): Boolean { + if (setting.isEditable) { + return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) + } else { + adapter.onClickDisabledSetting() + } + return false + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt new file mode 100644 index 0000000000..b3140f39eb --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt @@ -0,0 +1,46 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui.viewholder + +import android.view.View +import org.citra.citra_emu.databinding.ListItemSettingBinding +import org.citra.citra_emu.features.settings.model.view.SettingsItem +import org.citra.citra_emu.features.settings.model.view.StringInputSetting +import org.citra.citra_emu.features.settings.ui.SettingsAdapter + +class StringInputViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : + SettingViewHolder(binding.root, adapter) { + private lateinit var setting: SettingsItem + + override fun bind(item: SettingsItem) { + setting = item + binding.textSettingName.setText(item.nameId) + if (item.descriptionId != 0) { + binding.textSettingDescription.visibility = View.VISIBLE + binding.textSettingDescription.setText(item.descriptionId) + } else { + binding.textSettingDescription.visibility = View.GONE + } + binding.textSettingValue.visibility = View.VISIBLE + binding.textSettingValue.text = setting.setting?.valueAsString + } + + override fun onClick(clicked: View) { + if (!setting.isEditable) { + adapter.onClickDisabledSetting() + return + } + adapter.onStringInputClick((setting as StringInputSetting), bindingAdapterPosition) + } + + override fun onLongClick(clicked: View): Boolean { + if (setting.isEditable) { + return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) + } else { + adapter.onClickDisabledSetting() + } + return false + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.java deleted file mode 100644 index cb8c3e92a7..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.citra.citra_emu.features.settings.ui.viewholder; - -import android.view.View; -import android.widget.TextView; - -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.view.SettingsItem; -import org.citra.citra_emu.features.settings.model.view.SubmenuSetting; -import org.citra.citra_emu.features.settings.ui.SettingsAdapter; - -public final class SubmenuViewHolder extends SettingViewHolder { - private SubmenuSetting mItem; - - private TextView mTextSettingName; - private TextView mTextSettingDescription; - - public SubmenuViewHolder(View itemView, SettingsAdapter adapter) { - super(itemView, adapter); - } - - @Override - protected void findViews(View root) { - mTextSettingName = root.findViewById(R.id.text_setting_name); - mTextSettingDescription = root.findViewById(R.id.text_setting_description); - } - - @Override - public void bind(SettingsItem item) { - mItem = (SubmenuSetting) item; - - mTextSettingName.setText(item.getNameId()); - - if (item.getDescriptionId() > 0) { - mTextSettingDescription.setText(item.getDescriptionId()); - mTextSettingDescription.setVisibility(View.VISIBLE); - } else { - mTextSettingDescription.setVisibility(View.GONE); - } - } - - @Override - public void onClick(View clicked) { - getAdapter().onSubmenuClick(mItem); - } -} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt new file mode 100644 index 0000000000..3098abbff5 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt @@ -0,0 +1,36 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui.viewholder + +import android.view.View +import org.citra.citra_emu.databinding.ListItemSettingBinding +import org.citra.citra_emu.features.settings.model.view.SettingsItem +import org.citra.citra_emu.features.settings.model.view.SubmenuSetting +import org.citra.citra_emu.features.settings.ui.SettingsAdapter + +class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : + SettingViewHolder(binding.root, adapter) { + private lateinit var item: SubmenuSetting + + override fun bind(item: SettingsItem) { + this.item = item as SubmenuSetting + binding.textSettingName.setText(item.nameId) + if (item.descriptionId != 0) { + binding.textSettingDescription.setText(item.descriptionId) + binding.textSettingDescription.visibility = View.VISIBLE + } else { + binding.textSettingDescription.visibility = View.GONE + } + } + + override fun onClick(clicked: View) { + adapter.onSubmenuClick(item) + } + + override fun onLongClick(clicked: View): Boolean { + // no-op + return true + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt new file mode 100644 index 0000000000..bd5ecc2eb4 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt @@ -0,0 +1,62 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui.viewholder + +import android.view.View +import android.widget.CompoundButton +import org.citra.citra_emu.databinding.ListItemSettingSwitchBinding +import org.citra.citra_emu.features.settings.model.view.SettingsItem +import org.citra.citra_emu.features.settings.model.view.SwitchSetting +import org.citra.citra_emu.features.settings.ui.SettingsAdapter + +class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) : + SettingViewHolder(binding.root, adapter) { + + private lateinit var setting: SwitchSetting + + override fun bind(item: SettingsItem) { + setting = item as SwitchSetting + binding.textSettingName.setText(item.nameId) + if (item.descriptionId != 0) { + binding.textSettingDescription.setText(item.descriptionId) + binding.textSettingDescription.visibility = View.VISIBLE + } else { + binding.textSettingDescription.text = "" + binding.textSettingDescription.visibility = View.GONE + } + + binding.switchWidget.setOnCheckedChangeListener(null) + binding.switchWidget.isChecked = setting.isChecked + binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> + adapter.onBooleanClick(item, bindingAdapterPosition, binding.switchWidget.isChecked) + } + + binding.switchWidget.isEnabled = setting.isEditable + if (setting.isEditable) { + binding.textSettingName.alpha = 1f + binding.textSettingDescription.alpha = 1f + } else { + binding.textSettingName.alpha = 0.5f + binding.textSettingDescription.alpha = 0.5f + } + } + + override fun onClick(clicked: View) { + if (setting.isEditable) { + binding.switchWidget.toggle() + } else { + adapter.onClickDisabledSetting() + } + } + + override fun onLongClick(clicked: View): Boolean { + if (setting.isEditable) { + return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) + } else { + adapter.onClickDisabledSetting() + } + return false + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java deleted file mode 100644 index 4590100cd4..0000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java +++ /dev/null @@ -1,344 +0,0 @@ -package org.citra.citra_emu.features.settings.utils; - -import android.content.Context; -import android.net.Uri; - -import androidx.annotation.NonNull; -import androidx.documentfile.provider.DocumentFile; - -import org.citra.citra_emu.CitraApplication; -import org.citra.citra_emu.NativeLibrary; -import org.citra.citra_emu.R; -import org.citra.citra_emu.features.settings.model.FloatSetting; -import org.citra.citra_emu.features.settings.model.IntSetting; -import org.citra.citra_emu.features.settings.model.Setting; -import org.citra.citra_emu.features.settings.model.SettingSection; -import org.citra.citra_emu.features.settings.model.Settings; -import org.citra.citra_emu.features.settings.model.StringSetting; -import org.citra.citra_emu.features.settings.ui.SettingsActivityView; -import org.citra.citra_emu.utils.BiMap; -import org.citra.citra_emu.utils.DirectoryInitialization; -import org.citra.citra_emu.utils.Log; -import org.ini4j.Wini; - -import java.io.BufferedReader; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.util.HashMap; -import java.util.Set; -import java.util.TreeMap; -import java.util.TreeSet; - -/** - * Contains static methods for interacting with .ini files in which settings are stored. - */ -public final class SettingsFile { - public static final String FILE_NAME_CONFIG = "config"; - - public static final String KEY_CPU_JIT = "use_cpu_jit"; - - public static final String KEY_DESIGN = "design"; - - - public static final String KEY_GRAPHICS_API = "graphics_api"; - public static final String KEY_SPIRV_SHADER_GEN = "spirv_shader_gen"; - public static final String KEY_ASYNC_SHADERS = "async_shader_compilation"; - public static final String KEY_RENDERER_DEBUG = "renderer_debug"; - public static final String KEY_HW_SHADER = "use_hw_shader"; - public static final String KEY_SHADERS_ACCURATE_MUL = "shaders_accurate_mul"; - public static final String KEY_USE_SHADER_JIT = "use_shader_jit"; - public static final String KEY_USE_DISK_SHADER_CACHE = "use_disk_shader_cache"; - public static final String KEY_USE_VSYNC = "use_vsync_new"; - public static final String KEY_RESOLUTION_FACTOR = "resolution_factor"; - public static final String KEY_FRAME_LIMIT_ENABLED = "use_frame_limit"; - public static final String KEY_FRAME_LIMIT = "frame_limit"; - public static final String KEY_BACKGROUND_RED = "bg_red"; - public static final String KEY_BACKGROUND_BLUE = "bg_blue"; - public static final String KEY_BACKGROUND_GREEN = "bg_green"; - public static final String KEY_RENDER_3D = "render_3d"; - public static final String KEY_FACTOR_3D = "factor_3d"; - public static final String KEY_PP_SHADER_NAME = "pp_shader_name"; - public static final String KEY_FILTER_MODE = "filter_mode"; - public static final String KEY_TEXTURE_FILTER_NAME = "texture_filter_name"; - public static final String KEY_USE_ASYNCHRONOUS_GPU_EMULATION = "use_asynchronous_gpu_emulation"; - - public static final String KEY_LAYOUT_OPTION = "layout_option"; - public static final String KEY_SWAP_SCREEN = "swap_screen"; - public static final String KEY_CARDBOARD_SCREEN_SIZE = "cardboard_screen_size"; - public static final String KEY_CARDBOARD_X_SHIFT = "cardboard_x_shift"; - public static final String KEY_CARDBOARD_Y_SHIFT = "cardboard_y_shift"; - - public static final String KEY_DUMP_TEXTURES = "dump_textures"; - public static final String KEY_CUSTOM_TEXTURES = "custom_textures"; - public static final String KEY_PRELOAD_TEXTURES = "preload_textures"; - public static final String KEY_ASYNC_CUSTOM_LOADING = "async_custom_loading"; - - public static final String KEY_AUDIO_OUTPUT_TYPE = "output_type"; - public static final String KEY_ENABLE_AUDIO_STRETCHING = "enable_audio_stretching"; - public static final String KEY_VOLUME = "volume"; - public static final String KEY_AUDIO_INPUT_TYPE = "input_type"; - - public static final String KEY_USE_VIRTUAL_SD = "use_virtual_sd"; - - public static final String KEY_IS_NEW_3DS = "is_new_3ds"; - public static final String KEY_REGION_VALUE = "region_value"; - public static final String KEY_LANGUAGE = "language"; - public static final String KEY_PLUGIN_LOADER = "plugin_loader"; - public static final String KEY_ALLOW_PLUGIN_LOADER = "allow_plugin_loader"; - - public static final String KEY_INIT_CLOCK = "init_clock"; - public static final String KEY_INIT_TIME = "init_time"; - - public static final String KEY_BUTTON_A = "button_a"; - public static final String KEY_BUTTON_B = "button_b"; - public static final String KEY_BUTTON_X = "button_x"; - public static final String KEY_BUTTON_Y = "button_y"; - public static final String KEY_BUTTON_SELECT = "button_select"; - public static final String KEY_BUTTON_START = "button_start"; - public static final String KEY_BUTTON_UP = "button_up"; - public static final String KEY_BUTTON_DOWN = "button_down"; - public static final String KEY_BUTTON_LEFT = "button_left"; - public static final String KEY_BUTTON_RIGHT = "button_right"; - public static final String KEY_BUTTON_L = "button_l"; - public static final String KEY_BUTTON_R = "button_r"; - public static final String KEY_BUTTON_ZL = "button_zl"; - public static final String KEY_BUTTON_ZR = "button_zr"; - public static final String KEY_CIRCLEPAD_AXIS_VERTICAL = "circlepad_axis_vertical"; - public static final String KEY_CIRCLEPAD_AXIS_HORIZONTAL = "circlepad_axis_horizontal"; - public static final String KEY_CSTICK_AXIS_VERTICAL = "cstick_axis_vertical"; - public static final String KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal"; - public static final String KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical"; - public static final String KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal"; - public static final String KEY_CIRCLEPAD_UP = "circlepad_up"; - public static final String KEY_CIRCLEPAD_DOWN = "circlepad_down"; - public static final String KEY_CIRCLEPAD_LEFT = "circlepad_left"; - public static final String KEY_CIRCLEPAD_RIGHT = "circlepad_right"; - public static final String KEY_CSTICK_UP = "cstick_up"; - public static final String KEY_CSTICK_DOWN = "cstick_down"; - public static final String KEY_CSTICK_LEFT = "cstick_left"; - public static final String KEY_CSTICK_RIGHT = "cstick_right"; - - public static final String KEY_CAMERA_OUTER_RIGHT_NAME = "camera_outer_right_name"; - public static final String KEY_CAMERA_OUTER_RIGHT_CONFIG = "camera_outer_right_config"; - public static final String KEY_CAMERA_OUTER_RIGHT_FLIP = "camera_outer_right_flip"; - public static final String KEY_CAMERA_OUTER_LEFT_NAME = "camera_outer_left_name"; - public static final String KEY_CAMERA_OUTER_LEFT_CONFIG = "camera_outer_left_config"; - public static final String KEY_CAMERA_OUTER_LEFT_FLIP = "camera_outer_left_flip"; - public static final String KEY_CAMERA_INNER_NAME = "camera_inner_name"; - public static final String KEY_CAMERA_INNER_CONFIG = "camera_inner_config"; - public static final String KEY_CAMERA_INNER_FLIP = "camera_inner_flip"; - - public static final String KEY_LOG_FILTER = "log_filter"; - - private static BiMap<String, String> sectionsMap = new BiMap<>(); - - static { - //TODO: Add members to sectionsMap when game-specific settings are added - } - - - private SettingsFile() { - } - - /** - * Reads a given .ini file from disk and returns it as a HashMap of Settings, themselves - * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it - * failed. - * - * @param ini The ini file to load the settings from - * @param isCustomGame - * @param view The current view. - * @return An Observable that emits a HashMap of the file's contents, then completes. - */ - static HashMap<String, SettingSection> readFile(final DocumentFile ini, boolean isCustomGame, SettingsActivityView view) { - HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap(); - - BufferedReader reader = null; - - try { - Context context = CitraApplication.Companion.getAppContext(); - InputStream inputStream = context.getContentResolver().openInputStream(ini.getUri()); - reader = new BufferedReader(new InputStreamReader(inputStream)); - - SettingSection current = null; - for (String line; (line = reader.readLine()) != null; ) { - if (line.startsWith("[") && line.endsWith("]")) { - current = sectionFromLine(line, isCustomGame); - sections.put(current.getName(), current); - } else if ((current != null)) { - Setting setting = settingFromLine(current, line); - if (setting != null) { - current.putSetting(setting); - } - } - } - } catch (FileNotFoundException e) { - Log.error("[SettingsFile] File not found: " + ini.getUri() + e.getMessage()); - if (view != null) - view.onSettingsFileNotFound(); - } catch (IOException e) { - Log.error("[SettingsFile] Error reading from: " + ini.getUri() + e.getMessage()); - if (view != null) - view.onSettingsFileNotFound(); - } finally { - if (reader != null) { - try { - reader.close(); - } catch (IOException e) { - Log.error("[SettingsFile] Error closing: " + ini.getUri() + e.getMessage()); - } - } - } - - return sections; - } - - public static HashMap<String, SettingSection> readFile(final String fileName, SettingsActivityView view) { - return readFile(getSettingsFile(fileName), false, view); - } - - /** - * Reads a given .ini file from disk and returns it as a HashMap of SettingSections, themselves - * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it - * failed. - * - * @param gameId the id of the game to load it's settings. - * @param view The current view. - */ - public static HashMap<String, SettingSection> readCustomGameSettings(final String gameId, SettingsActivityView view) { - return readFile(getCustomGameSettingsFile(gameId), true, view); - } - - /** - * Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error - * telling why it failed. - * - * @param fileName The target filename without a path or extension. - * @param sections The HashMap containing the Settings we want to serialize. - * @param view The current view. - */ - public static void saveFile(final String fileName, TreeMap<String, SettingSection> sections, - SettingsActivityView view) { - DocumentFile ini = getSettingsFile(fileName); - - try { - Context context = CitraApplication.Companion.getAppContext(); - InputStream inputStream = context.getContentResolver().openInputStream(ini.getUri()); - Wini writer = new Wini(inputStream); - - Set<String> keySet = sections.keySet(); - for (String key : keySet) { - SettingSection section = sections.get(key); - writeSection(writer, section); - } - inputStream.close(); - OutputStream outputStream = context.getContentResolver().openOutputStream(ini.getUri(), "wt"); - writer.store(outputStream); - outputStream.flush(); - outputStream.close(); - } catch (IOException e) { - Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.getMessage()); - view.showToastMessage(CitraApplication.Companion.getAppContext().getString(R.string.error_saving, fileName, e.getMessage()), false); - } - } - - private static String mapSectionNameFromIni(String generalSectionName) { - if (sectionsMap.getForward(generalSectionName) != null) { - return sectionsMap.getForward(generalSectionName); - } - - return generalSectionName; - } - - private static String mapSectionNameToIni(String generalSectionName) { - if (sectionsMap.getBackward(generalSectionName) != null) { - return sectionsMap.getBackward(generalSectionName); - } - - return generalSectionName; - } - - public static DocumentFile getSettingsFile(String fileName) { - DocumentFile root = DocumentFile.fromTreeUri(CitraApplication.Companion.getAppContext(), Uri.parse(DirectoryInitialization.INSTANCE.getUserDirectory())); - DocumentFile configDirectory = root.findFile("config"); - return configDirectory.findFile(fileName + ".ini"); - } - - private static DocumentFile getCustomGameSettingsFile(String gameId) { - DocumentFile root = DocumentFile.fromTreeUri(CitraApplication.Companion.getAppContext(), Uri.parse(DirectoryInitialization.INSTANCE.getUserDirectory())); - DocumentFile configDirectory = root.findFile("GameSettings"); - return configDirectory.findFile(gameId + ".ini"); - } - - private static SettingSection sectionFromLine(String line, boolean isCustomGame) { - String sectionName = line.substring(1, line.length() - 1); - if (isCustomGame) { - sectionName = mapSectionNameToIni(sectionName); - } - return new SettingSection(sectionName); - } - - /** - * For a line of text, determines what type of data is being represented, and returns - * a Setting object containing this data. - * - * @param current The section currently being parsed by the consuming method. - * @param line The line of text being parsed. - * @return A typed Setting containing the key/value contained in the line. - */ - private static Setting settingFromLine(SettingSection current, String line) { - String[] splitLine = line.split("="); - - if (splitLine.length != 2) { - Log.warning("Skipping invalid config line \"" + line + "\""); - return null; - } - - String key = splitLine[0].trim(); - String value = splitLine[1].trim(); - - if (value.isEmpty()) { - Log.warning("Skipping null value in config line \"" + line + "\""); - return null; - } - - try { - int valueAsInt = Integer.parseInt(value); - - return new IntSetting(key, current.getName(), valueAsInt); - } catch (NumberFormatException ex) { - } - - try { - float valueAsFloat = Float.parseFloat(value); - - return new FloatSetting(key, current.getName(), valueAsFloat); - } catch (NumberFormatException ex) { - } - - return new StringSetting(key, current.getName(), value); - } - - /** - * Writes the contents of a Section HashMap to disk. - * - * @param parser A Wini pointed at a file on disk. - * @param section A section containing settings to be written to the file. - */ - private static void writeSection(Wini parser, SettingSection section) { - // Write the section header. - String header = section.getName(); - - // Write this section's values. - HashMap<String, Setting> settings = section.getSettings(); - Set<String> keySet = settings.keySet(); - - for (String key : keySet) { - Setting setting = settings.get(key); - parser.put(header, setting.getKey(), setting.getValueAsString()); - } - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt new file mode 100644 index 0000000000..83b5da972c --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt @@ -0,0 +1,258 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.utils + +import android.content.Context +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import org.citra.citra_emu.CitraApplication +import org.citra.citra_emu.NativeLibrary +import org.citra.citra_emu.R +import org.citra.citra_emu.features.settings.model.AbstractSetting +import org.citra.citra_emu.features.settings.model.BooleanSetting +import org.citra.citra_emu.features.settings.model.FloatSetting +import org.citra.citra_emu.features.settings.model.IntSetting +import org.citra.citra_emu.features.settings.model.ScaledFloatSetting +import org.citra.citra_emu.features.settings.model.SettingSection +import org.citra.citra_emu.features.settings.model.Settings.SettingsSectionMap +import org.citra.citra_emu.features.settings.model.StringSetting +import org.citra.citra_emu.features.settings.ui.SettingsActivityView +import org.citra.citra_emu.utils.BiMap +import org.citra.citra_emu.utils.DirectoryInitialization.userDirectory +import org.citra.citra_emu.utils.Log +import org.ini4j.Wini +import java.io.* +import java.lang.NumberFormatException +import java.util.* + + +/** + * Contains static methods for interacting with .ini files in which settings are stored. + */ +object SettingsFile { + const val FILE_NAME_CONFIG = "config" + + private var sectionsMap = BiMap<String?, String?>() + + /** + * Reads a given .ini file from disk and returns it as a HashMap of Settings, themselves + * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it + * failed. + * + * @param ini The ini file to load the settings from + * @param isCustomGame + * @param view The current view. + * @return An Observable that emits a HashMap of the file's contents, then completes. + */ + fun readFile( + ini: DocumentFile, + isCustomGame: Boolean, + view: SettingsActivityView? + ): HashMap<String, SettingSection?> { + val sections: HashMap<String, SettingSection?> = SettingsSectionMap() + var reader: BufferedReader? = null + try { + val context: Context = CitraApplication.appContext + val inputStream = context.contentResolver.openInputStream(ini.uri) + reader = BufferedReader(InputStreamReader(inputStream)) + var current: SettingSection? = null + var line: String? + while (reader.readLine().also { line = it } != null) { + if (line!!.startsWith("[") && line!!.endsWith("]")) { + current = sectionFromLine(line!!, isCustomGame) + sections[current.name] = current + } else if (current != null) { + val setting = settingFromLine(line!!) + if (setting != null) { + current.putSetting(setting) + } + } + } + } catch (e: FileNotFoundException) { + Log.error("[SettingsFile] File not found: " + ini.uri + e.message) + view?.onSettingsFileNotFound() + } catch (e: IOException) { + Log.error("[SettingsFile] Error reading from: " + ini.uri + e.message) + view?.onSettingsFileNotFound() + } finally { + if (reader != null) { + try { + reader.close() + } catch (e: IOException) { + Log.error("[SettingsFile] Error closing: " + ini.uri + e.message) + } + } + } + return sections + } + + fun readFile(fileName: String, view: SettingsActivityView?): HashMap<String, SettingSection?> { + return readFile(getSettingsFile(fileName), false, view) + } + + fun readFile(fileName: String): HashMap<String, SettingSection?> = readFile(fileName, null) + + /** + * Reads a given .ini file from disk and returns it as a HashMap of SettingSections, themselves + * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it + * failed. + * + * @param gameId the id of the game to load it's settings. + * @param view The current view. + */ + fun readCustomGameSettings( + gameId: String, + view: SettingsActivityView? + ): HashMap<String, SettingSection?> { + return readFile(getCustomGameSettingsFile(gameId), true, view) + } + + /** + * Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error + * telling why it failed. + * + * @param fileName The target filename without a path or extension. + * @param sections The HashMap containing the Settings we want to serialize. + * @param view The current view. + */ + fun saveFile( + fileName: String, + sections: TreeMap<String, SettingSection?>, + view: SettingsActivityView + ) { + val ini = getSettingsFile(fileName) + try { + val context: Context = CitraApplication.appContext + val inputStream = context.contentResolver.openInputStream(ini.uri) + val writer = Wini(inputStream) + val keySet: Set<String> = sections.keys + for (key in keySet) { + val section = sections[key] + writeSection(writer, section!!) + } + inputStream!!.close() + val outputStream = context.contentResolver.openOutputStream(ini.uri, "wt") + writer.store(outputStream) + outputStream!!.flush() + outputStream.close() + } catch (e: Exception) { + Log.error("[SettingsFile] File not found: $fileName.ini: ${e.message}") + view.showToastMessage( + CitraApplication.appContext + .getString(R.string.error_saving, fileName, e.message), false + ) + } + } + + private fun mapSectionNameFromIni(generalSectionName: String): String? { + return if (sectionsMap.getForward(generalSectionName) != null) { + sectionsMap.getForward(generalSectionName) + } else { + generalSectionName + } + } + + private fun mapSectionNameToIni(generalSectionName: String): String { + return if (sectionsMap.getBackward(generalSectionName) != null) { + sectionsMap.getBackward(generalSectionName).toString() + } else { + generalSectionName + } + } + + fun getSettingsFile(fileName: String): DocumentFile { + val root = DocumentFile.fromTreeUri(CitraApplication.appContext, Uri.parse(userDirectory)) + val configDirectory = root!!.findFile("config") + return configDirectory!!.findFile("$fileName.ini")!! + } + + private fun getCustomGameSettingsFile(gameId: String): DocumentFile { + val root = DocumentFile.fromTreeUri(CitraApplication.appContext, Uri.parse(userDirectory)) + val configDirectory = root!!.findFile("GameSettings") + return configDirectory!!.findFile("$gameId.ini")!! + } + + private fun sectionFromLine(line: String, isCustomGame: Boolean): SettingSection { + var sectionName: String = line.substring(1, line.length - 1) + if (isCustomGame) { + sectionName = mapSectionNameToIni(sectionName) + } + return SettingSection(sectionName) + } + + /** + * For a line of text, determines what type of data is being represented, and returns + * a Setting object containing this data. + * + * @param line The line of text being parsed. + * @return A typed Setting containing the key/value contained in the line. + */ + private fun settingFromLine(line: String): AbstractSetting? { + val splitLine = line.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + if (splitLine.size != 2) { + return null + } + val key = splitLine[0].trim { it <= ' ' } + val value = splitLine[1].trim { it <= ' ' } + if (value.isEmpty()) { + return null + } + + val booleanSetting = BooleanSetting.from(key) + if (booleanSetting != null) { + booleanSetting.boolean = value.toBoolean() + return booleanSetting + } + + val intSetting = IntSetting.from(key) + if (intSetting != null) { + try { + intSetting.int = value.toInt() + } catch (e: NumberFormatException) { + intSetting.int = if (value.toBoolean()) 1 else 0 + } + return intSetting + } + + val scaledFloatSetting = ScaledFloatSetting.from(key) + if (scaledFloatSetting != null) { + scaledFloatSetting.float = value.toFloat() * scaledFloatSetting.scale + return scaledFloatSetting + } + + val floatSetting = FloatSetting.from(key) + if (floatSetting != null) { + floatSetting.float = value.toFloat() + return floatSetting + } + + val stringSetting = StringSetting.from(key) + if (stringSetting != null) { + stringSetting.string = value + return stringSetting + } + + return null + } + + /** + * Writes the contents of a Section HashMap to disk. + * + * @param parser A Wini pointed at a file on disk. + * @param section A section containing settings to be written to the file. + */ + private fun writeSection(parser: Wini, section: SettingSection) { + // Write the section header. + val header = section.name + + // Write this section's values. + val settings = section.settings + val keySet: Set<String> = settings.keys + for (key in keySet) { + val setting = settings[key] + parser.put(header, setting!!.key, setting.valueAsString) + } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt index 05379d8d6b..b19fdd7b9b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt @@ -28,6 +28,7 @@ import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.R import org.citra.citra_emu.adapters.HomeSettingAdapter import org.citra.citra_emu.databinding.FragmentHomeSettingsBinding +import org.citra.citra_emu.features.settings.model.Settings import org.citra.citra_emu.features.settings.ui.SettingsActivity import org.citra.citra_emu.features.settings.utils.SettingsFile import org.citra.citra_emu.model.HomeSetting @@ -124,6 +125,12 @@ class HomeSettingsFragment : Fragment() { { getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) }, details = homeViewModel.gamesDir ), + HomeSetting( + R.string.preferences_theme, + R.string.theme_and_color_description, + R.drawable.ic_palette, + { SettingsActivity.launch(requireContext(), Settings.SECTION_THEME, "") } + ), HomeSetting( R.string.about, R.string.about_description, diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt new file mode 100644 index 0000000000..cf42bed129 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt @@ -0,0 +1,208 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.fragments + +import android.content.DialogInterface +import android.os.Bundle +import android.view.InputDevice +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.citra.citra_emu.R +import org.citra.citra_emu.databinding.DialogInputBinding +import org.citra.citra_emu.features.settings.model.view.InputBindingSetting +import org.citra.citra_emu.utils.Log +import kotlin.math.abs + +class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() { + private var _binding: DialogInputBinding? = null + private val binding get() = _binding!! + + private var setting: InputBindingSetting? = null + private var onCancel: (() -> Unit)? = null + private var onDismiss: (() -> Unit)? = null + + private val previousValues = ArrayList<Float>() + private var prevDeviceId = 0 + private var waitingForEvent = true + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (setting == null) { + dismiss() + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = DialogInputBinding.inflate(inflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + BottomSheetBehavior.from<View>(view.parent as View).state = + BottomSheetBehavior.STATE_EXPANDED + + isCancelable = false + view.requestFocus() + view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() } + if (setting!!.isButtonMappingSupported()) { + dialog?.setOnKeyListener { _, _, event -> onKeyEvent(event) } + } + if (setting!!.isAxisMappingSupported()) { + binding.root.setOnGenericMotionListener { _, event -> onMotionEvent(event) } + } + + val inputTypeId = when { + setting!!.isCirclePad() -> R.string.controller_circlepad + setting!!.isCStick() -> R.string.controller_c + setting!!.isDPad() -> R.string.controller_dpad + setting!!.isTrigger() -> R.string.controller_trigger + else -> R.string.button + } + binding.textTitle.text = + String.format( + getString(R.string.input_dialog_title), + getString(inputTypeId), + getString(setting!!.nameId) + ) + + var messageResId: Int = R.string.input_dialog_description + if (setting!!.isAxisMappingSupported() && !setting!!.isTrigger()) { + // Use specialized message for axis left/right or up/down + messageResId = if (setting!!.isHorizontalOrientation()) { + R.string.input_binding_description_horizontal_axis + } else { + R.string.input_binding_description_vertical_axis + } + } + binding.textMessage.text = getString(messageResId) + + binding.buttonClear.setOnClickListener { + setting?.removeOldMapping() + dismiss() + } + binding.buttonCancel.setOnClickListener { + onCancel?.invoke() + dismiss() + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + onDismiss?.invoke() + } + + private fun onKeyEvent(event: KeyEvent): Boolean { + Log.debug("[MotionBottomSheetDialogFragment] Received key event: " + event.action) + return when (event.action) { + KeyEvent.ACTION_UP -> { + setting?.onKeyInput(event) + dismiss() + // Even if we ignore the key, we still consume it. Thus return true regardless. + true + } + + else -> false + } + } + + private fun onMotionEvent(event: MotionEvent): Boolean { + Log.debug("[MotionBottomSheetDialogFragment] Received motion event: " + event.action) + if (event.source and InputDevice.SOURCE_CLASS_JOYSTICK == 0) return false + if (event.action != MotionEvent.ACTION_MOVE) return false + + val input = event.device + + val motionRanges = input.motionRanges + + if (input.id != prevDeviceId) { + previousValues.clear() + } + prevDeviceId = input.id + val firstEvent = previousValues.isEmpty() + + var numMovedAxis = 0 + var axisMoveValue = 0.0f + var lastMovedRange: InputDevice.MotionRange? = null + var lastMovedDir = '?' + if (waitingForEvent) { + for (i in motionRanges.indices) { + val range = motionRanges[i] + val axis = range.axis + val origValue = event.getAxisValue(axis) + if (firstEvent) { + previousValues.add(origValue) + } else { + val previousValue = previousValues[i] + + // Only handle the axes that are not neutral (more than 0.5) + // but ignore any axis that has a constant value (e.g. always 1) + if (abs(origValue) > 0.5f && origValue != previousValue) { + // It is common to have multiple axes with the same physical input. For example, + // shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE. + // To handle this, we ignore an axis motion that's the exact same as a motion + // we already saw. This way, we ignore axes with two names, but catch the case + // where a joystick is moved in two directions. + // ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html + if (origValue != axisMoveValue) { + axisMoveValue = origValue + numMovedAxis++ + lastMovedRange = range + lastMovedDir = if (origValue < 0.0f) '-' else '+' + } + } else if (abs(origValue) < 0.25f && abs(previousValue) > 0.75f) { + // Special case for d-pads (axis value jumps between 0 and 1 without any values + // in between). Without this, the user would need to press the d-pad twice + // due to the first press being caught by the "if (firstEvent)" case further up. + numMovedAxis++ + lastMovedRange = range + lastMovedDir = if (previousValue < 0.0f) '-' else '+' + } + } + previousValues[i] = origValue + } + + // If only one axis moved, that's the winner. + if (numMovedAxis == 1) { + waitingForEvent = false + setting?.onMotionInput(input, lastMovedRange!!, lastMovedDir) + dismiss() + } + } + return true + } + + companion object { + const val TAG = "MotionBottomSheetDialogFragment" + + fun newInstance( + setting: InputBindingSetting, + onCancel: () -> Unit, + onDismiss: () -> Unit + ): MotionBottomSheetDialogFragment { + val dialog = MotionBottomSheetDialogFragment() + dialog.apply { + this.setting = setting + this.onCancel = onCancel + this.onDismiss = onDismiss + } + return dialog + } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/ResetSettingsDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/ResetSettingsDialogFragment.kt new file mode 100644 index 0000000000..d4e4dec657 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/ResetSettingsDialogFragment.kt @@ -0,0 +1,31 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.fragments + +import android.app.Dialog +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.citra.citra_emu.R +import org.citra.citra_emu.features.settings.ui.SettingsActivity + +class ResetSettingsDialogFragment : DialogFragment() { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val settingsActivity = requireActivity() as SettingsActivity + + return MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.reset_all_settings) + .setMessage(R.string.reset_all_settings_description) + .setPositiveButton(android.R.string.ok) { _, _ -> + settingsActivity.onSettingsReset() + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + companion object { + const val TAG = "ResetSettingsDialogFragment" + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt index 3a9f8167cb..219f769fee 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt @@ -25,7 +25,6 @@ import androidx.navigation.findNavController import androidx.preference.PreferenceManager import com.google.android.material.textfield.MaterialAutoCompleteTextView import com.google.android.material.transition.MaterialSharedAxis -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.NativeLibrary @@ -33,6 +32,7 @@ import org.citra.citra_emu.R import org.citra.citra_emu.activities.EmulationActivity import org.citra.citra_emu.databinding.FragmentSystemFilesBinding import org.citra.citra_emu.features.settings.model.Settings +import org.citra.citra_emu.utils.SystemSaveGame import org.citra.citra_emu.viewmodel.GamesViewModel import org.citra.citra_emu.viewmodel.HomeViewModel import org.citra.citra_emu.viewmodel.SystemFilesViewModel @@ -74,7 +74,7 @@ class SystemFilesFragment : Fragment() { super.onCreate(savedInstanceState) enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) - NativeLibrary.loadSystemConfig() + SystemSaveGame.load() } override fun onCreateView( @@ -149,15 +149,15 @@ class SystemFilesFragment : Fragment() { override fun onPause() { super.onPause() - NativeLibrary.saveSystemConfig() + SystemSaveGame.save() } private fun reloadUi() { val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) - binding.switchRunSystemSetup.isChecked = NativeLibrary.getIsSystemSetupNeeded() + binding.switchRunSystemSetup.isChecked = SystemSaveGame.getIsSystemSetupNeeded() binding.switchRunSystemSetup.setOnCheckedChangeListener { _, isChecked -> - NativeLibrary.setSystemSetupNeeded(isChecked) + SystemSaveGame.setSystemSetupNeeded(isChecked) } val showHomeApps = preferences.getBoolean(Settings.PREF_SHOW_HOME_APPS, false) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt index cb198f31e3..6010851ea2 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt @@ -41,6 +41,7 @@ import org.citra.citra_emu.activities.EmulationActivity import org.citra.citra_emu.contracts.OpenFileResultContract import org.citra.citra_emu.databinding.ActivityMainBinding import org.citra.citra_emu.features.settings.model.Settings +import org.citra.citra_emu.features.settings.model.SettingsViewModel import org.citra.citra_emu.features.settings.ui.SettingsActivity import org.citra.citra_emu.features.settings.utils.SettingsFile import org.citra.citra_emu.fragments.SelectUserDirectoryDialogFragment @@ -54,11 +55,14 @@ import org.citra.citra_emu.utils.ThemeUtil import org.citra.citra_emu.viewmodel.GamesViewModel import org.citra.citra_emu.viewmodel.HomeViewModel -class MainActivity : AppCompatActivity() { +class MainActivity : AppCompatActivity(), ThemeProvider { private lateinit var binding: ActivityMainBinding private val homeViewModel: HomeViewModel by viewModels() private val gamesViewModel: GamesViewModel by viewModels() + private val settingsViewModel: SettingsViewModel by viewModels() + + override var themeId: Int = 0 override fun onCreate(savedInstanceState: Bundle?) { val splashScreen = installSplashScreen() @@ -67,6 +71,11 @@ class MainActivity : AppCompatActivity() { PermissionsHandler.hasWriteAccess(this) } + if (PermissionsHandler.hasWriteAccess(applicationContext) && + DirectoryInitialization.areCitraDirectoriesReady()) { + settingsViewModel.settings.loadSettings() + } + ThemeUtil.setTheme(this) super.onCreate(savedInstanceState) @@ -155,6 +164,8 @@ class MainActivity : AppCompatActivity() { override fun onResume() { checkUserPermissions() + + ThemeUtil.setCorrectTheme(this) super.onResume() } @@ -163,6 +174,11 @@ class MainActivity : AppCompatActivity() { super.onDestroy() } + override fun setTheme(resId: Int) { + super.setTheme(resId) + themeId = resId + } + private fun checkUserPermissions() { val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext) .getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/ThemeProvider.kt b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/ThemeProvider.kt new file mode 100644 index 0000000000..87bca2ce26 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/ThemeProvider.kt @@ -0,0 +1,12 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.ui.main + +interface ThemeProvider { + /** + * Provides theme ID by overriding an activity's 'setTheme' method and returning that result + */ + var themeId: Int +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/SystemSaveGame.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/SystemSaveGame.kt new file mode 100644 index 0000000000..f451762d7f --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/SystemSaveGame.kt @@ -0,0 +1,67 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.utils + +object SystemSaveGame { + external fun save() + + external fun load() + + external fun getIsSystemSetupNeeded(): Boolean + + external fun setSystemSetupNeeded(needed: Boolean) + + external fun getUsername(): String + + external fun setUsername(username: String) + + /** + * Returns birthday as an array with the month as the first element and the + * day as the second element + */ + external fun getBirthday(): ShortArray + + external fun setBirthday(month: Short, day: Short) + + external fun getSystemLanguage(): Int + + external fun setSystemLanguage(language: Int) + + external fun getSoundOutputMode(): Int + + external fun setSoundOutputMode(mode: Int) + + external fun getCountryCode(): Short + + external fun setCountryCode(countryCode: Short) + + external fun getPlayCoins(): Int + + external fun setPlayCoins(coins: Int) + + external fun getConsoleId(): Long + + external fun regenerateConsoleId() +} + +enum class BirthdayMonth(val code: Int, val days: Int) { + JANUARY(1, 31), + FEBRUARY(2, 29), + MARCH(3, 31), + APRIL(4, 30), + MAY(5, 31), + JUNE(6, 30), + JULY(7, 31), + AUGUST(8, 31), + SEPTEMBER(9, 30), + OCTOBER(10, 31), + NOVEMBER(11, 30), + DECEMBER(12, 31); + + companion object { + fun getMonthFromCode(code: Short): BirthdayMonth? = + entries.firstOrNull { it.code == code.toInt() } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/ThemeUtil.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/ThemeUtil.kt index ce3d24ceb4..69758a322b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/ThemeUtil.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/ThemeUtil.kt @@ -16,6 +16,7 @@ import androidx.preference.PreferenceManager import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.R import org.citra.citra_emu.features.settings.model.Settings +import org.citra.citra_emu.ui.main.ThemeProvider import kotlin.math.roundToInt object ThemeUtil { @@ -26,6 +27,20 @@ object ThemeUtil { fun setTheme(activity: AppCompatActivity) { setThemeMode(activity) + if (preferences.getBoolean(Settings.PREF_MATERIAL_YOU, false)) { + activity.setTheme(R.style.Theme_Citra_Main_MaterialYou) + } else { + activity.setTheme(R.style.Theme_Citra_Main) + } + + // Using a specific night mode check because this could apply incorrectly when using the + // light app mode, dark system mode, and black backgrounds. Launching the settings activity + // will then show light mode colors/navigation bars but with black backgrounds. + if (preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) && + isNightMode(activity) + ) { + activity.setTheme(R.style.ThemeOverlay_Citra_Dark) + } } fun setThemeMode(activity: AppCompatActivity) { @@ -64,6 +79,14 @@ object ThemeUtil { windowController.isAppearanceLightNavigationBars = false } + fun setCorrectTheme(activity: AppCompatActivity) { + val currentTheme = (activity as ThemeProvider).themeId + setTheme(activity) + if (currentTheme != (activity as ThemeProvider).themeId) { + activity.recreate() + } + } + @ColorInt fun getColorWithOpacity(@ColorInt color: Int, alphaFactor: Float): Int { return Color.argb( diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt index bc0a6b94ac..624826e0f0 100644 --- a/src/android/app/src/main/jni/CMakeLists.txt +++ b/src/android/app/src/main/jni/CMakeLists.txt @@ -31,6 +31,7 @@ add_library(citra-android SHARED native.cpp ndk_motion.cpp ndk_motion.h + system_save_game.cpp ) target_link_libraries(citra-android PRIVATE audio_core citra_common citra_core input_common network) diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index e0808b7f39..574b824a2a 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -75,13 +75,6 @@ static const std::array<int, Settings::NativeAnalog::NumAnalogs> default_analogs InputManager::N3DS_STICK_C, }}; -void Config::UpdateCFG() { - std::shared_ptr<Service::CFG::Module> cfg = std::make_shared<Service::CFG::Module>(); - cfg->SetSystemLanguage(static_cast<Service::CFG::SystemLanguage>( - sdl2_config->GetInteger("System", "language", Service::CFG::SystemLanguage::LANGUAGE_EN))); - cfg->UpdateConfigNANDSavegame(); -} - template <> void Config::ReadSetting(const std::string& group, Settings::Setting<std::string>& setting) { std::string setting_value = sdl2_config->Get(group, setting.GetLabel(), setting.GetDefault()); @@ -215,24 +208,11 @@ void Config::ReadValues() { ReadSetting("System", Settings::values.region_value); ReadSetting("System", Settings::values.init_clock); { - std::tm t; - t.tm_sec = 1; - t.tm_min = 0; - t.tm_hour = 0; - t.tm_mday = 1; - t.tm_mon = 0; - t.tm_year = 100; - t.tm_isdst = 0; - std::istringstream string_stream( - sdl2_config->GetString("System", "init_time", "2000-01-01 00:00:01")); - string_stream >> std::get_time(&t, "%Y-%m-%d %H:%M:%S"); - if (string_stream.fail()) { - LOG_ERROR(Config, "Failed To parse init_time. Using 2000-01-01 00:00:01"); + std::string time = sdl2_config->GetString("System", "init_time", "946681277"); + try { + Settings::values.init_time = std::stoll(time); + } catch (...) { } - Settings::values.init_time = - std::chrono::duration_cast<std::chrono::seconds>( - std::chrono::system_clock::from_time_t(std::mktime(&t)).time_since_epoch()) - .count(); } ReadSetting("System", Settings::values.plugin_loader_enabled); ReadSetting("System", Settings::values.allow_plugin_loader); @@ -286,9 +266,6 @@ void Config::ReadValues() { sdl2_config->GetString("WebService", "web_api_url", "https://api.citra-emu.org"); NetSettings::values.citra_username = sdl2_config->GetString("WebService", "citra_username", ""); NetSettings::values.citra_token = sdl2_config->GetString("WebService", "citra_token", ""); - - // Update CFG file based on settings - UpdateCFG(); } void Config::Reload() { diff --git a/src/android/app/src/main/jni/config.h b/src/android/app/src/main/jni/config.h index 162225728a..c2b0abcea0 100644 --- a/src/android/app/src/main/jni/config.h +++ b/src/android/app/src/main/jni/config.h @@ -17,7 +17,6 @@ private: bool LoadINI(const std::string& default_contents = "", bool retry = true); void ReadValues(); - void UpdateCFG(); public: Config(); diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index dbd845a840..f67d29e9cd 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -64,7 +64,6 @@ ANativeWindow* s_surf; std::shared_ptr<Common::DynamicLibrary> vulkan_library{}; std::unique_ptr<EmuWindow_Android> window; -std::shared_ptr<Service::CFG::Module> cfg; std::atomic<bool> stop_run{true}; std::atomic<bool> pause_emulation{false}; @@ -732,29 +731,4 @@ void Java_org_citra_citra_1emu_NativeLibrary_logDeviceInfo([[maybe_unused]] JNIE LOG_INFO(Frontend, "Host OS: Android API level {}", android_get_device_api_level()); } -void Java_org_citra_citra_1emu_NativeLibrary_loadSystemConfig([[maybe_unused]] JNIEnv* env, - [[maybe_unused]] jobject obj) { - if (Core::System::GetInstance().IsPoweredOn()) { - cfg = Service::CFG::GetModule(Core::System::GetInstance()); - } else { - cfg = std::make_shared<Service::CFG::Module>(); - } -} - -void Java_org_citra_citra_1emu_NativeLibrary_saveSystemConfig([[maybe_unused]] JNIEnv* env, - [[maybe_unused]] jobject obj) { - cfg->UpdateConfigNANDSavegame(); -} - -void Java_org_citra_citra_1emu_NativeLibrary_setSystemSetupNeeded([[maybe_unused]] JNIEnv* env, - [[maybe_unused]] jobject obj, - jboolean needed) { - cfg->SetSystemSetupNeeded(needed); -} - -jboolean Java_org_citra_citra_1emu_NativeLibrary_getIsSystemSetupNeeded( - [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) { - return cfg->IsSystemSetupNeeded(); -} - } // extern "C" diff --git a/src/android/app/src/main/jni/system_save_game.cpp b/src/android/app/src/main/jni/system_save_game.cpp new file mode 100644 index 0000000000..003ad1b638 --- /dev/null +++ b/src/android/app/src/main/jni/system_save_game.cpp @@ -0,0 +1,122 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <common/string_util.h> +#include <core/core.h> +#include <core/hle/service/cfg/cfg.h> +#include <core/hle/service/ptm/ptm.h> +#include "android_common/android_common.h" + +std::shared_ptr<Service::CFG::Module> cfg; + +extern "C" { + +void Java_org_citra_citra_1emu_utils_SystemSaveGame_save([[maybe_unused]] JNIEnv* env, + [[maybe_unused]] jobject obj) { + cfg->UpdateConfigNANDSavegame(); +} + +void Java_org_citra_citra_1emu_utils_SystemSaveGame_load([[maybe_unused]] JNIEnv* env, + [[maybe_unused]] jobject obj) { + if (Core::System::GetInstance().IsPoweredOn()) { + cfg = Service::CFG::GetModule(Core::System::GetInstance()); + } else { + cfg = std::make_shared<Service::CFG::Module>(); + } +} + +jboolean Java_org_citra_citra_1emu_utils_SystemSaveGame_getIsSystemSetupNeeded( + [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) { + return cfg->IsSystemSetupNeeded(); +} + +void Java_org_citra_citra_1emu_utils_SystemSaveGame_setSystemSetupNeeded( + [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj, jboolean needed) { + cfg->SetSystemSetupNeeded(needed); +} + +jstring Java_org_citra_citra_1emu_utils_SystemSaveGame_getUsername([[maybe_unused]] JNIEnv* env, + [[maybe_unused]] jobject obj) { + return ToJString(env, Common::UTF16ToUTF8(cfg->GetUsername())); +} + +void Java_org_citra_citra_1emu_utils_SystemSaveGame_setUsername([[maybe_unused]] JNIEnv* env, + [[maybe_unused]] jobject obj, + jstring username) { + cfg->SetUsername(Common::UTF8ToUTF16(GetJString(env, username))); +} + +jshortArray Java_org_citra_citra_1emu_utils_SystemSaveGame_getBirthday( + [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) { + jshortArray jbirthdayArray = env->NewShortArray(2); + auto birthday = cfg->GetBirthday(); + jshort birthdayArray[2]{static_cast<jshort>(get<0>(birthday)), + static_cast<jshort>(get<1>(birthday))}; + env->SetShortArrayRegion(jbirthdayArray, 0, 2, birthdayArray); + return jbirthdayArray; +} + +void Java_org_citra_citra_1emu_utils_SystemSaveGame_setBirthday([[maybe_unused]] JNIEnv* env, + [[maybe_unused]] jobject obj, + jshort jmonth, jshort jday) { + cfg->SetBirthday(static_cast<u8>(jmonth), static_cast<u8>(jday)); +} + +jint Java_org_citra_citra_1emu_utils_SystemSaveGame_getSystemLanguage( + [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) { + return cfg->GetSystemLanguage(); +} + +void Java_org_citra_citra_1emu_utils_SystemSaveGame_setSystemLanguage([[maybe_unused]] JNIEnv* env, + [[maybe_unused]] jobject obj, + jint jsystemLanguage) { + cfg->SetSystemLanguage(static_cast<Service::CFG::SystemLanguage>(jsystemLanguage)); +} + +jint Java_org_citra_citra_1emu_utils_SystemSaveGame_getSoundOutputMode( + [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) { + return cfg->GetSoundOutputMode(); +} + +void Java_org_citra_citra_1emu_utils_SystemSaveGame_setSoundOutputMode([[maybe_unused]] JNIEnv* env, + [[maybe_unused]] jobject obj, + jint jmode) { + cfg->SetSoundOutputMode(static_cast<Service::CFG::SoundOutputMode>(jmode)); +} + +jshort Java_org_citra_citra_1emu_utils_SystemSaveGame_getCountryCode([[maybe_unused]] JNIEnv* env, + [[maybe_unused]] jobject obj) { + return cfg->GetCountryCode(); +} + +void Java_org_citra_citra_1emu_utils_SystemSaveGame_setCountryCode([[maybe_unused]] JNIEnv* env, + [[maybe_unused]] jobject obj, + jshort jmode) { + cfg->SetCountryCode(static_cast<u8>(jmode)); +} + +jint Java_org_citra_citra_1emu_utils_SystemSaveGame_getPlayCoins([[maybe_unused]] JNIEnv* env, + [[maybe_unused]] jobject obj) { + return Service::PTM::Module::GetPlayCoins(); +} + +void Java_org_citra_citra_1emu_utils_SystemSaveGame_setPlayCoins([[maybe_unused]] JNIEnv* env, + [[maybe_unused]] jobject obj, + jint jcoins) { + Service::PTM::Module::SetPlayCoins(static_cast<u16>(jcoins)); +} + +jlong Java_org_citra_citra_1emu_utils_SystemSaveGame_getConsoleId([[maybe_unused]] JNIEnv* env, + [[maybe_unused]] jobject obj) { + return cfg->GetConsoleUniqueId(); +} + +void Java_org_citra_citra_1emu_utils_SystemSaveGame_regenerateConsoleId( + [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) { + const auto [random_number, console_id] = cfg->GenerateConsoleUniqueId(); + cfg->SetConsoleUniqueId(random_number, console_id); + cfg->UpdateConfigNANDSavegame(); +} + +} // extern "C" diff --git a/src/android/app/src/main/res/drawable/ic_palette.xml b/src/android/app/src/main/res/drawable/ic_palette.xml new file mode 100644 index 0000000000..43daec1ff4 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_palette.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="?attr/colorControlNormal" + android:pathData="M12,2C6.49,2 2,6.49 2,12s4.49,10 10,10c1.38,0 2.5,-1.12 2.5,-2.5c0,-0.61 -0.23,-1.2 -0.64,-1.67c-0.08,-0.1 -0.13,-0.21 -0.13,-0.33c0,-0.28 0.22,-0.5 0.5,-0.5H16c3.31,0 6,-2.69 6,-6C22,6.04 17.51,2 12,2zM17.5,13c-0.83,0 -1.5,-0.67 -1.5,-1.5c0,-0.83 0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5C19,12.33 18.33,13 17.5,13zM14.5,9C13.67,9 13,8.33 13,7.5C13,6.67 13.67,6 14.5,6S16,6.67 16,7.5C16,8.33 15.33,9 14.5,9zM5,11.5C5,10.67 5.67,10 6.5,10S8,10.67 8,11.5C8,12.33 7.33,13 6.5,13S5,12.33 5,11.5zM11,7.5C11,8.33 10.33,9 9.5,9S8,8.33 8,7.5C8,6.67 8.67,6 9.5,6S11,6.67 11,7.5z" /> +</vector> diff --git a/src/android/app/src/main/res/layout/activity_settings.xml b/src/android/app/src/main/res/layout/activity_settings.xml index c57abfd0a6..14ae83b041 100644 --- a/src/android/app/src/main/res/layout/activity_settings.xml +++ b/src/android/app/src/main/res/layout/activity_settings.xml @@ -1,20 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout + android:id="@+id/coordinator_main" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@+id/coordinator_settings" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:background="?attr/colorSurface"> <com.google.android.material.appbar.AppBarLayout android:id="@+id/appbar_settings" android:layout_width="match_parent" android:layout_height="wrap_content" - android:fitsSystemWindows="true"> + android:fitsSystemWindows="true" + app:elevation="0dp"> - <com.google.android.material.appbar.MaterialToolbar - android:id="@+id/toolbar_settings" + <com.google.android.material.appbar.CollapsingToolbarLayout + style="?attr/collapsingToolbarLayoutMediumStyle" + android:id="@+id/toolbar_settings_layout" android:layout_width="match_parent" - android:layout_height="?attr/actionBarSize" /> + android:layout_height="?attr/collapsingToolbarLayoutMediumSize" + app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"> + + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/toolbar_settings" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + app:layout_collapseMode="pin" /> + + </com.google.android.material.appbar.CollapsingToolbarLayout> </com.google.android.material.appbar.AppBarLayout> @@ -22,6 +35,16 @@ android:id="@+id/frame_content" android:layout_width="match_parent" android:layout_height="match_parent" + android:layout_marginHorizontal="12dp" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + <View + android:id="@+id/navigation_bar_shade" + android:layout_width="match_parent" + android:layout_height="1px" + android:background="@android:color/transparent" + android:clickable="false" + android:focusable="false" + android:layout_gravity="bottom|center_horizontal" /> + </androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/src/android/app/src/main/res/layout/dialog_input.xml b/src/android/app/src/main/res/layout/dialog_input.xml new file mode 100644 index 0000000000..3ea1e4d7b5 --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_input.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingHorizontal="24dp" + android:paddingBottom="24dp" + android:defaultFocusHighlightEnabled="false" + android:focusable="true" + android:focusableInTouchMode="true" + android:focusedByDefault="true" + android:orientation="vertical"> + + <com.google.android.material.bottomsheet.BottomSheetDragHandleView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/text_title" + style="@style/TextAppearance.Material3.HeadlineSmall" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:text="@string/start" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/text_message" + style="@style/TextAppearance.Material3.BodyLarge" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:gravity="center" + tools:text="@string/input_binding_description" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:orientation="horizontal"> + + <Button + android:id="@+id/button_cancel" + style="@style/Widget.Material3.Button.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:layout_weight="1" + android:focusable="false" + android:text="@android:string/cancel" /> + + <Button + android:id="@+id/button_clear" + style="@style/Widget.Material3.Button.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:layout_weight="1" + android:focusable="false" + android:text="@string/clear" /> + + </LinearLayout> + +</LinearLayout> diff --git a/src/android/app/src/main/res/layout/dialog_software_keyboard.xml b/src/android/app/src/main/res/layout/dialog_software_keyboard.xml new file mode 100644 index 0000000000..a6d315278e --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_software_keyboard.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<com.google.android.material.textfield.TextInputLayout 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:id="@+id/edit_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="16dp" + android:paddingHorizontal="24dp" + tools:hint="@string/cheats_name" + app:errorEnabled="true" + app:layout_constraintBottom_toTopOf="@id/edit_notes" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/edit_text_input" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:importantForAutofill="no" + android:inputType="text" + android:minHeight="48dp" + android:textAlignment="viewStart" /> + +</com.google.android.material.textfield.TextInputLayout> diff --git a/src/android/app/src/main/res/layout/list_item_setting.xml b/src/android/app/src/main/res/layout/list_item_setting.xml index a2a07b3811..2aaaa4ba63 100644 --- a/src/android/app/src/main/res/layout/list_item_setting.xml +++ b/src/android/app/src/main/res/layout/list_item_setting.xml @@ -1,5 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -8,34 +10,43 @@ android:focusable="true" android:gravity="center_vertical" android:minHeight="72dp" - android:paddingTop="@dimen/spacing_large" - android:paddingBottom="@dimen/spacing_large"> + android:padding="@dimen/spacing_large"> - <TextView - android:id="@+id/text_setting_name" - style="@style/TextAppearance.AppCompat.Headline" - android:layout_width="0dp" + <LinearLayout + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentStart="true" - android:layout_alignParentTop="true" - android:layout_alignParentEnd="true" - android:layout_marginStart="@dimen/spacing_large" - android:layout_marginEnd="@dimen/spacing_large" - android:textSize="16sp" - tools:text="Setting Name" /> + android:orientation="vertical"> - <TextView - android:id="@+id/text_setting_description" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_below="@+id/text_setting_name" - android:layout_alignStart="@+id/text_setting_name" - android:layout_alignParentStart="true" - android:layout_alignParentEnd="true" - android:layout_marginStart="@dimen/spacing_large" - android:layout_marginTop="@dimen/spacing_small" - android:layout_marginEnd="@dimen/spacing_large" - android:visibility="visible" - tools:text="@string/app_disclaimer" /> + <com.google.android.material.textview.MaterialTextView + android:id="@+id/text_setting_name" + style="@style/TextAppearance.Material3.HeadlineMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAlignment="viewStart" + android:textSize="16sp" + app:lineHeight="22dp" + tools:text="Setting Name" /> -</RelativeLayout> \ No newline at end of file + <com.google.android.material.textview.MaterialTextView + android:id="@+id/text_setting_description" + style="@style/TextAppearance.Material3.BodySmall" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_small" + android:textAlignment="viewStart" + tools:text="@string/app_disclaimer" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/text_setting_value" + style="@style/TextAppearance.Material3.LabelMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_small" + android:textAlignment="viewStart" + android:textStyle="bold" + android:visibility="gone" + tools:text="1x" /> + + </LinearLayout> + +</RelativeLayout> diff --git a/src/android/app/src/main/res/layout/list_item_setting_switch.xml b/src/android/app/src/main/res/layout/list_item_setting_switch.xml new file mode 100644 index 0000000000..cbace0e7f7 --- /dev/null +++ b/src/android/app/src/main/res/layout/list_item_setting_switch.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?android:attr/selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:minHeight="72dp" + android:paddingVertical="@dimen/spacing_large" + android:paddingStart="@dimen/spacing_large" + android:paddingEnd="24dp"> + + <com.google.android.material.materialswitch.MaterialSwitch + android:id="@+id/switch_widget" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_centerVertical="true" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_centerVertical="true" + android:layout_marginEnd="@dimen/spacing_large" + android:layout_toStartOf="@+id/switch_widget" + android:gravity="center_vertical" + android:orientation="vertical"> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/text_setting_name" + style="@style/TextAppearance.Material3.HeadlineMedium" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAlignment="viewStart" + android:textSize="16sp" + app:lineHeight="22dp" + tools:text="@string/frame_limit_enable" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/text_setting_description" + style="@style/TextAppearance.Material3.BodySmall" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_small" + android:textAlignment="viewStart" + tools:text="@string/frame_limit_enable_description" /> + + </LinearLayout> + +</RelativeLayout> diff --git a/src/android/app/src/main/res/layout/list_item_settings_header.xml b/src/android/app/src/main/res/layout/list_item_settings_header.xml index aabd4cfc1a..e072d32546 100644 --- a/src/android/app/src/main/res/layout/list_item_settings_header.xml +++ b/src/android/app/src/main/res/layout/list_item_settings_header.xml @@ -1,19 +1,16 @@ <?xml version="1.0" encoding="utf-8"?> -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" +<com.google.android.material.textview.MaterialTextView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/text_header_name" + style="@style/TextAppearance.Material3.TitleSmall" android:layout_width="match_parent" - android:layout_height="48dp"> - - <TextView - android:id="@+id/text_header_name" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center_vertical" - android:layout_marginStart="@dimen/spacing_large" - android:layout_marginBottom="@dimen/spacing_small" - android:layout_marginTop="@dimen/spacing_small" - android:textColor="?attr/colorPrimary" - android:textStyle="bold" - tools:text="CPU Settings" /> - -</FrameLayout> + android:layout_height="wrap_content" + android:layout_gravity="start|center_vertical" + android:paddingHorizontal="@dimen/spacing_large" + android:paddingVertical="16dp" + android:focusable="false" + android:clickable="false" + android:textAlignment="viewStart" + android:textColor="?attr/colorPrimary" + android:textStyle="bold" + tools:text="CPU Settings" /> diff --git a/src/android/app/src/main/res/layout/sysclock_datetime_picker.xml b/src/android/app/src/main/res/layout/sysclock_datetime_picker.xml deleted file mode 100644 index d082f52833..0000000000 --- a/src/android/app/src/main/res/layout/sysclock_datetime_picker.xml +++ /dev/null @@ -1,22 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:orientation="vertical" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:padding="8dp" - android:gravity="center"> - - <DatePicker - android:id="@+id/date_picker" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:calendarViewShown="false" - android:datePickerMode="spinner" - android:spinnersShown="true" /> - - <TimePicker - android:id="@+id/time_picker" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:timePickerMode="spinner" /> -</LinearLayout> diff --git a/src/android/app/src/main/res/values-night-v31/themes.xml b/src/android/app/src/main/res/values-night-v31/themes.xml new file mode 100644 index 0000000000..c0b82149b1 --- /dev/null +++ b/src/android/app/src/main/res/values-night-v31/themes.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="Theme.Citra.Main.MaterialYou" parent="Theme.Citra.Main"> + <item name="colorPrimary">@color/m3_sys_color_dynamic_dark_primary</item> + <item name="colorOnPrimary">@color/m3_sys_color_dynamic_dark_on_primary</item> + <item name="colorPrimaryContainer">@color/m3_sys_color_dynamic_dark_primary_container</item> + <item name="colorOnPrimaryContainer">@color/m3_sys_color_dynamic_dark_on_primary_container</item> + <item name="colorSecondary">@color/m3_sys_color_dynamic_dark_secondary</item> + <item name="colorOnSecondary">@color/m3_sys_color_dynamic_dark_on_secondary</item> + <item name="colorSecondaryContainer">@color/m3_sys_color_dynamic_dark_secondary_container</item> + <item name="colorOnSecondaryContainer">@color/m3_sys_color_dynamic_dark_on_secondary_container</item> + <item name="colorTertiary">@color/m3_sys_color_dynamic_dark_tertiary</item> + <item name="colorOnTertiary">@color/m3_sys_color_dynamic_dark_on_tertiary</item> + <item name="colorTertiaryContainer">@color/m3_sys_color_dynamic_dark_tertiary_container</item> + <item name="colorOnTertiaryContainer">@color/m3_sys_color_dynamic_dark_on_tertiary_container</item> + <item name="android:colorBackground">@color/m3_sys_color_dynamic_dark_background</item> + <item name="colorOnBackground">@color/m3_sys_color_dynamic_dark_on_background</item> + <item name="colorSurface">@color/m3_sys_color_dynamic_dark_surface</item> + <item name="colorOnSurface">@color/m3_sys_color_dynamic_dark_on_surface</item> + <item name="colorSurfaceVariant">@color/m3_sys_color_dynamic_dark_surface_variant</item> + <item name="colorOnSurfaceVariant">@color/m3_sys_color_dynamic_dark_on_surface_variant</item> + <item name="colorOutline">@color/m3_sys_color_dynamic_dark_outline</item> + <item name="colorOnSurfaceInverse">@color/m3_sys_color_dynamic_dark_on_surface_variant</item> + <item name="colorSurfaceInverse">@color/m3_sys_color_dynamic_dark_surface_variant</item> + <item name="colorPrimaryInverse">@color/m3_sys_color_dynamic_dark_inverse_primary</item> + </style> + +</resources> diff --git a/src/android/app/src/main/res/values-night/themes.xml b/src/android/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000000..8670410d46 --- /dev/null +++ b/src/android/app/src/main/res/values-night/themes.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="ThemeOverlay.Citra.Dark" parent=""> + <item name="colorSurface">@android:color/black</item> + <item name="android:colorBackground">@android:color/black</item> + </style> + +</resources> diff --git a/src/android/app/src/main/res/values-v31/themes.xml b/src/android/app/src/main/res/values-v31/themes.xml new file mode 100644 index 0000000000..54ce7c8eca --- /dev/null +++ b/src/android/app/src/main/res/values-v31/themes.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="Theme.Citra.Main.MaterialYou" parent="Theme.Citra.Main"> + <item name="colorPrimary">@color/m3_sys_color_dynamic_light_primary</item> + <item name="colorOnPrimary">@color/m3_sys_color_dynamic_light_on_primary</item> + <item name="colorPrimaryContainer">@color/m3_sys_color_dynamic_light_primary_container</item> + <item name="colorOnPrimaryContainer">@color/m3_sys_color_dynamic_light_on_primary_container</item> + <item name="colorSecondary">@color/m3_sys_color_dynamic_light_secondary</item> + <item name="colorOnSecondary">@color/m3_sys_color_dynamic_light_on_secondary</item> + <item name="colorSecondaryContainer">@color/m3_sys_color_dynamic_light_secondary_container</item> + <item name="colorOnSecondaryContainer">@color/m3_sys_color_dynamic_light_on_secondary_container</item> + <item name="colorTertiary">@color/m3_sys_color_dynamic_light_tertiary</item> + <item name="colorOnTertiary">@color/m3_sys_color_dynamic_light_on_tertiary</item> + <item name="colorTertiaryContainer">@color/m3_sys_color_dynamic_light_tertiary_container</item> + <item name="colorOnTertiaryContainer">@color/m3_sys_color_dynamic_light_on_tertiary_container</item> + <item name="android:colorBackground">@color/m3_sys_color_dynamic_light_background</item> + <item name="colorOnBackground">@color/m3_sys_color_dynamic_light_on_background</item> + <item name="colorSurface">@color/m3_sys_color_dynamic_light_surface</item> + <item name="colorOnSurface">@color/m3_sys_color_dynamic_light_on_surface</item> + <item name="colorSurfaceVariant">@color/m3_sys_color_dynamic_light_surface_variant</item> + <item name="colorOnSurfaceVariant">@color/m3_sys_color_dynamic_light_on_surface_variant</item> + <item name="colorOutline">@color/m3_sys_color_dynamic_light_outline</item> + <item name="colorOnSurfaceInverse">@color/m3_sys_color_dynamic_light_on_surface_variant</item> + <item name="colorSurfaceInverse">@color/m3_sys_color_dynamic_light_surface_variant</item> + <item name="colorPrimaryInverse">@color/m3_sys_color_dynamic_light_inverse_primary</item> + </style> + +</resources> diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index c7cdadd195..46c3e22821 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -1,10 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> - -<!-- All lists for ListPreference keys/values are placed here --> <resources> + <string-array name="systemClockNames" translatable="true"> - <item>Device clock</item> - <item>Simulated clock</item> + <item>@string/device_clock</item> + <item>@string/simulated_clock</item> </string-array> <integer-array name="systemClockValues" translatable="false"> @@ -12,39 +11,15 @@ <item>1</item> </integer-array> - <string-array name="designNames" translatable="true"> - <item>Light</item> - <item>Dark</item> - <item>System default</item> - </string-array> - - <integer-array name="designValues" translatable="false"> - <item>0</item> - <item>1</item> - <item>2</item> - </integer-array> - - <!-- Pre-Android 10 does not support System Default --> - <string-array name="designNamesOld" translatable="true"> - <item>Light</item> - <item>Dark</item> - </string-array> - - <!-- Pre-Android 10 does not support System Default --> - <integer-array name="designValuesOld" translatable="false"> - <item>0</item> - <item>1</item> - </integer-array> - <string-array name="regionNames"> - <item>Auto-select</item> - <item>Japan</item> - <item>USA</item> - <item>Europe</item> - <item>Australia</item> - <item>China</item> - <item>Korea</item> - <item>Taiwan</item> + <item>@string/auto_select</item> + <item>@string/system_region_jpn</item> + <item>@string/system_region_usa</item> + <item>@string/system_region_eur</item> + <item>@string/system_region_aus</item> + <item>@string/system_region_chn</item> + <item>@string/system_region_kor</item> + <item>@string/system_region_twn</item> </string-array> <integer-array name="regionValues"> @@ -59,18 +34,18 @@ </integer-array> <string-array name="languageNames"> - <item>Japanese (日本語)</item> - <item>English</item> - <item>French (français)</item> - <item>German (Deutsch)</item> - <item>Italian (italiano)</item> - <item>Spanish (español)</item> - <item>Simplified Chinese (简体中文)</item> - <item>Korean (한국어)</item> - <item>Dutch (Nederlands)</item> - <item>Portuguese (português)</item> - <item>Russian (Русский)</item> - <item>Traditional Chinese (正體中文)</item> + <item>@string/language_japanese</item> + <item>@string/language_english</item> + <item>@string/language_french</item> + <item>@string/language_german</item> + <item>@string/language_italian</item> + <item>@string/language_spanish</item> + <item>@string/language_simplified_chinese</item> + <item>@string/language_korean</item> + <item>@string/language_dutch</item> + <item>@string/language_portuguese</item> + <item>@string/language_russian</item> + <item>@string/language_traditional_chinese</item> </string-array> <integer-array name="languageValues"> @@ -89,25 +64,25 @@ </integer-array> <string-array name="n3dsButtons"> - <item>a</item> - <item>b</item> - <item>x</item> - <item>y</item> - <item>L</item> - <item>R</item> - <item>ZL</item> - <item>ZR</item> - <item>Start</item> - <item>Select</item> - <item>D-Pad</item> - <item>Circle Pad</item> - <item>C Stick</item> + <item>@string/button_a</item> + <item>@string/button_b</item> + <item>@string/button_x</item> + <item>@string/button_y</item> + <item>@string/button_l</item> + <item>@string/button_r</item> + <item>@string/button_zl</item> + <item>@string/button_zr</item> + <item>@string/button_start</item> + <item>@string/button_select</item> + <item>@string/controller_dpad</item> + <item>@string/controller_circlepad</item> + <item>@string/controller_c</item> </string-array> <string-array name="cameraImageSourceNames"> - <item>Blank</item> - <item>Still Image</item> - <item>Device Camera</item> + <item>@string/blank</item> + <item>@string/still_image</item> + <item>@string/device_camera</item> </string-array> <string-array name="cameraImageSourceValues"> @@ -117,9 +92,9 @@ </string-array> <string-array name="cameraDeviceNames"> - <item>Default</item> - <item>Any Front Camera</item> - <item>Any Back Camera</item> + <item>@string/option_default</item> + <item>@string/any_front_camera</item> + <item>@string/any_back_camera</item> </string-array> <string-array name="cameraDeviceValues"> @@ -129,10 +104,10 @@ </string-array> <string-array name="cameraFlipNames"> - <item>None</item> - <item>Horizontal</item> - <item>Vertical</item> - <item>Reverse</item> + <item>@string/none</item> + <item>@string/horizontal</item> + <item>@string/vertical</item> + <item>@string/reverse</item> </string-array> <integer-array name="cameraFlipValues"> @@ -143,11 +118,11 @@ </integer-array> <string-array name="audioInputTypeNames"> - <item>Auto</item> - <item>None</item> - <item>Static Noise</item> - <item>Real Device (Cubeb)</item> - <item>Real Device (OpenAL)</item> + <item>@string/auto</item> + <item>@string/none</item> + <item>@string/static_noise</item> + <item>@string/real_cubeb</item> + <item>@string/real_openal</item> </string-array> <integer-array name="audioInputTypeValues"> @@ -159,12 +134,12 @@ </integer-array> <string-array name="render3dModes"> - <item>Off</item> - <item>Side by Side</item> - <item>Anaglyph</item> - <item>Interlaced</item> - <item>Reverse Interlaced</item> - <item>Cardboard VR</item> + <item>@string/off</item> + <item>@string/side_by_side</item> + <item>@string/anaglyph</item> + <item>@string/interlaced</item> + <item>@string/reverse_interlaced</item> + <item>@string/cardboard_vr</item> </string-array> <integer-array name="render3dValues"> @@ -177,8 +152,8 @@ </integer-array> <string-array name="graphicsApiNames"> - <item>OpenGL ES</item> - <item>Vulkan</item> + <item>@string/opengles</item> + <item>@string/vulkan</item> </string-array> <integer-array name="graphicsApiValues"> @@ -187,12 +162,12 @@ </integer-array> <string-array name="textureFilterNames"> - <item>None</item> - <item>Anime4K</item> - <item>Bicubic</item> - <item>Nearest Neighbor</item> - <item>ScaleForce</item> - <item>xBRZ</item> + <item>@string/none</item> + <item>@string/anime4k</item> + <item>@string/bicubic</item> + <item>@string/scaleforce</item> + <item>@string/xbrz</item> + <item>@string/mmpx</item> </string-array> <integer-array name="textureFilterValues"> @@ -204,6 +179,17 @@ <item>5</item> </integer-array> + <string-array name="themeModeEntries"> + <item>@string/theme_mode_follow_system</item> + <item>@string/theme_mode_light</item> + <item>@string/theme_mode_dark</item> + </string-array> + <integer-array name="themeModeValues"> + <item>-1</item> + <item>1</item> + <item>2</item> + </integer-array> + <string-array name="systemFileRegions"> <item>@string/system_region_jpn</item> <item>@string/system_region_usa</item> @@ -233,4 +219,235 @@ <item>2</item> <item>4</item> </integer-array> + + <string-array name="soundOutputModes"> + <item>@string/mono</item> + <item>@string/stereo</item> + <item>@string/surround</item> + </string-array> + <integer-array name="soundOutputModeValues"> + <item>0</item> + <item>1</item> + <item>2</item> + </integer-array> + + <string-array name="countries"> + <item></item> + <item>@string/japan</item> + <item></item> + <item></item> + <item></item> + <item></item> + <item></item> + <item></item> + <item>@string/anguilla</item> + <item>@string/antigua_and_barbuda</item> <!-- 0-9 --> + <item>@string/argentina</item> + <item>@string/aruba</item> + <item>@string/bahamas</item> + <item>@string/barbados</item> + <item>@string/belize</item> + <item>@string/bolivia</item> + <item>@string/brazil</item> + <item>@string/british_virgin_islands</item> + <item>@string/canada</item> + <item>@string/cayman_islands</item> <!-- 10-19 --> + <item>@string/chile</item> + <item>@string/colombia</item> + <item>@string/costa_rica</item> + <item>@string/dominica</item> + <item>@string/dominican_republic</item> + <item>@string/ecuador</item> + <item>@string/el_salvador</item> + <item>@string/french_guiana</item> + <item>@string/grenada</item> + <item>@string/guadeloupe</item> <!-- 20-29 --> + <item>@string/guatemala</item> + <item>@string/guyana</item> + <item>@string/haiti</item> + <item>@string/honduras</item> + <item>@string/jamaica</item> + <item>@string/matinique</item> + <item>@string/mexico</item> + <item>@string/monsterrat</item> + <item>@string/netherlands_antilles</item> + <item>@string/nicaragua</item> <!-- 30-39 --> + <item>@string/panama</item> + <item>@string/paraguay</item> + <item>@string/peru</item> + <item>@string/saint_kittis_and_nevis</item> + <item>@string/saint_lucia</item> + <item>@string/saint_vincent_and_the_grenadines</item> + <item>@string/suriname</item> + <item>@string/trinidad_and_tobago</item> + <item>@string/turks_and_caicos_islands</item> + <item>@string/united_states</item> <!-- 40-49 --> + <item>@string/uruguay</item> + <item>@string/us_virgin_islands</item> + <item>@string/venezuela</item> + <item></item> + <item></item> + <item></item> + <item></item> + <item></item> + <item></item> + <item></item> <!-- 50-59 --> + <item></item> + <item></item> + <item></item> + <item></item> + <item>@string/albania</item> + <item>@string/australia</item> + <item>@string/austria</item> + <item>@string/belgium</item> + <item>@string/bosnia_and_herzegovnia</item> + <item>@string/botswana</item> <!-- 60-69 --> + <item>@string/bulgaria</item> + <item>@string/croatia</item> + <item>@string/cyprus</item> + <item>@string/czech_republic</item> + <item>@string/denmark</item> + <item>@string/estonia</item> + <item>@string/finland</item> + <item>@string/france</item> + <item>@string/germany</item> + <item>@string/greece</item> <!-- 70-79 --> + <item>@string/hungary</item> + <item>@string/iceland</item> + <item>@string/ireland</item> + <item>@string/italy</item> + <item>@string/latvia</item> + <item>@string/lesotho</item> + <item>@string/liechtenstein</item> + <item>@string/lithuania</item> + <item>@string/luxembourg</item> + <item>@string/macedonia</item> <!-- 80-89 --> + <item>@string/malta</item> + <item>@string/montenegro</item> + <item>@string/mozambique</item> + <item>@string/namibia</item> + <item>@string/netherlands</item> + <item>@string/new_zealand</item> + <item>@string/norway</item> + <item>@string/poland</item> + <item>@string/portugal</item> + <item>@string/romania</item> <!-- 90-99 --> + <item>@string/russia</item> + <item>@string/serbia</item> + <item>@string/slovakia</item> + <item>@string/slovenia</item> + <item>@string/south_africa</item> + <item>@string/spain</item> + <item>@string/swaziland</item> + <item>@string/sweden</item> + <item>@string/switzerland</item> + <item>@string/turkey</item> <!-- 100-109 --> + <item>@string/united_kingdom</item> + <item>@string/zambia</item> + <item>@string/zimbabwe</item> + <item>@string/azerbaijan</item> + <item>@string/mauritania</item> + <item>@string/mali</item> + <item>@string/niger</item> + <item>@string/chad</item> + <item>@string/sudan</item> + <item>@string/eritrea</item> <!-- 110-119 --> + <item>@string/djibouti</item> + <item>@string/somalia</item> + <item>@string/andorra</item> + <item>@string/gibraltar</item> + <item>@string/guernsey</item> + <item>@string/isle_of_man</item> + <item>@string/jersey</item> + <item>@string/monaco</item> + <item>@string/taiwan</item> + <item></item> <!-- 120-129 --> + <item></item> + <item></item> + <item></item> + <item></item> + <item></item> + <item></item> + <item>@string/south_korea</item> + <item></item> + <item></item> + <item></item> <!-- 130-139 --> + <item></item> + <item></item> + <item></item> + <item></item> + <item>@string/hong_kong</item> + <item>@string/macau</item> + <item></item> + <item></item> + <item></item> + <item></item> <!-- 140-149 --> + <item></item> + <item></item> + <item>@string/indonesia</item> + <item>@string/singapore</item> + <item>@string/thailand</item> + <item>@string/philippines</item> + <item>@string/malaysia</item> + <item></item> + <item></item> + <item></item> <!-- 150-159 --> + <item>@string/china</item> + <item></item> + <item></item> + <item></item> + <item></item> + <item></item> + <item></item> + <item></item> + <item>@string/united_arab_emirates</item> + <item>@string/india</item> <!-- 160-169 --> + <item>@string/egypt</item> + <item>@string/oman</item> + <item>@string/qatar</item> + <item>@string/kuwait</item> + <item>@string/saudi_arabia</item> + <item>@string/syria</item> + <item>@string/bahrain</item> + <item>@string/jordan</item> + <item></item> + <item></item> <!-- 170-179 --> + <item></item> + <item></item> + <item></item> + <item></item> + <item>@string/san_marino</item> + <item>@string/vatican_city</item> + <item>@string/bermuda</item> <!-- 180-189 --> + </string-array> + + <string-array name="months"> + <item>@string/january</item> + <item>@string/february</item> + <item>@string/march</item> + <item>@string/april</item> + <item>@string/may</item> + <item>@string/june</item> + <item>@string/july</item> + <item>@string/august</item> + <item>@string/september</item> + <item>@string/october</item> + <item>@string/november</item> + <item>@string/december</item> + </string-array> + <integer-array name="monthValues"> + <item>1</item> + <item>2</item> + <item>3</item> + <item>4</item> + <item>5</item> + <item>6</item> + <item>7</item> + <item>8</item> + <item>9</item> + <item>10</item> + <item>11</item> + <item>12</item> + </integer-array> + </resources> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 9c646b0357..59558a055e 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -105,9 +105,12 @@ <string name="controller_circlepad">Circle Pad</string> <string name="controller_c">C-Stick</string> <string name="controller_triggers">Triggers</string> + <string name="controller_trigger">Trigger</string> <string name="controller_dpad">D-Pad</string> <string name="controller_axis_vertical">Up/Down Axis</string> <string name="controller_axis_horizontal">Left/Right Axis</string> + <string name="input_dialog_title">Bind %1$s %2$s</string> + <string name="input_dialog_description">Press or move an input.</string> <string name="input_binding">Input Binding</string> <string name="input_binding_description">Press or move an input to bind it to %1$s.</string> <string name="input_binding_description_vertical_axis">Move your joystick up or down.</string> @@ -116,6 +119,7 @@ <string name="button_b" translatable="false">B</string> <string name="button_select" translatable="false">SELECT</string> <string name="button_start" translatable="false">START</string> + <string name="button_home">HOME</string> <string name="button_x" translatable="false">X</string> <string name="button_y" translatable="false">Y</string> <string name="button_l" translatable="false">L</string> @@ -137,13 +141,6 @@ <string name="how_to_get_keys"><![CDATA[<a href="https://citra-emu.org/wiki/aes-keys/">How to get keys?</a>]]></string> <string name="show_home_apps">Show HOME menu apps in games list</string> <string name="run_system_setup">Run System Setup when the HOME Menu is launched</string> - <string name="system_region_jpn">JPN</string> - <string name="system_region_usa">USA</string> - <string name="system_region_eur">EUR</string> - <string name="system_region_aus">AUS</string> - <string name="system_region_chn">CHN</string> - <string name="system_region_kor">KOR</string> - <string name="system_region_twn">TWN</string> <string name="system_type_minimal">Minimal</string> <string name="system_type_old_3ds">Old 3DS</string> <string name="system_type_new_3ds">New 3DS</string> @@ -161,19 +158,31 @@ <!-- Generic buttons (Shared with lots of stuff) --> <string name="generic_buttons">Buttons</string> + <string name="button">Button</string> <!-- Core settings strings --> <string name="cpu_jit">CPU JIT</string> <string name="cpu_jit_description">Uses the Just-in-Time (JIT) compiler for CPU emulation. When enabled, game performance will be significantly improved.</string> <string name="init_clock">Clock</string> <string name="init_clock_description">Set the emulated 3DS clock to either reflect that of your device or start at a simulated date and time.</string> + <string name="cpu_clock_speed">CPU Clock Speed</string> <!-- System settings strings --> + <string name="username">Username</string> + <string name="new_3ds">New 3DS Mode</string> <string name="clock">Clock</string> <string name="init_time">Offset Time</string> <string name="init_time_description">If the clock is set to \"Simulated clock\", this changes the fixed date and time to start at.</string> <string name="emulated_region">Region</string> <string name="emulated_language">Language</string> + <string name="birthday">Birthday</string> + <string name="birthday_month">Month</string> + <string name="birthday_day">Day</string> + <string name="country">Country</string> + <string name="play_coins">Play Coins</string> + <string name="console_id">Console ID</string> + <string name="regenerate_console_id">Regenerate Console ID</string> + <string name="regenerate_console_id_description">This will replace your current virtual 3DS with a new one. Your current virtual 3DS will not be recoverable. This might have unexpected effects in games. This might fail if you use an outdated config savegame. Continue?</string> <string name="plugin_loader">3GX Plugin Loader</string> <string name="plugin_loader_description">Loads 3GX plugins from the emulated SD card if they are available.</string> <string name="allow_plugin_loader">Allow Games to Change Plugin Loader State</string> @@ -246,9 +255,11 @@ <string name="async_custom_loading_description">Load custom textures asynchronously with background threads to reduce loading stutter.</string> <!-- Audio settings strings --> + <string name="audio_volume">Volume</string> <string name="audio_stretch">Audio Stretching</string> <string name="audio_stretch_description">Stretches audio to reduce stuttering. When enabled, increases audio latency and slightly reduces performance.</string> <string name="audio_input_type">Audio Input Device</string> + <string name="sound_output_mode">Sound Output Mode</string> <!-- Miscellaneous --> <string name="clear">Clear</string> @@ -261,7 +272,7 @@ <string name="back">Back</string> <string name="learn_more">Learn More</string> <string name="close">Close</string> - <string name="reset_to_default">Reset to default</string> + <string name="reset_to_default">Reset to Default</string> <string name="redump_games"><![CDATA[Please follow the guides to redump your <a href="https://citra-emu.org/wiki/dumping-game-cartridges/">game cartidges</a> or <a href="https://citra-emu.org/wiki/dumping-installed-titles/">installed titles</a>.]]></string> <string name="option_default">Default</string> <string name="none">None</string> @@ -269,6 +280,15 @@ <string name="off">Off</string> <string name="install">Install</string> <string name="delete">Delete</string> + <string name="reset_all_settings">Reset All Settings?</string> + <string name="reset_all_settings_description">All advanced settings will be reset to their default configuration. This can not be undone.</string> + <string name="settings_reset">Settings reset</string> + <string name="select_rtc_date">Select RTC date</string> + <string name="select_rtc_time">Select RTC time</string> + <string name="reset_setting_confirmation">Do you want to reset this setting back to its default value?</string> + <string name="setting_not_editable">You can\'t edit this now</string> + <string name="setting_not_editable_description">This option can\'t be changed while a game is running.</string> + <string name="auto_select">Auto-Select</string> <!-- Add Directory Screen--> <string name="select_game_folder">Select Game Folder</string> @@ -286,6 +306,7 @@ <string name="preferences_graphics">Graphics</string> <string name="preferences_audio">Audio</string> <string name="preferences_debug">Debug</string> + <string name="preferences_theme">Theme and Color</string> <!-- ROM loading errors --> <string name="loader_error_encrypted">Your ROM is Encrypted</string> @@ -408,4 +429,235 @@ <string name="cia_install_error_invalid">\"%s\" is not a valid CIA</string> <string name="cia_install_error_encrypted">\"%s\" must be decrypted before being used with Citra.\n A real 3DS is required</string> <string name="cia_install_error_unknown">An unknown error occurred while installing \"%s\".\n Please see the log for more details</string> + + <!-- Theme Modes --> + <string name="change_theme_mode">Change Theme Mode</string> + <string name="theme_mode_follow_system">Follow System</string> + <string name="theme_mode_light">Light</string> + <string name="theme_mode_dark">Dark</string> + <string name="material_you">Material You</string> + <string name="material_you_description">Use system colors across the app</string> + + <!-- Black backgrounds theme --> + <string name="use_black_backgrounds">Black Backgrounds</string> + <string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string> + + <!-- Clock types --> + <string name="device_clock">Device Clock</string> + <string name="simulated_clock">Simulated Clock</string> + + <!-- Region names --> + <string name="system_region_jpn">JPN</string> + <string name="system_region_usa">USA</string> + <string name="system_region_eur">EUR</string> + <string name="system_region_aus">AUS</string> + <string name="system_region_chn">CHN</string> + <string name="system_region_kor">KOR</string> + <string name="system_region_twn">TWN</string> + + <!-- Language names --> + <string name="language_japanese">Japanese (日本語)</string> + <string name="language_english">English</string> + <string name="language_french">French (Français)</string> + <string name="language_german">German (Deutsch)</string> + <string name="language_italian">Italian (Italiano)</string> + <string name="language_spanish">Spanish (Español)</string> + <string name="language_simplified_chinese">Simplified Chinese (简体中文)</string> + <string name="language_korean">Korean (한국어)</string> + <string name="language_dutch">Dutch (Nederlands)</string> + <string name="language_portuguese">Portuguese (Português)</string> + <string name="language_russian">Russian (Русский)</string> + <string name="language_traditional_chinese">Traditional Chinese (正體中文)</string> + + <!-- Camera image sources --> + <string name="blank">Blank</string> + <string name="still_image">Still Image</string> + <string name="device_camera">Device Camera</string> + + <!-- Camera device names --> + <string name="any_front_camera">Any Front Camera</string> + <string name="any_back_camera">Any Back Camera</string> + + <!-- Camera flip names --> + <string name="horizontal">Horizontal</string> + <string name="vertical">Vertical</string> + <string name="reverse">Reverse</string> + + <!-- Audio input types --> + <string name="static_noise">Static Noise</string> + <string name="real_cubeb">Real Device (cubeb)</string> + <string name="real_openal">Real Device (OpenAL)</string> + + <!-- Render 3D modes --> + <string name="side_by_side">Side by Side</string> + <string name="anaglyph">Anaglyph</string> + <string name="interlaced">Interlaced</string> + <string name="reverse_interlaced">Reverse Interlaced</string> + + <!-- Graphics API names --> + <string name="opengles">OpenGLES</string> + <string name="vulkan">Vulkan</string> + + <!-- Texture filter names --> + <string name="anime4k">Anime4K</string> + <string name="bicubic">Bicubic</string> + <string name="nearest_neighbor">Nearest Neighbor</string> + <string name="scaleforce">ScaleForce</string> + <string name="xbrz">xBRZ</string> + <string name="mmpx">MMPX</string> + + <!-- Sound output modes --> + <string name="mono">Mono</string> + <string name="stereo">Stereo</string> + <string name="surround">Surround</string> + + <!-- Countries --> + <string name="japan">Japan</string> + <string name="anguilla">Anguilla</string> + <string name="antigua_and_barbuda">Antigua and Barbuda</string> + <string name="argentina">Argentina</string> + <string name="aruba">Aruba</string> + <string name="bahamas">Bahamas</string> + <string name="barbados">Barbados</string> + <string name="belize">Belize</string> + <string name="bolivia">Bolivia</string> + <string name="brazil">Brazil</string> + <string name="british_virgin_islands">British Virgin Islands</string> + <string name="canada">Canada</string> + <string name="cayman_islands">Cayman Islands</string> + <string name="chile">Chile</string> + <string name="colombia">Colombia</string> + <string name="costa_rica">Costa Rica</string> + <string name="dominica">Dominica</string> + <string name="dominican_republic">Dominican Republic</string> + <string name="ecuador">Ecuador</string> + <string name="el_salvador">El Salvador</string> + <string name="french_guiana">French Guiana</string> + <string name="grenada">Grenada</string> + <string name="guadeloupe">Guadeloupe</string> + <string name="guatemala">Guatemala</string> + <string name="guyana">Guyana</string> + <string name="haiti">Haiti</string> + <string name="honduras">Honduras</string> + <string name="jamaica">Jamaica</string> + <string name="matinique">Martinique</string> + <string name="mexico">Mexico</string> + <string name="monsterrat">Montserrat</string> + <string name="netherlands_antilles">Netherlands Antilles</string> + <string name="nicaragua">Nicaragua</string> + <string name="panama">Panama</string> + <string name="paraguay">Paraguay</string> + <string name="peru">Peru</string> + <string name="saint_kittis_and_nevis">Saint Kitts and Nevis</string> + <string name="saint_lucia">Saint Lucia</string> + <string name="saint_vincent_and_the_grenadines">Saint Vincent and the Grenadines</string> + <string name="suriname">Suriname</string> + <string name="trinidad_and_tobago">Trinidad and Tobago</string> + <string name="turks_and_caicos_islands">Turks and Caicos Islands</string> + <string name="united_states">United States</string> + <string name="uruguay">Uruguay</string> + <string name="us_virgin_islands">US Virgin Islands</string> + <string name="venezuela">Venezuela</string> + <string name="albania">Albania</string> + <string name="australia">Australia</string> + <string name="austria">Austria</string> + <string name="belgium">Belgium</string> + <string name="bosnia_and_herzegovnia">Bosnia and Herzegovina</string> + <string name="botswana">Botswana</string> + <string name="bulgaria">Bulgaria</string> + <string name="croatia">Croatia</string> + <string name="cyprus">Cyprus</string> + <string name="czech_republic">Czech Republic</string> + <string name="denmark">Denmark</string> + <string name="estonia">Estonia</string> + <string name="finland">Finland</string> + <string name="france">France</string> + <string name="germany">Germany</string> + <string name="greece">Greece</string> + <string name="hungary">Hungary</string> + <string name="iceland">Iceland</string> + <string name="ireland">Ireland</string> + <string name="italy">Italy</string> + <string name="latvia">Latvia</string> + <string name="lesotho">Lesotho</string> + <string name="liechtenstein">Liechtenstein</string> + <string name="lithuania">Lithuania</string> + <string name="luxembourg">Luxembourg</string> + <string name="macedonia">Macedonia</string> + <string name="malta">Malta</string> + <string name="montenegro">Montenegro</string> + <string name="mozambique">Mozambique</string> + <string name="namibia">Namibia</string> + <string name="netherlands">Netherlands</string> + <string name="new_zealand">New Zealand</string> + <string name="norway">Norway</string> + <string name="poland">Poland</string> + <string name="portugal">Portugal</string> + <string name="romania">Romania</string> + <string name="russia">Russia</string> + <string name="serbia">Serbia</string> + <string name="slovakia">Slovakia</string> + <string name="slovenia">Slovenia</string> + <string name="south_africa">South Africa</string> + <string name="spain">Spain</string> + <string name="swaziland">Swaziland</string> + <string name="sweden">Sweden</string> + <string name="switzerland">Switzerland</string> + <string name="turkey">Turkey</string> + <string name="united_kingdom">United Kingdom</string> + <string name="zambia">Zambia</string> + <string name="zimbabwe">Zimbabwe</string> + <string name="azerbaijan">Azerbaijan</string> + <string name="mauritania">Mauritania</string> + <string name="mali">Mali</string> + <string name="niger">Niger</string> + <string name="chad">Chad</string> + <string name="sudan">Sudan</string> + <string name="eritrea">Eritrea</string> + <string name="djibouti">Djibouti</string> + <string name="somalia">Somalia</string> + <string name="andorra">Andorra</string> + <string name="gibraltar">Gibraltar</string> + <string name="guernsey">Guernsey</string> + <string name="isle_of_man">Isle of Man</string> + <string name="jersey">Jersey</string> + <string name="monaco">Monaco</string> + <string name="taiwan">Taiwan</string> + <string name="south_korea">South Korea</string> + <string name="hong_kong">Hong Kong</string> + <string name="macau">Macau</string> + <string name="indonesia">Indonesia</string> + <string name="singapore">Singapore</string> + <string name="thailand">Thailand</string> + <string name="philippines">Philippines</string> + <string name="malaysia">Malaysia</string> + <string name="china">China</string> + <string name="united_arab_emirates">United Arab Emirates</string> + <string name="india">India</string> + <string name="egypt">Egypt</string> + <string name="oman">Oman</string> + <string name="qatar">Qatar</string> + <string name="kuwait">Kuwait</string> + <string name="saudi_arabia">Saudi Arabia</string> + <string name="syria">Syria</string> + <string name="bahrain">Bahrain</string> + <string name="jordan">Jordan</string> + <string name="san_marino">San Marino</string> + <string name="vatican_city">Vatican City</string> + <string name="bermuda">Bermuda</string> + + <!-- Months --> + <string name="january">January</string> + <string name="february">February</string> + <string name="march">March</string> + <string name="april">April</string> + <string name="may">May</string> + <string name="june">June</string> + <string name="july">July</string> + <string name="august">August</string> + <string name="september">September</string> + <string name="october">October</string> + <string name="november">November</string> + <string name="december">December</string> + </resources>