From c17ec1d1aa867a6cb302d7f8c86f6929be6c5c40 Mon Sep 17 00:00:00 2001 From: Charles Lombardo 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 ebcfa1933..3a357ed31 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 6a6075782..2f631b61e 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 0f10f1858..000000000 --- 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 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 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 000000000..e60b1ca36 --- /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 000000000..c3b2c8e2e --- /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 000000000..7c3660854 --- /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 000000000..54af79efb --- /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 000000000..41ecc5038 --- /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 932dcf1d3..000000000 --- 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 000000000..28b026f91 --- /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 275f0ecea..000000000 --- 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 000000000..81c5dbbf8 --- /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() + + 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 f712e5bfa..000000000 --- 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 000000000..12d0b4ae4 --- /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 000000000..21629f7b0 --- /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() + + 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 b762847c9..000000000 --- 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 0a291aa6b..000000000 --- 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 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 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 000000000..02c5fa2d5 --- /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() + + /** + * 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 997dd1e26..000000000 --- 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> 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 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 { - @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 sections = new Settings.SettingsSectionMap(); - - public SettingSection getSection(String sectionName) { - return sections.get(sectionName); - } - - public boolean isEmpty() { - return sections.isEmpty(); - } - - public HashMap 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> 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 updatedSections) { - for (Map.Entry 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> entry : configFileSectionsMap.entrySet()) { - String fileName = entry.getKey(); - List sectionNames = entry.getValue(); - TreeMap 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 000000000..926f669cc --- /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, SettingSection> that constructs a new SettingSection instead of returning null + * when getting a key not already in the map + */ + class SettingsSectionMap : HashMap() { + 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 = 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) { + 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() + 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> = 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 000000000..3f9b4ad1f --- /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 b906b7010..000000000 --- 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 000000000..87b425f5b --- /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 000000000..865ebbdd0 --- /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 6bafecfe0..000000000 --- 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 afc3352cc..000000000 --- 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 000000000..6332e0e31 --- /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 bac8876cd..000000000 --- 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 000000000..e99b842f9 --- /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 6d4d954e8..000000000 --- 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 000000000..a6ae5e31c --- /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 000000000..8a237a14e --- /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 8a5642696..000000000 --- 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 000000000..5f6b4b70c --- /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 ee9d225d6..000000000 --- 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 000000000..75c6ec331 --- /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 551b13f99..000000000 --- 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 000000000..1227bf46d --- /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 000000000..ebbe1a675 --- /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 057145d9d..000000000 --- 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 000000000..c763e29f5 --- /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, + val values: Array?, + 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 9d44a923f..000000000 --- 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 000000000..08e1c6047 --- /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 000000000..a1d27849d --- /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 58ffbbfea..000000000 --- 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 000000000..796f577b1 --- /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, + 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 84a7d9d64..000000000 --- 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 000000000..ae3653109 --- /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 bd2f5f5aa..000000000 --- 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 000000000..126a010f0 --- /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 b03ec11b4..000000000 --- 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 implements DialogInterface.OnClickListener, Slider.OnChangeListener { - private SettingsFragmentView mView; - private Context mContext; - private ArrayList 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 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 000000000..6d9cb9798 --- /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(), DialogInterface.OnClickListener { + private var settings: ArrayList? = 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?) { + 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 = 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 76d4223f5..000000000 --- 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 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 000000000..77458cd54 --- /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) { + 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 3e53cf465..000000000 --- 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 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 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 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 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 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 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 supportedCameraNameList = new ArrayList<>(); - ArrayList 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 cameraDeviceNameList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceNames))); - cameraDeviceNameList.addAll(supportedCameraNameList); - ArrayList 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 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 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 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 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 000000000..de0cc1826 --- /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? = 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() + 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) { + 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) { + 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) { + 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) { + settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.camera)) + + // Get the camera IDs + val cameraManager = + settingsActivity.getSystemService(Context.CAMERA_SERVICE) as CameraManager? + val supportedCameraNameList = ArrayList() + val supportedCameraIdList = ArrayList() + 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) { + 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) { + 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) { + 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) { + 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) { + 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 c36eb55a7..000000000 --- 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 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 000000000..e1bb25230 --- /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) + + /** + * 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 d914f7d0b..000000000 --- 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 09ea93010..000000000 --- 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 000000000..6e06e7a01 --- /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 baf80ed76..000000000 --- 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 000000000..617348c89 --- /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 7d95c250a..000000000 --- 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 000000000..9d2dc15af --- /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 000000000..ac0c60a90 --- /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 2643ea121..000000000 --- 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 000000000..5b4d39cf4 --- /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 f735b7752..000000000 --- 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 000000000..e5e6d5df6 --- /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 ce503bc54..000000000 --- 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 000000000..e5817adc7 --- /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 000000000..b3140f39e --- /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 cb8c3e92a..000000000 --- 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 000000000..3098abbff --- /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 000000000..bd5ecc2eb --- /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 4590100cd..000000000 --- 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 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 readFile(final DocumentFile ini, boolean isCustomGame, SettingsActivityView view) { - HashMap 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 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 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 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 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 settings = section.getSettings(); - Set 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 000000000..83b5da972 --- /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() + + /** + * 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 { + val sections: HashMap = 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 { + return readFile(getSettingsFile(fileName), false, view) + } + + fun readFile(fileName: String): HashMap = 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 { + 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, + 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 = 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 = 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 05379d8d6..b19fdd7b9 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 000000000..cf42bed12 --- /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() + 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.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 000000000..d4e4dec65 --- /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 3a9f8167c..219f769fe 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 cb198f31e..6010851ea 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 000000000..87bca2ce2 --- /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 000000000..f451762d7 --- /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 ce3d24ceb..69758a322 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 bc0a6b94a..624826e0f 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 e0808b7f3..574b824a2 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 default_analogs InputManager::N3DS_STICK_C, }}; -void Config::UpdateCFG() { - std::shared_ptr cfg = std::make_shared(); - cfg->SetSystemLanguage(static_cast( - sdl2_config->GetInteger("System", "language", Service::CFG::SystemLanguage::LANGUAGE_EN))); - cfg->UpdateConfigNANDSavegame(); -} - template <> void Config::ReadSetting(const std::string& group, Settings::Setting& 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::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 162225728..c2b0abcea 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 dbd845a84..f67d29e9c 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 vulkan_library{}; std::unique_ptr window; -std::shared_ptr cfg; std::atomic stop_run{true}; std::atomic 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(); - } -} - -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 000000000..003ad1b63 --- /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 +#include +#include +#include +#include "android_common/android_common.h" + +std::shared_ptr 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(); + } +} + +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(get<0>(birthday)), + static_cast(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(jmonth), static_cast(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(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(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(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(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 000000000..43daec1ff --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_palette.xml @@ -0,0 +1,9 @@ + + + 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 c57abfd0a..14ae83b04 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 @@ + + android:layout_height="match_parent" + android:background="?attr/colorSurface"> + android:fitsSystemWindows="true" + app:elevation="0dp"> - + android:layout_height="?attr/collapsingToolbarLayoutMediumSize" + app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"> + + + + @@ -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" /> + + 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 000000000..3ea1e4d7b --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_input.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + +