Android UI Overhaul Part 2 (#7147)

This commit is contained in:
Charles Lombardo 2023-11-22 17:31:48 -05:00 committed by GitHub
parent 33a1f27a99
commit c17ec1d1aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
104 changed files with 5613 additions and 3752 deletions

View file

@ -515,14 +515,6 @@ object NativeLibrary {
*/ */
external fun logDeviceInfo() external fun logDeviceInfo()
external fun loadSystemConfig()
external fun saveSystemConfig()
external fun setSystemSetupNeeded(needed: Boolean)
external fun getIsSystemSetupNeeded(): Boolean
@Keep @Keep
@JvmStatic @JvmStatic
fun createFile(directory: String, filename: String): Boolean = fun createFile(directory: String, filename: String): Boolean =

View file

@ -535,7 +535,7 @@ public final class EmulationActivity extends AppCompatActivity {
@Override @Override
public boolean dispatchKeyEvent(KeyEvent event) { public boolean dispatchKeyEvent(KeyEvent event) {
int action; 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()) { switch (event.getAction()) {
case KeyEvent.ACTION_DOWN: case KeyEvent.ACTION_DOWN:
@ -693,8 +693,8 @@ public final class EmulationActivity extends AppCompatActivity {
int axis = range.getAxis(); int axis = range.getAxis();
float origValue = event.getAxisValue(axis); float origValue = event.getAxisValue(axis);
float value = mControllerMappingHelper.scaleAxis(input, axis, origValue); float value = mControllerMappingHelper.scaleAxis(input, axis, origValue);
int nextMapping = mPreferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1); int nextMapping = mPreferences.getInt(InputBindingSetting.Companion.getInputAxisButtonKey(axis), -1);
int guestOrientation = mPreferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1); int guestOrientation = mPreferences.getInt(InputBindingSetting.Companion.getInputAxisOrientationKey(axis), -1);
if (nextMapping == -1 || guestOrientation == -1) { if (nextMapping == -1 || guestOrientation == -1) {
// Axis is unmapped // Axis is unmapped

View file

@ -1,140 +0,0 @@
package org.citra.citra_emu.dialogs;
import android.content.Context;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
import org.citra.citra_emu.utils.Log;
import java.util.ArrayList;
import java.util.List;
/**
* {@link AlertDialog} derivative that listens for
* motion events from controllers and joysticks.
*/
public final class MotionAlertDialog extends AlertDialog {
// The selected input preference
private final InputBindingSetting setting;
private final ArrayList<Float> mPreviousValues = new ArrayList<>();
private int mPrevDeviceId = 0;
private boolean mWaitingForEvent = true;
/**
* Constructor
*
* @param context The current {@link Context}.
* @param setting The Preference to show this dialog for.
*/
public MotionAlertDialog(Context context, InputBindingSetting setting) {
super(context);
this.setting = setting;
}
public boolean onKeyEvent(int keyCode, KeyEvent event) {
Log.debug("[MotionAlertDialog] Received key event: " + event.getAction());
switch (event.getAction()) {
case KeyEvent.ACTION_UP:
setting.onKeyInput(event);
dismiss();
// Even if we ignore the key, we still consume it. Thus return true regardless.
return true;
default:
return false;
}
}
@Override
public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) {
return super.onKeyLongPress(keyCode, event);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// Handle this key if we care about it, otherwise pass it down the framework
return onKeyEvent(event.getKeyCode(), event) || super.dispatchKeyEvent(event);
}
@Override
public boolean dispatchGenericMotionEvent(@NonNull MotionEvent event) {
// Handle this event if we care about it, otherwise pass it down the framework
return onMotionEvent(event) || super.dispatchGenericMotionEvent(event);
}
private boolean onMotionEvent(MotionEvent event) {
if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)
return false;
if (event.getAction() != MotionEvent.ACTION_MOVE)
return false;
InputDevice input = event.getDevice();
List<InputDevice.MotionRange> motionRanges = input.getMotionRanges();
if (input.getId() != mPrevDeviceId) {
mPreviousValues.clear();
}
mPrevDeviceId = input.getId();
boolean firstEvent = mPreviousValues.isEmpty();
int numMovedAxis = 0;
float axisMoveValue = 0.0f;
InputDevice.MotionRange lastMovedRange = null;
char lastMovedDir = '?';
if (mWaitingForEvent) {
for (int i = 0; i < motionRanges.size(); i++) {
InputDevice.MotionRange range = motionRanges.get(i);
int axis = range.getAxis();
float origValue = event.getAxisValue(axis);
float value = origValue;//ControllerMappingHelper.scaleAxis(input, axis, origValue);
if (firstEvent) {
mPreviousValues.add(value);
} else {
float previousValue = mPreviousValues.get(i);
// Only handle the axes that are not neutral (more than 0.5)
// but ignore any axis that has a constant value (e.g. always 1)
if (Math.abs(value) > 0.5f && value != previousValue) {
// It is common to have multiple axes with the same physical input. For example,
// shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE.
// To handle this, we ignore an axis motion that's the exact same as a motion
// we already saw. This way, we ignore axes with two names, but catch the case
// where a joystick is moved in two directions.
// ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html
if (value != axisMoveValue) {
axisMoveValue = value;
numMovedAxis++;
lastMovedRange = range;
lastMovedDir = value < 0.0f ? '-' : '+';
}
}
// Special case for d-pads (axis value jumps between 0 and 1 without any values
// in between). Without this, the user would need to press the d-pad twice
// due to the first press being caught by the "if (firstEvent)" case further up.
else if (Math.abs(value) < 0.25f && Math.abs(previousValue) > 0.75f) {
numMovedAxis++;
lastMovedRange = range;
lastMovedDir = previousValue < 0.0f ? '-' : '+';
}
}
mPreviousValues.set(i, value);
}
// If only one axis moved, that's the winner.
if (numMovedAxis == 1) {
mWaitingForEvent = false;
setting.onMotionInput(input, lastMovedRange, lastMovedDir);
dismiss();
}
}
return true;
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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";
}
}

View file

@ -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 }
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,37 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.model
enum class FloatSetting(
override val key: String,
override val section: String,
override val defaultValue: Float
) : AbstractFloatSetting {
// There are no float settings currently
EMPTY_SETTING("", "", 0.0f);
override var float: Float = defaultValue
override val valueAsString: String
get() = float.toString()
override val isRuntimeEditable: Boolean
get() {
for (setting in NOT_RUNTIME_EDITABLE) {
if (setting == this) {
return false
}
}
return true
}
companion object {
private val NOT_RUNTIME_EDITABLE = emptyList<FloatSetting>()
fun from(key: String): FloatSetting? = FloatSetting.values().firstOrNull { it.key == key }
fun clear() = FloatSetting.values().forEach { it.float = it.defaultValue }
}
}

View file

@ -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);
}
}

View file

@ -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 }
}
}

View file

@ -0,0 +1,41 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.model
enum class ScaledFloatSetting(
override val key: String,
override val section: String,
override val defaultValue: Float,
val scale: Int
) : AbstractFloatSetting {
AUDIO_VOLUME("volume", Settings.SECTION_AUDIO, 1.0f, 100);
override var float: Float = defaultValue
get() = field * scale
set(value) {
field = value / scale
}
override val valueAsString: String get() = (float / scale).toString()
override val isRuntimeEditable: Boolean
get() {
for (setting in NOT_RUNTIME_EDITABLE) {
if (setting == this) {
return false
}
}
return true
}
companion object {
private val NOT_RUNTIME_EDITABLE = emptyList<ScaledFloatSetting>()
fun from(key: String): ScaledFloatSetting? =
ScaledFloatSetting.values().firstOrNull { it.key == key }
fun clear() = ScaledFloatSetting.values().forEach { it.float = it.defaultValue * it.scale }
}
}

View file

@ -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();
}

View file

@ -1,55 +0,0 @@
package org.citra.citra_emu.features.settings.model;
import java.util.HashMap;
/**
* A semantically-related group of Settings objects. These Settings are
* internally stored as a HashMap.
*/
public final class SettingSection {
private String mName;
private HashMap<String, Setting> mSettings = new HashMap<>();
/**
* Create a new SettingSection with no Settings in it.
*
* @param name The header of this section; e.g. [Core] or [Enhancements] without the brackets.
*/
public SettingSection(String name) {
mName = name;
}
public String getName() {
return mName;
}
/**
* Convenience method; inserts a value directly into the backing HashMap.
*
* @param setting The Setting to be inserted.
*/
public void putSetting(Setting setting) {
mSettings.put(setting.getKey(), setting);
}
/**
* Convenience method; gets a value directly from the backing HashMap.
*
* @param key Used to retrieve the Setting.
* @return A Setting object (you should probably cast this before using)
*/
public Setting getSetting(String key) {
return mSettings.get(key);
}
public HashMap<String, Setting> getSettings() {
return mSettings;
}
public void mergeSection(SettingSection settingSection) {
for (Setting setting : settingSection.mSettings.values()) {
putSetting(setting);
}
}
}

View file

@ -0,0 +1,38 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.model
/**
* A semantically-related group of Settings objects. These Settings are
* internally stored as a HashMap.
*/
class SettingSection(val name: String) {
val settings = HashMap<String, AbstractSetting>()
/**
* Convenience method; inserts a value directly into the backing HashMap.
*
* @param setting The Setting to be inserted.
*/
fun putSetting(setting: AbstractSetting) {
settings[setting.key!!] = setting
}
/**
* Convenience method; gets a value directly from the backing HashMap.
*
* @param key Used to retrieve the Setting.
* @return A Setting object (you should probably cast this before using)
*/
fun getSetting(key: String): AbstractSetting? {
return settings[key]
}
fun mergeSection(settingSection: SettingSection) {
for (setting in settingSection.settings.values) {
putSetting(setting)
}
}
}

View file

@ -1,131 +0,0 @@
package org.citra.citra_emu.features.settings.model;
import android.text.TextUtils;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.ui.SettingsActivityView;
import org.citra.citra_emu.features.settings.utils.SettingsFile;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class Settings {
public static final String PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch";
public static final String PREF_MATERIAL_YOU = "MaterialYouTheme";
public static final String PREF_THEME_MODE = "ThemeMode";
public static final String PREF_BLACK_BACKGROUNDS = "BlackBackgrounds";
public static final String PREF_SHOW_HOME_APPS = "ShowHomeApps";
public static final String SECTION_CORE = "Core";
public static final String SECTION_SYSTEM = "System";
public static final String SECTION_CAMERA = "Camera";
public static final String SECTION_CONTROLS = "Controls";
public static final String SECTION_RENDERER = "Renderer";
public static final String SECTION_LAYOUT = "Layout";
public static final String SECTION_UTILITY = "Utility";
public static final String SECTION_AUDIO = "Audio";
public static final String SECTION_DEBUG = "Debug";
private String gameId;
private static final Map<String, List<String>> configFileSectionsMap = new HashMap<>();
static {
configFileSectionsMap.put(SettingsFile.FILE_NAME_CONFIG, Arrays.asList(SECTION_CORE, SECTION_SYSTEM, SECTION_CAMERA, SECTION_CONTROLS, SECTION_RENDERER, SECTION_LAYOUT, SECTION_UTILITY, SECTION_AUDIO, SECTION_DEBUG));
}
/**
* A HashMap<String, SettingSection> that constructs a new SettingSection instead of returning null
* when getting a key not already in the map
*/
public static final class SettingsSectionMap extends HashMap<String, SettingSection> {
@Override
public SettingSection get(Object key) {
if (!(key instanceof String)) {
return null;
}
String stringKey = (String) key;
if (!super.containsKey(stringKey)) {
SettingSection section = new SettingSection(stringKey);
super.put(stringKey, section);
return section;
}
return super.get(key);
}
}
private HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap();
public SettingSection getSection(String sectionName) {
return sections.get(sectionName);
}
public boolean isEmpty() {
return sections.isEmpty();
}
public HashMap<String, SettingSection> getSections() {
return sections;
}
public void loadSettings(SettingsActivityView view) {
sections = new Settings.SettingsSectionMap();
loadCitraSettings(view);
if (!TextUtils.isEmpty(gameId)) {
loadCustomGameSettings(gameId, view);
}
}
private void loadCitraSettings(SettingsActivityView view) {
for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) {
String fileName = entry.getKey();
sections.putAll(SettingsFile.readFile(fileName, view));
}
}
private void loadCustomGameSettings(String gameId, SettingsActivityView view) {
// custom game settings
mergeSections(SettingsFile.readCustomGameSettings(gameId, view));
}
private void mergeSections(HashMap<String, SettingSection> updatedSections) {
for (Map.Entry<String, SettingSection> entry : updatedSections.entrySet()) {
if (sections.containsKey(entry.getKey())) {
SettingSection originalSection = sections.get(entry.getKey());
SettingSection updatedSection = entry.getValue();
originalSection.mergeSection(updatedSection);
} else {
sections.put(entry.getKey(), entry.getValue());
}
}
}
public void loadSettings(String gameId, SettingsActivityView view) {
this.gameId = gameId;
loadSettings(view);
}
public void saveSettings(SettingsActivityView view) {
if (TextUtils.isEmpty(gameId)) {
view.showToastMessage(CitraApplication.Companion.getAppContext().getString(R.string.ini_saved), false);
for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) {
String fileName = entry.getKey();
List<String> sectionNames = entry.getValue();
TreeMap<String, SettingSection> iniSections = new TreeMap<>();
for (String section : sectionNames) {
iniSections.put(section, sections.get(section));
}
SettingsFile.saveFile(fileName, iniSections, view);
}
}
}
}

View file

@ -0,0 +1,201 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.model
import android.text.TextUtils
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.R
import org.citra.citra_emu.features.settings.ui.SettingsActivityView
import org.citra.citra_emu.features.settings.utils.SettingsFile
import java.util.TreeMap
class Settings {
private var gameId: String? = null
var isLoaded = false
/**
* A HashMap<String></String>, SettingSection> that constructs a new SettingSection instead of returning null
* when getting a key not already in the map
*/
class SettingsSectionMap : HashMap<String, SettingSection?>() {
override operator fun get(key: String): SettingSection? {
if (!super.containsKey(key)) {
val section = SettingSection(key)
super.put(key, section)
return section
}
return super.get(key)
}
}
var sections: HashMap<String, SettingSection?> = SettingsSectionMap()
fun getSection(sectionName: String): SettingSection? {
return sections[sectionName]
}
val isEmpty: Boolean
get() = sections.isEmpty()
fun loadSettings(view: SettingsActivityView? = null) {
sections = SettingsSectionMap()
loadCitraSettings(view)
if (!TextUtils.isEmpty(gameId)) {
loadCustomGameSettings(gameId!!, view)
}
isLoaded = true
}
private fun loadCitraSettings(view: SettingsActivityView?) {
for ((fileName) in configFileSectionsMap) {
sections.putAll(SettingsFile.readFile(fileName, view))
}
}
private fun loadCustomGameSettings(gameId: String, view: SettingsActivityView?) {
// Custom game settings
mergeSections(SettingsFile.readCustomGameSettings(gameId, view))
}
private fun mergeSections(updatedSections: HashMap<String, SettingSection?>) {
for ((key, updatedSection) in updatedSections) {
if (sections.containsKey(key)) {
val originalSection = sections[key]
originalSection!!.mergeSection(updatedSection!!)
} else {
sections[key] = updatedSection
}
}
}
fun loadSettings(gameId: String, view: SettingsActivityView) {
this.gameId = gameId
loadSettings(view)
}
fun saveSettings(view: SettingsActivityView) {
if (TextUtils.isEmpty(gameId)) {
view.showToastMessage(
CitraApplication.appContext.getString(R.string.ini_saved),
false
)
for ((fileName, sectionNames) in configFileSectionsMap.entries) {
val iniSections = TreeMap<String, SettingSection?>()
for (section in sectionNames) {
iniSections[section] = sections[section]
}
SettingsFile.saveFile(fileName, iniSections, view)
}
} else {
// TODO: Implement per game settings
}
}
companion object {
const val SECTION_CORE = "Core"
const val SECTION_SYSTEM = "System"
const val SECTION_CAMERA = "Camera"
const val SECTION_CONTROLS = "Controls"
const val SECTION_RENDERER = "Renderer"
const val SECTION_LAYOUT = "Layout"
const val SECTION_UTILITY = "Utility"
const val SECTION_AUDIO = "Audio"
const val SECTION_DEBUG = "Debugging"
const val SECTION_THEME = "Theme"
const val KEY_BUTTON_A = "button_a"
const val KEY_BUTTON_B = "button_b"
const val KEY_BUTTON_X = "button_x"
const val KEY_BUTTON_Y = "button_y"
const val KEY_BUTTON_SELECT = "button_select"
const val KEY_BUTTON_START = "button_start"
const val KEY_BUTTON_HOME = "button_home"
const val KEY_BUTTON_UP = "button_up"
const val KEY_BUTTON_DOWN = "button_down"
const val KEY_BUTTON_LEFT = "button_left"
const val KEY_BUTTON_RIGHT = "button_right"
const val KEY_BUTTON_L = "button_l"
const val KEY_BUTTON_R = "button_r"
const val KEY_BUTTON_ZL = "button_zl"
const val KEY_BUTTON_ZR = "button_zr"
const val KEY_CIRCLEPAD_AXIS_VERTICAL = "circlepad_axis_vertical"
const val KEY_CIRCLEPAD_AXIS_HORIZONTAL = "circlepad_axis_horizontal"
const val KEY_CSTICK_AXIS_VERTICAL = "cstick_axis_vertical"
const val KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal"
const val KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical"
const val KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal"
val buttonKeys = listOf(
KEY_BUTTON_A,
KEY_BUTTON_B,
KEY_BUTTON_X,
KEY_BUTTON_Y,
KEY_BUTTON_SELECT,
KEY_BUTTON_START,
KEY_BUTTON_HOME
)
val buttonTitles = listOf(
R.string.button_a,
R.string.button_b,
R.string.button_x,
R.string.button_y,
R.string.button_select,
R.string.button_start,
R.string.button_home
)
val circlePadKeys = listOf(
KEY_CIRCLEPAD_AXIS_VERTICAL,
KEY_CIRCLEPAD_AXIS_HORIZONTAL
)
val cStickKeys = listOf(
KEY_CSTICK_AXIS_VERTICAL,
KEY_CSTICK_AXIS_HORIZONTAL
)
val dPadKeys = listOf(
KEY_DPAD_AXIS_VERTICAL,
KEY_DPAD_AXIS_HORIZONTAL
)
val axisTitles = listOf(
R.string.controller_axis_vertical,
R.string.controller_axis_horizontal
)
val triggerKeys = listOf(
KEY_BUTTON_L,
KEY_BUTTON_R,
KEY_BUTTON_ZL,
KEY_BUTTON_ZR
)
val triggerTitles = listOf(
R.string.button_l,
R.string.button_r,
R.string.button_zl,
R.string.button_zr
)
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
const val PREF_MATERIAL_YOU = "MaterialYouTheme"
const val PREF_THEME_MODE = "ThemeMode"
const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds"
const val PREF_SHOW_HOME_APPS = "ShowHomeApps"
private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap()
init {
configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] =
listOf(
SECTION_CORE,
SECTION_SYSTEM,
SECTION_CAMERA,
SECTION_CONTROLS,
SECTION_RENDERER,
SECTION_LAYOUT,
SECTION_UTILITY,
SECTION_AUDIO,
SECTION_DEBUG
)
}
}
}

View file

@ -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()
}

View file

@ -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;
}
}

View file

@ -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 }
}
}

View file

@ -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
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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
}
}

View file

@ -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;
}
}

View file

@ -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
}

View file

@ -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;
}
}

View file

@ -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"
}
}

View file

@ -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
}

View file

@ -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();
}

View file

@ -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
}
}

View file

@ -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;
}
}

View file

@ -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
}
}

View file

@ -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;
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,78 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.model.view
import org.citra.citra_emu.features.settings.model.AbstractSetting
import org.citra.citra_emu.features.settings.model.AbstractStringSetting
class StringSingleChoiceSetting(
setting: AbstractSetting?,
titleId: Int,
descriptionId: Int,
val choices: Array<String>,
val values: Array<String>?,
val key: String? = null,
private val defaultValue: String? = null
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_STRING_SINGLE_CHOICE
fun getValueAt(index: Int): String? {
if (values == null) return null
return if (index >= 0 && index < values.size) {
values[index]
} else {
""
}
}
val selectedValue: String
get() {
if (setting == null) {
return defaultValue!!
}
try {
val setting = setting as AbstractStringSetting
return setting.string
} catch (_: ClassCastException) {
}
try {
val setting = setting as AbstractShortSetting
return setting.short.toString()
} catch (_: ClassCastException) {
}
return defaultValue!!
}
val selectValueIndex: Int
get() {
val selectedValue = selectedValue
for (i in values!!.indices) {
if (values[i] == selectedValue) {
return i
}
}
return -1
}
/**
* Write a value to the backing int. If that int was previously null,
* initializes a new one and returns it, so it can be added to the Hashmap.
*
* @param selection New value of the int.
* @return the existing setting with the new value applied.
*/
fun setSelectedValue(selection: String): AbstractStringSetting {
val stringSetting = setting as AbstractStringSetting
stringSetting.string = selection
return stringSetting
}
fun setSelectedValue(selection: Short): AbstractShortSetting {
val shortSetting = setting as AbstractShortSetting
shortSetting.short = selection
return shortSetting
}
}

View file

@ -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;
}
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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;
});
}
}

View file

@ -0,0 +1,292 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.ui
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.preference.PreferenceManager
import com.google.android.material.color.MaterialColors
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R
import org.citra.citra_emu.databinding.ActivitySettingsBinding
import java.io.IOException
import org.citra.citra_emu.features.settings.model.BooleanSetting
import org.citra.citra_emu.features.settings.model.FloatSetting
import org.citra.citra_emu.features.settings.model.IntSetting
import org.citra.citra_emu.features.settings.model.ScaledFloatSetting
import org.citra.citra_emu.features.settings.model.Settings
import org.citra.citra_emu.features.settings.model.SettingsViewModel
import org.citra.citra_emu.features.settings.model.StringSetting
import org.citra.citra_emu.features.settings.utils.SettingsFile
import org.citra.citra_emu.utils.SystemSaveGame
import org.citra.citra_emu.utils.DirectoryInitialization
import org.citra.citra_emu.utils.InsetsHelper
import org.citra.citra_emu.utils.ThemeUtil
class SettingsActivity : AppCompatActivity(), SettingsActivityView {
private val presenter = SettingsActivityPresenter(this)
private lateinit var binding: ActivitySettingsBinding
private val settingsViewModel: SettingsViewModel by viewModels()
override val settings: Settings get() = settingsViewModel.settings
override fun onCreate(savedInstanceState: Bundle?) {
ThemeUtil.setTheme(this)
super.onCreate(savedInstanceState)
binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
WindowCompat.setDecorFitsSystemWindows(window, false)
val launcher = intent
val gameID = launcher.getStringExtra(ARG_GAME_ID)
val menuTag = launcher.getStringExtra(ARG_MENU_TAG)
presenter.onCreate(savedInstanceState, menuTag!!, gameID!!)
// Show "Back" button in the action bar for navigation
setSupportActionBar(binding.toolbarSettings)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
if (InsetsHelper.getSystemGestureType(applicationContext) !=
InsetsHelper.GESTURE_NAVIGATION
) {
binding.navigationBarShade.setBackgroundColor(
ThemeUtil.getColorWithOpacity(
MaterialColors.getColor(
binding.navigationBarShade,
com.google.android.material.R.attr.colorSurface
),
ThemeUtil.SYSTEM_BAR_ALPHA
)
)
}
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() = navigateBack()
}
)
setInsets()
}
override fun onSupportNavigateUp(): Boolean {
navigateBack()
return true
}
private fun navigateBack() {
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStack()
} else {
finish()
}
}
override fun onSaveInstanceState(outState: Bundle) {
// Critical: If super method is not called, rotations will be busted.
super.onSaveInstanceState(outState)
presenter.saveState(outState)
}
override fun onStart() {
super.onStart()
presenter.onStart()
}
/**
* If this is called, the user has left the settings screen (potentially through the
* home button) and will expect their changes to be persisted. So we kick off an
* IntentService which will do so on a background thread.
*/
override fun onStop() {
super.onStop()
presenter.onStop(isFinishing)
}
override fun showSettingsFragment(menuTag: String, addToStack: Boolean, gameId: String) {
if (!addToStack && settingsFragment != null) {
return
}
val transaction = supportFragmentManager.beginTransaction()
if (addToStack) {
if (areSystemAnimationsEnabled()) {
transaction.setCustomAnimations(
R.anim.anim_settings_fragment_in,
R.anim.anim_settings_fragment_out,
0,
R.anim.anim_pop_settings_fragment_out
)
}
transaction.addToBackStack(null)
}
transaction.replace(
R.id.frame_content,
SettingsFragment.newInstance(menuTag, gameId),
FRAGMENT_TAG
)
transaction.commit()
}
private fun areSystemAnimationsEnabled(): Boolean {
val duration = android.provider.Settings.Global.getFloat(
contentResolver,
android.provider.Settings.Global.ANIMATOR_DURATION_SCALE,
1f
)
val transition = android.provider.Settings.Global.getFloat(
contentResolver,
android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE,
1f
)
return duration != 0f && transition != 0f
}
override fun onSettingsFileLoaded() {
val fragment: SettingsFragmentView? = settingsFragment
fragment?.loadSettingsList()
}
override fun onSettingsFileNotFound() {
val fragment: SettingsFragmentView? = settingsFragment
fragment?.loadSettingsList()
}
override fun showToastMessage(message: String, isLong: Boolean) {
Toast.makeText(
this,
message,
if (isLong) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
).show()
}
override fun onSettingChanged() {
presenter.onSettingChanged()
}
fun onSettingsReset() {
// Prevents saving to a non-existent settings file
presenter.onSettingsReset()
val controllerKeys = Settings.buttonKeys + Settings.circlePadKeys + Settings.cStickKeys +
Settings.dPadKeys + Settings.triggerKeys
val editor =
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext).edit()
controllerKeys.forEach { editor.remove(it) }
editor.apply()
// Reset the static memory representation of each setting
BooleanSetting.clear()
FloatSetting.clear()
ScaledFloatSetting.clear()
IntSetting.clear()
StringSetting.clear()
// Delete settings file because the user may have changed values that do not exist in the UI
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
if (!settingsFile.delete()) {
throw IOException("Failed to delete $settingsFile")
}
// Set the root of the document tree before we create a new config file or the native code
// will fail when creating the file.
if (DirectoryInitialization.setCitraUserDirectory()) {
CitraApplication.documentsTree.setRoot(Uri.parse(DirectoryInitialization.userPath))
NativeLibrary.createConfigFile()
} else {
throw IllegalStateException("Citra directory unavailable when accessing config file!")
}
// Set default values for system config file
SystemSaveGame.apply {
setUsername("CITRA")
setBirthday(3, 25)
setSystemLanguage(1)
setSoundOutputMode(2)
setCountryCode(49)
setPlayCoins(42)
}
showToastMessage(getString(R.string.settings_reset), true)
finish()
}
fun setToolbarTitle(title: String) {
binding.toolbarSettingsLayout.title = title
}
private val settingsFragment: SettingsFragment?
get() = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as SettingsFragment?
private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener(
binding.frameContent
) { view: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
view.updatePadding(
left = barInsets.left + cutoutInsets.left,
right = barInsets.right + cutoutInsets.right
)
val mlpAppBar = binding.appbarSettings.layoutParams as MarginLayoutParams
mlpAppBar.leftMargin = barInsets.left + cutoutInsets.left
mlpAppBar.rightMargin = barInsets.right + cutoutInsets.right
binding.appbarSettings.layoutParams = mlpAppBar
val mlpShade = binding.navigationBarShade.layoutParams as MarginLayoutParams
mlpShade.height = barInsets.bottom
binding.navigationBarShade.layoutParams = mlpShade
windowInsets
}
}
companion object {
private const val ARG_MENU_TAG = "menu_tag"
private const val ARG_GAME_ID = "game_id"
private const val FRAGMENT_TAG = "settings"
@JvmStatic
fun launch(context: Context, menuTag: String?, gameId: String?) {
val settings = Intent(context, SettingsActivity::class.java)
settings.putExtra(ARG_MENU_TAG, menuTag)
settings.putExtra(ARG_GAME_ID, gameId)
context.startActivity(settings)
}
fun launch(
context: Context,
launcher: ActivityResultLauncher<Intent>,
menuTag: String?,
gameId: String?
) {
val settings = Intent(context, SettingsActivity::class.java)
settings.putExtra(ARG_MENU_TAG, menuTag)
settings.putExtra(ARG_GAME_ID, gameId)
launcher.launch(settings)
}
}
}

View file

@ -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);
}
}

View file

@ -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"
}
}

View file

@ -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();
}

View file

@ -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()
}

View file

@ -1,393 +0,0 @@
package org.citra.citra_emu.features.settings.ui;
import android.content.Context;
import android.content.DialogInterface;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.DatePicker;
import android.widget.TextView;
import android.widget.TimePicker;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.slider.Slider;
import org.citra.citra_emu.R;
import org.citra.citra_emu.dialogs.MotionAlertDialog;
import org.citra.citra_emu.features.settings.model.FloatSetting;
import org.citra.citra_emu.features.settings.model.IntSetting;
import org.citra.citra_emu.features.settings.model.StringSetting;
import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting;
import org.citra.citra_emu.features.settings.model.view.DateTimeSetting;
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
import org.citra.citra_emu.features.settings.model.view.SliderSetting;
import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting;
import org.citra.citra_emu.features.settings.model.view.SubmenuSetting;
import org.citra.citra_emu.features.settings.ui.viewholder.CheckBoxSettingViewHolder;
import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder;
import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder;
import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder;
import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder;
import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder;
import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder;
import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder;
import org.citra.citra_emu.utils.Log;
import java.util.ArrayList;
public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolder> implements DialogInterface.OnClickListener, Slider.OnChangeListener {
private SettingsFragmentView mView;
private Context mContext;
private ArrayList<SettingsItem> mSettings;
private SettingsItem mClickedItem;
private int mClickedPosition;
private int mSliderProgress;
private AlertDialog mDialog;
private TextView mTextSliderValue;
public SettingsAdapter(SettingsFragmentView view, Context context) {
mView = view;
mContext = context;
mClickedPosition = -1;
}
@Override
public SettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view;
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
switch (viewType) {
case SettingsItem.TYPE_HEADER:
view = inflater.inflate(R.layout.list_item_settings_header, parent, false);
return new HeaderViewHolder(view, this);
case SettingsItem.TYPE_CHECKBOX:
view = inflater.inflate(R.layout.list_item_setting_checkbox, parent, false);
return new CheckBoxSettingViewHolder(view, this);
case SettingsItem.TYPE_SINGLE_CHOICE:
case SettingsItem.TYPE_STRING_SINGLE_CHOICE:
view = inflater.inflate(R.layout.list_item_setting, parent, false);
return new SingleChoiceViewHolder(view, this);
case SettingsItem.TYPE_SLIDER:
view = inflater.inflate(R.layout.list_item_setting, parent, false);
return new SliderViewHolder(view, this);
case SettingsItem.TYPE_SUBMENU:
view = inflater.inflate(R.layout.list_item_setting, parent, false);
return new SubmenuViewHolder(view, this);
case SettingsItem.TYPE_INPUT_BINDING:
view = inflater.inflate(R.layout.list_item_setting, parent, false);
return new InputBindingSettingViewHolder(view, this, mContext);
case SettingsItem.TYPE_DATETIME_SETTING:
view = inflater.inflate(R.layout.list_item_setting, parent, false);
return new DateTimeViewHolder(view, this);
default:
Log.error("[SettingsAdapter] Invalid view type: " + viewType);
return null;
}
}
@Override
public void onBindViewHolder(SettingViewHolder holder, int position) {
holder.bind(getItem(position));
}
private SettingsItem getItem(int position) {
return mSettings.get(position);
}
@Override
public int getItemCount() {
if (mSettings != null) {
return mSettings.size();
} else {
return 0;
}
}
@Override
public int getItemViewType(int position) {
return getItem(position).getType();
}
public void setSettings(ArrayList<SettingsItem> settings) {
mSettings = settings;
notifyDataSetChanged();
}
public void onBooleanClick(CheckBoxSetting item, int position, boolean checked) {
IntSetting setting = item.setChecked(checked);
notifyItemChanged(position);
if (setting != null) {
mView.putSetting(setting);
}
mView.onSettingChanged();
}
public void onSingleChoiceClick(SingleChoiceSetting item) {
mClickedItem = item;
int value = getSelectionForSingleChoiceValue(item);
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
.setTitle(item.getNameId())
.setSingleChoiceItems(item.getChoicesId(), value, this);
mDialog = builder.show();
}
public void onSingleChoiceClick(SingleChoiceSetting item, int position) {
mClickedPosition = position;
onSingleChoiceClick(item);
}
public void onStringSingleChoiceClick(StringSingleChoiceSetting item) {
mClickedItem = item;
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
.setTitle(item.getNameId())
.setSingleChoiceItems(item.getChoicesId(), item.getSelectValueIndex(), this);
mDialog = builder.show();
}
public void onStringSingleChoiceClick(StringSingleChoiceSetting item, int position) {
mClickedPosition = position;
onStringSingleChoiceClick(item);
}
DialogInterface.OnClickListener defaultCancelListener = (dialog, which) -> closeDialog();
public void onDateTimeClick(DateTimeSetting item, int position) {
mClickedItem = item;
mClickedPosition = position;
LayoutInflater inflater = LayoutInflater.from(mView.getActivity());
View view = inflater.inflate(R.layout.sysclock_datetime_picker, null);
DatePicker dp = view.findViewById(R.id.date_picker);
TimePicker tp = view.findViewById(R.id.time_picker);
//set date and time to substrings of settingValue; format = 2018-12-24 04:20:69 (alright maybe not that 69)
String settingValue = item.getValue();
dp.updateDate(Integer.parseInt(settingValue.substring(0, 4)), Integer.parseInt(settingValue.substring(5, 7)) - 1, Integer.parseInt(settingValue.substring(8, 10)));
tp.setIs24HourView(true);
tp.setHour(Integer.parseInt(settingValue.substring(11, 13)));
tp.setMinute(Integer.parseInt(settingValue.substring(14, 16)));
DialogInterface.OnClickListener ok = (dialog, which) -> {
//set it
int year = dp.getYear();
if (year < 2000) {
year = 2000;
}
String month = ("00" + (dp.getMonth() + 1)).substring(String.valueOf(dp.getMonth() + 1).length());
String day = ("00" + dp.getDayOfMonth()).substring(String.valueOf(dp.getDayOfMonth()).length());
String hr = ("00" + tp.getHour()).substring(String.valueOf(tp.getHour()).length());
String min = ("00" + tp.getMinute()).substring(String.valueOf(tp.getMinute()).length());
String datetime = year + "-" + month + "-" + day + " " + hr + ":" + min + ":01";
StringSetting setting = item.setSelectedValue(datetime);
if (setting != null) {
mView.putSetting(setting);
}
mView.onSettingChanged();
mClickedItem = null;
closeDialog();
};
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
.setView(view)
.setPositiveButton(android.R.string.ok, ok)
.setNegativeButton(android.R.string.cancel, defaultCancelListener);
mDialog = builder.show();
}
public void onSliderClick(SliderSetting item, int position) {
mClickedItem = item;
mClickedPosition = position;
mSliderProgress = item.getSelectedValue();
LayoutInflater inflater = LayoutInflater.from(mView.getActivity());
View view = inflater.inflate(R.layout.dialog_slider, null);
Slider slider = view.findViewById(R.id.slider);
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
.setTitle(item.getNameId())
.setView(view)
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, defaultCancelListener)
.setNeutralButton(R.string.slider_default, (DialogInterface dialog, int which) -> {
slider.setValue(item.getDefaultValue());
onClick(dialog, which);
});
mDialog = builder.show();
mTextSliderValue = view.findViewById(R.id.text_value);
mTextSliderValue.setText(String.valueOf(mSliderProgress));
TextView units = view.findViewById(R.id.text_units);
units.setText(item.getUnits());
slider.setValueFrom(item.getMin());
slider.setValueTo(item.getMax());
slider.setValue(mSliderProgress);
slider.addOnChangeListener(this);
}
public void onSubmenuClick(SubmenuSetting item) {
mView.loadSubMenu(item.getMenuKey());
}
public void onInputBindingClick(final InputBindingSetting item, final int position) {
final MotionAlertDialog dialog = new MotionAlertDialog(mContext, item);
dialog.setTitle(R.string.input_binding);
int messageResId = R.string.input_binding_description;
if (item.IsAxisMappingSupported() && !item.IsTrigger()) {
// Use specialized message for axis left/right or up/down
if (item.IsHorizontalOrientation()) {
messageResId = R.string.input_binding_description_horizontal_axis;
} else {
messageResId = R.string.input_binding_description_vertical_axis;
}
}
dialog.setMessage(String.format(mContext.getString(messageResId), mContext.getString(item.getNameId())));
dialog.setButton(AlertDialog.BUTTON_NEGATIVE, mContext.getString(android.R.string.cancel), this);
dialog.setButton(AlertDialog.BUTTON_NEUTRAL, mContext.getString(R.string.clear), (dialogInterface, i) ->
item.removeOldMapping());
dialog.setOnDismissListener(dialog1 ->
{
StringSetting setting = new StringSetting(item.getKey(), item.getSection(), item.getValue());
notifyItemChanged(position);
mView.putSetting(setting);
mView.onSettingChanged();
});
dialog.setCanceledOnTouchOutside(false);
dialog.show();
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (mClickedItem instanceof SingleChoiceSetting) {
SingleChoiceSetting scSetting = (SingleChoiceSetting) mClickedItem;
int value = getValueForSingleChoiceSelection(scSetting, which);
if (scSetting.getSelectedValue() != value) {
mView.onSettingChanged();
}
// Get the backing Setting, which may be null (if for example it was missing from the file)
IntSetting setting = scSetting.setSelectedValue(value);
if (setting != null) {
mView.putSetting(setting);
}
closeDialog();
} else if (mClickedItem instanceof StringSingleChoiceSetting) {
StringSingleChoiceSetting scSetting = (StringSingleChoiceSetting) mClickedItem;
String value = scSetting.getValueAt(which);
if (!scSetting.getSelectedValue().equals(value))
mView.onSettingChanged();
StringSetting setting = scSetting.setSelectedValue(value);
if (setting != null) {
mView.putSetting(setting);
}
closeDialog();
} else if (mClickedItem instanceof SliderSetting) {
SliderSetting sliderSetting = (SliderSetting) mClickedItem;
if (sliderSetting.getSelectedValue() != mSliderProgress) {
mView.onSettingChanged();
}
if (sliderSetting.getSetting() instanceof FloatSetting) {
float value = (float) mSliderProgress;
FloatSetting setting = sliderSetting.setSelectedValue(value);
if (setting != null) {
mView.putSetting(setting);
}
} else {
IntSetting setting = sliderSetting.setSelectedValue(mSliderProgress);
if (setting != null) {
mView.putSetting(setting);
}
}
closeDialog();
}
mClickedItem = null;
mSliderProgress = -1;
}
public void closeDialog() {
if (mDialog != null) {
if (mClickedPosition != -1) {
notifyItemChanged(mClickedPosition);
mClickedPosition = -1;
}
mDialog.dismiss();
mDialog = null;
}
}
private int getValueForSingleChoiceSelection(SingleChoiceSetting item, int which) {
int valuesId = item.getValuesId();
if (valuesId > 0) {
int[] valuesArray = mContext.getResources().getIntArray(valuesId);
return valuesArray[which];
} else {
return which;
}
}
private int getSelectionForSingleChoiceValue(SingleChoiceSetting item) {
int value = item.getSelectedValue();
int valuesId = item.getValuesId();
if (valuesId > 0) {
int[] valuesArray = mContext.getResources().getIntArray(valuesId);
for (int index = 0; index < valuesArray.length; index++) {
int current = valuesArray[index];
if (current == value) {
return index;
}
}
} else {
return value;
}
return -1;
}
@Override
public void onValueChange(@NonNull Slider slider, float value, boolean fromUser) {
mSliderProgress = (int) value;
mTextSliderValue.setText(String.valueOf(mSliderProgress));
}
}

View file

@ -0,0 +1,503 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.ui
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.icu.util.Calendar
import android.icu.util.TimeZone
import android.text.InputFilter
import android.text.format.DateFormat
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.slider.Slider
import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat
import org.citra.citra_emu.R
import org.citra.citra_emu.databinding.DialogSliderBinding
import org.citra.citra_emu.databinding.DialogSoftwareKeyboardBinding
import org.citra.citra_emu.databinding.ListItemSettingBinding
import org.citra.citra_emu.databinding.ListItemSettingSwitchBinding
import org.citra.citra_emu.databinding.ListItemSettingsHeaderBinding
import org.citra.citra_emu.features.settings.model.AbstractBooleanSetting
import org.citra.citra_emu.features.settings.model.AbstractFloatSetting
import org.citra.citra_emu.features.settings.model.AbstractIntSetting
import org.citra.citra_emu.features.settings.model.AbstractSetting
import org.citra.citra_emu.features.settings.model.AbstractStringSetting
import org.citra.citra_emu.features.settings.model.FloatSetting
import org.citra.citra_emu.features.settings.model.ScaledFloatSetting
import org.citra.citra_emu.features.settings.model.view.AbstractShortSetting
import org.citra.citra_emu.features.settings.model.view.DateTimeSetting
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
import org.citra.citra_emu.features.settings.model.view.SettingsItem
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting
import org.citra.citra_emu.features.settings.model.view.SliderSetting
import org.citra.citra_emu.features.settings.model.view.StringInputSetting
import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting
import org.citra.citra_emu.features.settings.model.view.SubmenuSetting
import org.citra.citra_emu.features.settings.model.view.SwitchSetting
import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder
import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder
import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder
import org.citra.citra_emu.features.settings.ui.viewholder.RunnableViewHolder
import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder
import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder
import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder
import org.citra.citra_emu.features.settings.ui.viewholder.StringInputViewHolder
import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder
import org.citra.citra_emu.features.settings.ui.viewholder.SwitchSettingViewHolder
import org.citra.citra_emu.fragments.MessageDialogFragment
import org.citra.citra_emu.fragments.MotionBottomSheetDialogFragment
import org.citra.citra_emu.utils.SystemSaveGame
import java.lang.IllegalStateException
import java.lang.NumberFormatException
import java.text.SimpleDateFormat
class SettingsAdapter(
private val fragmentView: SettingsFragmentView,
private val context: Context
) : RecyclerView.Adapter<SettingViewHolder?>(), DialogInterface.OnClickListener {
private var settings: ArrayList<SettingsItem>? = null
private var clickedItem: SettingsItem? = null
private var clickedPosition: Int
private var dialog: AlertDialog? = null
private var sliderProgress = 0
private var textSliderValue: TextView? = null
private var textInputValue: String = ""
private var defaultCancelListener =
DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> closeDialog() }
init {
clickedPosition = -1
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
SettingsItem.TYPE_HEADER -> {
HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
}
SettingsItem.TYPE_SWITCH -> {
SwitchSettingViewHolder(ListItemSettingSwitchBinding.inflate(inflater), this)
}
SettingsItem.TYPE_SINGLE_CHOICE, SettingsItem.TYPE_STRING_SINGLE_CHOICE -> {
SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
SettingsItem.TYPE_SLIDER -> {
SliderViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
SettingsItem.TYPE_SUBMENU -> {
SubmenuViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
SettingsItem.TYPE_DATETIME_SETTING -> {
DateTimeViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
SettingsItem.TYPE_RUNNABLE -> {
RunnableViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
SettingsItem.TYPE_INPUT_BINDING -> {
InputBindingSettingViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
SettingsItem.TYPE_STRING_INPUT -> {
StringInputViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
else -> {
// TODO: Create an error view since we can't return null now
HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
}
}
}
override fun onBindViewHolder(holder: SettingViewHolder, position: Int) {
holder.bind(getItem(position))
}
private fun getItem(position: Int): SettingsItem {
return settings!![position]
}
override fun getItemCount(): Int {
return if (settings != null) {
settings!!.size
} else {
0
}
}
override fun getItemViewType(position: Int): Int {
return getItem(position).type
}
fun setSettingsList(settings: ArrayList<SettingsItem>?) {
this.settings = settings
notifyDataSetChanged()
}
fun onBooleanClick(item: SwitchSetting, position: Int, checked: Boolean) {
val setting = item.setChecked(checked)
fragmentView.putSetting(setting)
fragmentView.onSettingChanged()
}
private fun onSingleChoiceClick(item: SingleChoiceSetting) {
clickedItem = item
val value = getSelectionForSingleChoiceValue(item)
dialog = MaterialAlertDialogBuilder(context)
.setTitle(item.nameId)
.setSingleChoiceItems(item.choicesId, value, this)
.show()
}
fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
clickedPosition = position
onSingleChoiceClick(item)
}
private fun onStringSingleChoiceClick(item: StringSingleChoiceSetting) {
clickedItem = item
dialog = MaterialAlertDialogBuilder(context)
.setTitle(item.nameId)
.setSingleChoiceItems(item.choices, item.selectValueIndex, this)
.show()
}
fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) {
clickedPosition = position
onStringSingleChoiceClick(item)
}
@SuppressLint("SimpleDateFormat")
fun onDateTimeClick(item: DateTimeSetting, position: Int) {
clickedItem = item
clickedPosition = position
val storedTime: Long = try {
java.lang.Long.decode(item.value) * 1000
} catch (e: NumberFormatException) {
val date = item.value.substringBefore(" ")
val time = item.value.substringAfter(" ")
val formatter = SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssZZZZ")
val gmt = formatter.parse("${date}T${time}+0000")
gmt!!.time
}
// Helper to extract hour and minute from epoch time
val calendar: Calendar = Calendar.getInstance()
calendar.timeInMillis = storedTime
calendar.timeZone = TimeZone.getTimeZone("UTC")
var timeFormat: Int = TimeFormat.CLOCK_12H
if (DateFormat.is24HourFormat(fragmentView.activityView as AppCompatActivity)) {
timeFormat = TimeFormat.CLOCK_24H
}
val datePicker: MaterialDatePicker<Long> = MaterialDatePicker.Builder.datePicker()
.setSelection(storedTime)
.setTitleText(R.string.select_rtc_date)
.build()
val timePicker: MaterialTimePicker = MaterialTimePicker.Builder()
.setTimeFormat(timeFormat)
.setHour(calendar.get(Calendar.HOUR_OF_DAY))
.setMinute(calendar.get(Calendar.MINUTE))
.setTitleText(R.string.select_rtc_time)
.build()
datePicker.addOnPositiveButtonClickListener {
timePicker.show(
(fragmentView.activityView as AppCompatActivity).supportFragmentManager,
"TimePicker"
)
}
timePicker.addOnPositiveButtonClickListener {
var epochTime: Long = datePicker.selection!! / 1000
epochTime += timePicker.hour.toLong() * 60 * 60
epochTime += timePicker.minute.toLong() * 60
val rtcString = epochTime.toString()
if (item.value != rtcString) {
fragmentView.onSettingChanged()
}
notifyItemChanged(clickedPosition)
val setting = item.setSelectedValue(rtcString)
fragmentView.putSetting(setting)
clickedItem = null
}
datePicker.show(
(fragmentView.activityView as AppCompatActivity).supportFragmentManager,
"DatePicker"
)
}
fun onSliderClick(item: SliderSetting, position: Int) {
clickedItem = item
clickedPosition = position
sliderProgress = item.selectedValue
val inflater = LayoutInflater.from(context)
val sliderBinding = DialogSliderBinding.inflate(inflater)
textSliderValue = sliderBinding.textValue
textSliderValue!!.text = sliderProgress.toString()
sliderBinding.textUnits.text = item.units
sliderBinding.slider.apply {
valueFrom = item.min.toFloat()
valueTo = item.max.toFloat()
value = sliderProgress.toFloat()
addOnChangeListener { _: Slider, value: Float, _: Boolean ->
sliderProgress = value.toInt()
textSliderValue!!.text = sliderProgress.toString()
}
}
dialog = MaterialAlertDialogBuilder(context)
.setTitle(item.nameId)
.setView(sliderBinding.root)
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, defaultCancelListener)
.setNeutralButton(R.string.slider_default) { dialog: DialogInterface, which: Int ->
sliderBinding.slider.value = when (item.setting) {
is ScaledFloatSetting -> {
val scaledSetting = item.setting as ScaledFloatSetting
scaledSetting.defaultValue * scaledSetting.scale
}
is FloatSetting -> (item.setting as FloatSetting).defaultValue
else -> item.defaultValue!!
}
onClick(dialog, which)
}
.show()
}
fun onSubmenuClick(item: SubmenuSetting) {
fragmentView.loadSubMenu(item.menuKey)
}
fun onInputBindingClick(item: InputBindingSetting, position: Int) {
val activity = fragmentView.activityView as FragmentActivity
MotionBottomSheetDialogFragment.newInstance(
item,
{ closeDialog() },
{
notifyItemChanged(position)
fragmentView.onSettingChanged()
}
).show(activity.supportFragmentManager, MotionBottomSheetDialogFragment.TAG)
}
fun onStringInputClick(item: StringInputSetting, position: Int) {
clickedItem = item
clickedPosition = position
textInputValue = item.selectedValue
val inflater = LayoutInflater.from(context)
val inputBinding = DialogSoftwareKeyboardBinding.inflate(inflater)
inputBinding.editTextInput.setText(textInputValue)
inputBinding.editTextInput.doOnTextChanged { text, _, _, _ ->
textInputValue = text.toString()
}
if (item.characterLimit != 0) {
inputBinding.editTextInput.filters =
arrayOf(InputFilter.LengthFilter(item.characterLimit))
}
dialog = MaterialAlertDialogBuilder(context)
.setView(inputBinding.root)
.setTitle(item.nameId)
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, defaultCancelListener)
.show()
}
override fun onClick(dialog: DialogInterface, which: Int) {
when (clickedItem) {
is SingleChoiceSetting -> {
val scSetting = clickedItem as SingleChoiceSetting
val setting = when (scSetting.setting) {
is AbstractIntSetting -> {
val value = getValueForSingleChoiceSelection(scSetting, which)
if (scSetting.selectedValue != value) {
fragmentView.onSettingChanged()
}
scSetting.setSelectedValue(value)
}
is AbstractShortSetting -> {
val value = getValueForSingleChoiceSelection(scSetting, which).toShort()
if (scSetting.selectedValue.toShort() != value) {
fragmentView.onSettingChanged()
}
scSetting.setSelectedValue(value)
}
else -> throw IllegalStateException("Unrecognized type used for SingleChoiceSetting!")
}
fragmentView.putSetting(setting)
closeDialog()
}
is StringSingleChoiceSetting -> {
val scSetting = clickedItem as StringSingleChoiceSetting
val setting = when (scSetting.setting) {
is AbstractStringSetting -> {
val value = scSetting.getValueAt(which)
if (scSetting.selectedValue != value) fragmentView.onSettingChanged()
scSetting.setSelectedValue(value!!)
}
is AbstractShortSetting -> {
if (scSetting.selectValueIndex != which) fragmentView.onSettingChanged()
scSetting.setSelectedValue(scSetting.getValueAt(which)?.toShort() ?: 1)
}
else -> throw IllegalStateException("Unrecognized type used for StringSingleChoiceSetting!")
}
fragmentView.putSetting(setting)
closeDialog()
}
is SliderSetting -> {
val sliderSetting = clickedItem as SliderSetting
if (sliderSetting.selectedValue != sliderProgress) {
fragmentView.onSettingChanged()
}
when (sliderSetting.setting) {
is FloatSetting,
is ScaledFloatSetting -> {
val value = sliderProgress.toFloat()
val setting = sliderSetting.setSelectedValue(value)
fragmentView.putSetting(setting)
}
else -> {
val setting = sliderSetting.setSelectedValue(sliderProgress)
fragmentView.putSetting(setting)
}
}
closeDialog()
}
is StringInputSetting -> {
val inputSetting = clickedItem as StringInputSetting
if (inputSetting.selectedValue != textInputValue) {
fragmentView.onSettingChanged()
}
val setting = inputSetting.setSelectedValue(textInputValue)
fragmentView.putSetting(setting)
closeDialog()
}
}
clickedItem = null
sliderProgress = -1
textInputValue = ""
}
fun onLongClick(setting: AbstractSetting, position: Int): Boolean {
MaterialAlertDialogBuilder(context)
.setMessage(R.string.reset_setting_confirmation)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
when (setting) {
is AbstractBooleanSetting -> setting.boolean = setting.defaultValue as Boolean
is AbstractFloatSetting -> {
if (setting is ScaledFloatSetting) {
setting.float = setting.defaultValue * setting.scale
} else {
setting.float = setting.defaultValue as Float
}
}
is AbstractIntSetting -> setting.int = setting.defaultValue as Int
is AbstractStringSetting -> setting.string = setting.defaultValue as String
is AbstractShortSetting -> setting.short = setting.defaultValue as Short
}
notifyItemChanged(position)
fragmentView.onSettingChanged()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
return true
}
fun onClickDisabledSetting() {
MessageDialogFragment.newInstance(
R.string.setting_not_editable,
R.string.setting_not_editable_description
).show((fragmentView as SettingsFragment).childFragmentManager, MessageDialogFragment.TAG)
}
fun onClickRegenerateConsoleId() {
MaterialAlertDialogBuilder(context)
.setTitle(R.string.regenerate_console_id)
.setMessage(R.string.regenerate_console_id_description)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
SystemSaveGame.regenerateConsoleId()
notifyDataSetChanged()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
fun closeDialog() {
if (dialog != null) {
if (clickedPosition != -1) {
notifyItemChanged(clickedPosition)
clickedPosition = -1
}
dialog!!.dismiss()
dialog = null
}
}
private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int {
val valuesId = item.valuesId
return if (valuesId > 0) {
val valuesArray = context.resources.getIntArray(valuesId)
valuesArray[which]
} else {
which
}
}
private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
val value = item.selectedValue
val valuesId = item.valuesId
if (valuesId > 0) {
val valuesArray = context.resources.getIntArray(valuesId)
for (index in valuesArray.indices) {
val current = valuesArray[index]
if (current == value) {
return index
}
}
} else {
return value
}
return -1
}
}

View file

@ -1,151 +0,0 @@
package org.citra.citra_emu.features.settings.ui;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.model.Setting;
import org.citra.citra_emu.features.settings.model.Settings;
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
import org.citra.citra_emu.ui.DividerItemDecoration;
import java.util.ArrayList;
public final class SettingsFragment extends Fragment implements SettingsFragmentView {
private static final String ARGUMENT_MENU_TAG = "menu_tag";
private static final String ARGUMENT_GAME_ID = "game_id";
private SettingsFragmentPresenter mPresenter = new SettingsFragmentPresenter(this);
private SettingsActivityView mActivity;
private SettingsAdapter mAdapter;
private RecyclerView mRecyclerView;
public static Fragment newInstance(String menuTag, String gameId) {
SettingsFragment fragment = new SettingsFragment();
Bundle arguments = new Bundle();
arguments.putString(ARGUMENT_MENU_TAG, menuTag);
arguments.putString(ARGUMENT_GAME_ID, gameId);
fragment.setArguments(arguments);
return fragment;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
mActivity = (SettingsActivityView) context;
mPresenter.onAttach();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
String menuTag = getArguments().getString(ARGUMENT_MENU_TAG);
String gameId = getArguments().getString(ARGUMENT_GAME_ID);
mAdapter = new SettingsAdapter(this, getActivity());
mPresenter.onCreate(menuTag, gameId);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_settings, container, false);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
LinearLayoutManager manager = new LinearLayoutManager(getActivity());
mRecyclerView = view.findViewById(R.id.list_settings);
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.setLayoutManager(manager);
mRecyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), null));
SettingsActivityView activity = (SettingsActivityView) getActivity();
mPresenter.onViewCreated(activity.getSettings());
setInsets();
}
@Override
public void onDetach() {
super.onDetach();
mActivity = null;
if (mAdapter != null) {
mAdapter.closeDialog();
}
}
@Override
public void onSettingsFileLoaded(Settings settings) {
mPresenter.setSettings(settings);
}
@Override
public void passSettingsToActivity(Settings settings) {
if (mActivity != null) {
mActivity.setSettings(settings);
}
}
@Override
public void showSettingsList(ArrayList<SettingsItem> settingsList) {
mAdapter.setSettings(settingsList);
}
@Override
public void loadDefaultSettings() {
mPresenter.loadDefaultSettings();
}
@Override
public void loadSubMenu(String menuKey) {
mActivity.showSettingsFragment(menuKey, true, getArguments().getString(ARGUMENT_GAME_ID));
}
@Override
public void showToastMessage(String message, boolean is_long) {
mActivity.showToastMessage(message, is_long);
}
@Override
public void putSetting(Setting setting) {
mPresenter.putSetting(setting);
}
@Override
public void onSettingChanged() {
mActivity.onSettingChanged();
}
private void setInsets() {
ViewCompat.setOnApplyWindowInsetsListener(mRecyclerView, (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(insets.left, 0, insets.right, insets.bottom);
return windowInsets;
});
}
}

View file

@ -0,0 +1,128 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.ui
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.divider.MaterialDividerItemDecoration
import org.citra.citra_emu.databinding.FragmentSettingsBinding
import org.citra.citra_emu.features.settings.model.AbstractSetting
import org.citra.citra_emu.features.settings.model.view.SettingsItem
class SettingsFragment : Fragment(), SettingsFragmentView {
override var activityView: SettingsActivityView? = null
private val fragmentPresenter = SettingsFragmentPresenter(this)
private var settingsAdapter: SettingsAdapter? = null
private var _binding: FragmentSettingsBinding? = null
private val binding get() = _binding!!
override fun onAttach(context: Context) {
super.onAttach(context)
activityView = requireActivity() as SettingsActivityView
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val menuTag = requireArguments().getString(ARGUMENT_MENU_TAG)
val gameId = requireArguments().getString(ARGUMENT_GAME_ID)
fragmentPresenter.onCreate(menuTag!!, gameId!!)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSettingsBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
settingsAdapter = SettingsAdapter(this, requireActivity())
val dividerDecoration = MaterialDividerItemDecoration(
requireContext(),
LinearLayoutManager.VERTICAL
)
dividerDecoration.isLastItemDecorated = false
binding.listSettings.apply {
adapter = settingsAdapter
layoutManager = LinearLayoutManager(activity)
addItemDecoration(dividerDecoration)
}
fragmentPresenter.onViewCreated(settingsAdapter!!)
setInsets()
}
override fun onDetach() {
super.onDetach()
activityView = null
if (settingsAdapter != null) {
settingsAdapter!!.closeDialog()
}
}
override fun showSettingsList(settingsList: ArrayList<SettingsItem>) {
settingsAdapter!!.setSettingsList(settingsList)
}
override fun loadSettingsList() {
fragmentPresenter.loadSettingsList()
}
override fun loadSubMenu(menuKey: String) {
activityView!!.showSettingsFragment(
menuKey,
true,
requireArguments().getString(ARGUMENT_GAME_ID)!!
)
}
override fun showToastMessage(message: String?, is_long: Boolean) {
activityView!!.showToastMessage(message!!, is_long)
}
override fun putSetting(setting: AbstractSetting) {
fragmentPresenter.putSetting(setting)
}
override fun onSettingChanged() {
activityView!!.onSettingChanged()
}
private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener(
binding.listSettings
) { view: View, windowInsets: WindowInsetsCompat ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.updatePadding(bottom = insets.bottom)
windowInsets
}
}
companion object {
private const val ARGUMENT_MENU_TAG = "menu_tag"
private const val ARGUMENT_GAME_ID = "game_id"
fun newInstance(menuTag: String?, gameId: String?): Fragment {
val fragment = SettingsFragment()
val arguments = Bundle()
arguments.putString(ARGUMENT_MENU_TAG, menuTag)
arguments.putString(ARGUMENT_GAME_ID, gameId)
fragment.arguments = arguments
return fragment
}
}
}

View file

@ -1,410 +0,0 @@
package org.citra.citra_emu.features.settings.ui;
import android.app.Activity;
import android.content.Context;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.text.TextUtils;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.model.Setting;
import org.citra.citra_emu.features.settings.model.SettingSection;
import org.citra.citra_emu.features.settings.model.Settings;
import org.citra.citra_emu.features.settings.model.StringSetting;
import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting;
import org.citra.citra_emu.features.settings.model.view.DateTimeSetting;
import org.citra.citra_emu.features.settings.model.view.HeaderSetting;
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
import org.citra.citra_emu.features.settings.model.view.SliderSetting;
import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting;
import org.citra.citra_emu.features.settings.model.view.SubmenuSetting;
import org.citra.citra_emu.features.settings.utils.SettingsFile;
import org.citra.citra_emu.utils.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Objects;
public final class SettingsFragmentPresenter {
private SettingsFragmentView mView;
private String mMenuTag;
private String mGameID;
private Settings mSettings;
private ArrayList<SettingsItem> mSettingsList;
public SettingsFragmentPresenter(SettingsFragmentView view) {
mView = view;
}
public void onCreate(String menuTag, String gameId) {
mGameID = gameId;
mMenuTag = menuTag;
}
public void onViewCreated(Settings settings) {
setSettings(settings);
}
/**
* If the screen is rotated, the Activity will forget the settings map. This fragment
* won't, though; so rather than have the Activity reload from disk, have the fragment pass
* the settings map back to the Activity.
*/
public void onAttach() {
if (mSettings != null) {
mView.passSettingsToActivity(mSettings);
}
}
public void putSetting(Setting setting) {
mSettings.getSection(setting.getSection()).putSetting(setting);
}
private StringSetting asStringSetting(Setting setting) {
if (setting == null) {
return null;
}
StringSetting stringSetting = new StringSetting(setting.getKey(), setting.getSection(), setting.getValueAsString());
putSetting(stringSetting);
return stringSetting;
}
public void loadDefaultSettings() {
loadSettingsList();
}
public void setSettings(Settings settings) {
if (mSettingsList == null && settings != null) {
mSettings = settings;
loadSettingsList();
} else {
mView.getActivity().setTitle(R.string.preferences_settings);
mView.showSettingsList(mSettingsList);
}
}
private void loadSettingsList() {
if (!TextUtils.isEmpty(mGameID)) {
mView.getActivity().setTitle("Game Settings: " + mGameID);
}
ArrayList<SettingsItem> sl = new ArrayList<>();
if (mMenuTag == null) {
return;
}
switch (mMenuTag) {
case SettingsFile.FILE_NAME_CONFIG:
addConfigSettings(sl);
break;
case Settings.SECTION_CORE:
addGeneralSettings(sl);
break;
case Settings.SECTION_SYSTEM:
addSystemSettings(sl);
break;
case Settings.SECTION_CAMERA:
addCameraSettings(sl);
break;
case Settings.SECTION_CONTROLS:
addInputSettings(sl);
break;
case Settings.SECTION_RENDERER:
addGraphicsSettings(sl);
break;
case Settings.SECTION_AUDIO:
addAudioSettings(sl);
break;
case Settings.SECTION_DEBUG:
addDebugSettings(sl);
break;
default:
mView.showToastMessage("Unimplemented menu", false);
return;
}
mSettingsList = sl;
mView.showSettingsList(mSettingsList);
}
private void addConfigSettings(ArrayList<SettingsItem> sl) {
mView.getActivity().setTitle(R.string.preferences_settings);
sl.add(new SubmenuSetting(null, null, R.string.preferences_general, 0, Settings.SECTION_CORE));
sl.add(new SubmenuSetting(null, null, R.string.preferences_system, 0, Settings.SECTION_SYSTEM));
sl.add(new SubmenuSetting(null, null, R.string.preferences_camera, 0, Settings.SECTION_CAMERA));
sl.add(new SubmenuSetting(null, null, R.string.preferences_controls, 0, Settings.SECTION_CONTROLS));
sl.add(new SubmenuSetting(null, null, R.string.preferences_graphics, 0, Settings.SECTION_RENDERER));
sl.add(new SubmenuSetting(null, null, R.string.preferences_audio, 0, Settings.SECTION_AUDIO));
sl.add(new SubmenuSetting(null, null, R.string.preferences_debug, 0, Settings.SECTION_DEBUG));
}
private void addGeneralSettings(ArrayList<SettingsItem> sl) {
mView.getActivity().setTitle(R.string.preferences_general);
SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER);
Setting frameLimitEnable = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED);
Setting frameLimitValue = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT);
sl.add(new CheckBoxSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED, Settings.SECTION_RENDERER, R.string.frame_limit_enable, R.string.frame_limit_enable_description, true, frameLimitEnable));
sl.add(new SliderSetting(SettingsFile.KEY_FRAME_LIMIT, Settings.SECTION_RENDERER, R.string.frame_limit_slider, R.string.frame_limit_slider_description, 1, 200, "%", 100, frameLimitValue));
}
private void addSystemSettings(ArrayList<SettingsItem> sl) {
mView.getActivity().setTitle(R.string.preferences_system);
SettingSection systemSection = mSettings.getSection(Settings.SECTION_SYSTEM);
Setting region = systemSection.getSetting(SettingsFile.KEY_REGION_VALUE);
Setting language = systemSection.getSetting(SettingsFile.KEY_LANGUAGE);
Setting systemClock = systemSection.getSetting(SettingsFile.KEY_INIT_CLOCK);
Setting dateTime = systemSection.getSetting(SettingsFile.KEY_INIT_TIME);
Setting pluginLoader = systemSection.getSetting(SettingsFile.KEY_PLUGIN_LOADER);
Setting allowPluginLoader = systemSection.getSetting(SettingsFile.KEY_ALLOW_PLUGIN_LOADER);
sl.add(new SingleChoiceSetting(SettingsFile.KEY_REGION_VALUE, Settings.SECTION_SYSTEM, R.string.emulated_region, 0, R.array.regionNames, R.array.regionValues, -1, region));
sl.add(new SingleChoiceSetting(SettingsFile.KEY_LANGUAGE, Settings.SECTION_SYSTEM, R.string.emulated_language, 0, R.array.languageNames, R.array.languageValues, 1, language));
sl.add(new HeaderSetting(null, null, R.string.clock, 0));
sl.add(new SingleChoiceSetting(SettingsFile.KEY_INIT_CLOCK, Settings.SECTION_SYSTEM, R.string.init_clock, R.string.init_clock_description, R.array.systemClockNames, R.array.systemClockValues, 0, systemClock));
sl.add(new DateTimeSetting(SettingsFile.KEY_INIT_TIME, Settings.SECTION_SYSTEM, R.string.init_time, R.string.init_time_description, "2000-01-01 00:00:01", dateTime));
sl.add(new HeaderSetting(null, null, R.string.plugin_loader, 0));
sl.add(new CheckBoxSetting(SettingsFile.KEY_PLUGIN_LOADER, Settings.SECTION_SYSTEM, R.string.plugin_loader, R.string.plugin_loader_description, false, pluginLoader));
sl.add(new CheckBoxSetting(SettingsFile.KEY_ALLOW_PLUGIN_LOADER, Settings.SECTION_SYSTEM, R.string.allow_plugin_loader, R.string.allow_plugin_loader_description, true, allowPluginLoader));
}
private void addCameraSettings(ArrayList<SettingsItem> sl) {
final Activity activity = mView.getActivity();
activity.setTitle(R.string.preferences_camera);
// Get the camera IDs
CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
ArrayList<String> supportedCameraNameList = new ArrayList<>();
ArrayList<String> supportedCameraIdList = new ArrayList<>();
if (cameraManager != null) {
try {
for (String id : cameraManager.getCameraIdList()) {
final CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
if (Objects.requireNonNull(characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)) == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
continue; // Legacy cameras cannot be used with the NDK
}
supportedCameraIdList.add(id);
final int facing = Objects.requireNonNull(characteristics.get(CameraCharacteristics.LENS_FACING));
int stringId = R.string.camera_facing_external;
switch (facing) {
case CameraCharacteristics.LENS_FACING_FRONT:
stringId = R.string.camera_facing_front;
break;
case CameraCharacteristics.LENS_FACING_BACK:
stringId = R.string.camera_facing_back;
break;
case CameraCharacteristics.LENS_FACING_EXTERNAL:
stringId = R.string.camera_facing_external;
break;
}
supportedCameraNameList.add(String.format("%1$s (%2$s)", id, activity.getString(stringId)));
}
} catch (CameraAccessException e) {
Log.error("Couldn't retrieve camera list");
e.printStackTrace();
}
}
// Create the names and values for display
ArrayList<String> cameraDeviceNameList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceNames)));
cameraDeviceNameList.addAll(supportedCameraNameList);
ArrayList<String> cameraDeviceValueList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceValues)));
cameraDeviceValueList.addAll(supportedCameraIdList);
final String[] cameraDeviceNames = cameraDeviceNameList.toArray(new String[]{});
final String[] cameraDeviceValues = cameraDeviceValueList.toArray(new String[]{});
final boolean haveCameraDevices = !supportedCameraIdList.isEmpty();
String[] imageSourceNames = activity.getResources().getStringArray(R.array.cameraImageSourceNames);
String[] imageSourceValues = activity.getResources().getStringArray(R.array.cameraImageSourceValues);
if (!haveCameraDevices) {
// Remove the last entry (ndk / Device Camera)
imageSourceNames = Arrays.copyOfRange(imageSourceNames, 0, imageSourceNames.length - 1);
imageSourceValues = Arrays.copyOfRange(imageSourceValues, 0, imageSourceValues.length - 1);
}
final String defaultImageSource = haveCameraDevices ? "ndk" : "image";
SettingSection cameraSection = mSettings.getSection(Settings.SECTION_CAMERA);
Setting innerCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_NAME);
Setting innerCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG));
Setting innerCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_FLIP);
sl.add(new HeaderSetting(null, null, R.string.inner_camera, 0));
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, innerCameraImageSource));
if (haveCameraDevices)
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_front", innerCameraConfig));
sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, innerCameraFlip));
Setting outerLeftCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME);
Setting outerLeftCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG));
Setting outerLeftCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP);
sl.add(new HeaderSetting(null, null, R.string.outer_left_camera, 0));
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerLeftCameraImageSource));
if (haveCameraDevices)
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerLeftCameraConfig));
sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerLeftCameraFlip));
Setting outerRightCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME);
Setting outerRightCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG));
Setting outerRightCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP);
sl.add(new HeaderSetting(null, null, R.string.outer_right_camera, 0));
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerRightCameraImageSource));
if (haveCameraDevices)
sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerRightCameraConfig));
sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerRightCameraFlip));
}
private void addInputSettings(ArrayList<SettingsItem> sl) {
mView.getActivity().setTitle(R.string.preferences_controls);
SettingSection controlsSection = mSettings.getSection(Settings.SECTION_CONTROLS);
Setting buttonA = controlsSection.getSetting(SettingsFile.KEY_BUTTON_A);
Setting buttonB = controlsSection.getSetting(SettingsFile.KEY_BUTTON_B);
Setting buttonX = controlsSection.getSetting(SettingsFile.KEY_BUTTON_X);
Setting buttonY = controlsSection.getSetting(SettingsFile.KEY_BUTTON_Y);
Setting buttonSelect = controlsSection.getSetting(SettingsFile.KEY_BUTTON_SELECT);
Setting buttonStart = controlsSection.getSetting(SettingsFile.KEY_BUTTON_START);
Setting circlepadAxisVert = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL);
Setting circlepadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL);
Setting cstickAxisVert = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL);
Setting cstickAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL);
Setting dpadAxisVert = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL);
Setting dpadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL);
// Setting buttonUp = controlsSection.getSetting(SettingsFile.KEY_BUTTON_UP);
// Setting buttonDown = controlsSection.getSetting(SettingsFile.KEY_BUTTON_DOWN);
// Setting buttonLeft = controlsSection.getSetting(SettingsFile.KEY_BUTTON_LEFT);
// Setting buttonRight = controlsSection.getSetting(SettingsFile.KEY_BUTTON_RIGHT);
Setting buttonL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_L);
Setting buttonR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_R);
Setting buttonZL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZL);
Setting buttonZR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZR);
sl.add(new HeaderSetting(null, null, R.string.generic_buttons, 0));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_A, Settings.SECTION_CONTROLS, R.string.button_a, buttonA));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_B, Settings.SECTION_CONTROLS, R.string.button_b, buttonB));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_X, Settings.SECTION_CONTROLS, R.string.button_x, buttonX));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_Y, Settings.SECTION_CONTROLS, R.string.button_y, buttonY));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_SELECT, Settings.SECTION_CONTROLS, R.string.button_select, buttonSelect));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_START, Settings.SECTION_CONTROLS, R.string.button_start, buttonStart));
sl.add(new HeaderSetting(null, null, R.string.controller_circlepad, 0));
sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, circlepadAxisVert));
sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, circlepadAxisHoriz));
sl.add(new HeaderSetting(null, null, R.string.controller_c, 0));
sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, cstickAxisVert));
sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, cstickAxisHoriz));
sl.add(new HeaderSetting(null, null, R.string.controller_dpad, 0));
sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, dpadAxisVert));
sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, dpadAxisHoriz));
// TODO(bunnei): Figure out what to do with these. Configuring is functional, but removing for MVP because they are confusing.
// sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_UP, Settings.SECTION_CONTROLS, R.string.generic_up, buttonUp));
// sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_DOWN, Settings.SECTION_CONTROLS, R.string.generic_down, buttonDown));
// sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_LEFT, Settings.SECTION_CONTROLS, R.string.generic_left, buttonLeft));
// sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_RIGHT, Settings.SECTION_CONTROLS, R.string.generic_right, buttonRight));
sl.add(new HeaderSetting(null, null, R.string.controller_triggers, 0));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_L, Settings.SECTION_CONTROLS, R.string.button_l, buttonL));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_R, Settings.SECTION_CONTROLS, R.string.button_r, buttonR));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZL, Settings.SECTION_CONTROLS, R.string.button_zl, buttonZL));
sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZR, Settings.SECTION_CONTROLS, R.string.button_zr, buttonZR));
}
private void addGraphicsSettings(ArrayList<SettingsItem> sl) {
mView.getActivity().setTitle(R.string.preferences_graphics);
SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER);
Setting graphicsApi = rendererSection.getSetting(SettingsFile.KEY_GRAPHICS_API);
Setting spirvShaderGen = rendererSection.getSetting(SettingsFile.KEY_SPIRV_SHADER_GEN);
Setting asyncShaders = rendererSection.getSetting(SettingsFile.KEY_ASYNC_SHADERS);
Setting resolutionFactor = rendererSection.getSetting(SettingsFile.KEY_RESOLUTION_FACTOR);
Setting filterMode = rendererSection.getSetting(SettingsFile.KEY_FILTER_MODE);
Setting shadersAccurateMul = rendererSection.getSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL);
Setting render3dMode = rendererSection.getSetting(SettingsFile.KEY_RENDER_3D);
Setting factor3d = rendererSection.getSetting(SettingsFile.KEY_FACTOR_3D);
Setting useDiskShaderCache = rendererSection.getSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE);
Setting textureFilterName = rendererSection.getSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME);
SettingSection layoutSection = mSettings.getSection(Settings.SECTION_LAYOUT);
Setting cardboardScreenSize = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE);
Setting cardboardXShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT);
Setting cardboardYShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_Y_SHIFT);
SettingSection utilitySection = mSettings.getSection(Settings.SECTION_UTILITY);
Setting dumpTextures = utilitySection.getSetting(SettingsFile.KEY_DUMP_TEXTURES);
Setting customTextures = utilitySection.getSetting(SettingsFile.KEY_CUSTOM_TEXTURES);
Setting asyncCustomLoading = utilitySection.getSetting(SettingsFile.KEY_ASYNC_CUSTOM_LOADING);
//Setting preloadTextures = utilitySection.getSetting(SettingsFile.KEY_PRELOAD_TEXTURES);
sl.add(new HeaderSetting(null, null, R.string.renderer, 0));
sl.add(new SingleChoiceSetting(SettingsFile.KEY_GRAPHICS_API, Settings.SECTION_RENDERER, R.string.graphics_api, 0, R.array.graphicsApiNames, R.array.graphicsApiValues, 0, graphicsApi));
sl.add(new CheckBoxSetting(SettingsFile.KEY_SPIRV_SHADER_GEN, Settings.SECTION_RENDERER, R.string.spirv_shader_gen, R.string.spirv_shader_gen_description, true, spirvShaderGen));
sl.add(new CheckBoxSetting(SettingsFile.KEY_ASYNC_SHADERS, Settings.SECTION_RENDERER, R.string.async_shaders, R.string.async_shaders_description, false, asyncShaders));
sl.add(new SliderSetting(SettingsFile.KEY_RESOLUTION_FACTOR, Settings.SECTION_RENDERER, R.string.internal_resolution, R.string.internal_resolution_description, 1, 4, "x", 1, resolutionFactor));
sl.add(new CheckBoxSetting(SettingsFile.KEY_FILTER_MODE, Settings.SECTION_RENDERER, R.string.linear_filtering, R.string.linear_filtering_description, true, filterMode));
sl.add(new CheckBoxSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL, Settings.SECTION_RENDERER, R.string.shaders_accurate_mul, R.string.shaders_accurate_mul_description, false, shadersAccurateMul));
sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE, Settings.SECTION_RENDERER, R.string.use_disk_shader_cache, R.string.use_disk_shader_cache_description, true, useDiskShaderCache));
sl.add(new SingleChoiceSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME, Settings.SECTION_RENDERER, R.string.texture_filter_name, 0, R.array.textureFilterNames, R.array.textureFilterValues, 0, textureFilterName));
sl.add(new HeaderSetting(null, null, R.string.stereoscopy, 0));
sl.add(new SingleChoiceSetting(SettingsFile.KEY_RENDER_3D, Settings.SECTION_RENDERER, R.string.render3d, 0, R.array.render3dModes, R.array.render3dValues, 0, render3dMode));
sl.add(new SliderSetting(SettingsFile.KEY_FACTOR_3D, Settings.SECTION_RENDERER, R.string.factor3d, R.string.factor3d_description, 0, 100, "%", 0, factor3d));
sl.add(new HeaderSetting(null, null, R.string.cardboard_vr, 0));
sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE, Settings.SECTION_LAYOUT, R.string.cardboard_screen_size, R.string.cardboard_screen_size_description, 30, 100, "%", 85, cardboardScreenSize));
sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT, Settings.SECTION_LAYOUT, R.string.cardboard_x_shift, R.string.cardboard_x_shift_description, -100, 100, "%", 0, cardboardXShift));
sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_Y_SHIFT, Settings.SECTION_LAYOUT, R.string.cardboard_y_shift, R.string.cardboard_y_shift_description, -100, 100, "%", 0, cardboardYShift));
sl.add(new HeaderSetting(null, null, R.string.utility, 0));
sl.add(new CheckBoxSetting(SettingsFile.KEY_DUMP_TEXTURES, Settings.SECTION_UTILITY, R.string.dump_textures, R.string.dump_textures_description, false, dumpTextures));
sl.add(new CheckBoxSetting(SettingsFile.KEY_CUSTOM_TEXTURES, Settings.SECTION_UTILITY, R.string.custom_textures, R.string.custom_textures_description, false, customTextures));
sl.add(new CheckBoxSetting(SettingsFile.KEY_ASYNC_CUSTOM_LOADING, Settings.SECTION_UTILITY, R.string.async_custom_loading, R.string.async_custom_loading_description, true, asyncCustomLoading));
//Disabled until custom texture implementation gets rewrite, current one overloads RAM and crashes Citra.
//sl.add(new CheckBoxSetting(SettingsFile.KEY_PRELOAD_TEXTURES, Settings.SECTION_UTILITY, R.string.preload_textures, R.string.preload_textures_description, false, preloadTextures));
}
private void addAudioSettings(ArrayList<SettingsItem> sl) {
mView.getActivity().setTitle(R.string.preferences_audio);
SettingSection audioSection = mSettings.getSection(Settings.SECTION_AUDIO);
Setting audioStretch = audioSection.getSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING);
Setting audioInputType = audioSection.getSetting(SettingsFile.KEY_AUDIO_INPUT_TYPE);
sl.add(new CheckBoxSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING, Settings.SECTION_AUDIO, R.string.audio_stretch, R.string.audio_stretch_description, true, audioStretch));
sl.add(new SingleChoiceSetting(SettingsFile.KEY_AUDIO_INPUT_TYPE, Settings.SECTION_AUDIO, R.string.audio_input_type, 0, R.array.audioInputTypeNames, R.array.audioInputTypeValues, 0, audioInputType));
}
private void addDebugSettings(ArrayList<SettingsItem> sl) {
mView.getActivity().setTitle(R.string.preferences_debug);
SettingSection coreSection = mSettings.getSection(Settings.SECTION_CORE);
SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER);
Setting useCpuJit = coreSection.getSetting(SettingsFile.KEY_CPU_JIT);
Setting hardwareShader = rendererSection.getSetting(SettingsFile.KEY_HW_SHADER);
Setting vsyncEnable = rendererSection.getSetting(SettingsFile.KEY_USE_VSYNC);
Setting rendererDebug = rendererSection.getSetting(SettingsFile.KEY_RENDERER_DEBUG);
sl.add(new HeaderSetting(null, null, R.string.debug_warning, 0));
sl.add(new CheckBoxSetting(SettingsFile.KEY_CPU_JIT, Settings.SECTION_CORE, R.string.cpu_jit, R.string.cpu_jit_description, true, useCpuJit, true, mView));
sl.add(new CheckBoxSetting(SettingsFile.KEY_HW_SHADER, Settings.SECTION_RENDERER, R.string.hw_shaders, R.string.hw_shaders_description, true, hardwareShader, true, mView));
sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_VSYNC, Settings.SECTION_RENDERER, R.string.vsync, R.string.vsync_description, true, vsyncEnable));
sl.add(new CheckBoxSetting(SettingsFile.KEY_RENDERER_DEBUG, Settings.SECTION_DEBUG, R.string.renderer_debug, R.string.renderer_debug_description, false, rendererDebug));
}
}

View file

@ -1,78 +0,0 @@
package org.citra.citra_emu.features.settings.ui;
import androidx.fragment.app.FragmentActivity;
import org.citra.citra_emu.features.settings.model.Setting;
import org.citra.citra_emu.features.settings.model.Settings;
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
import java.util.ArrayList;
/**
* Abstraction for a screen showing a list of settings. Instances of
* this type of view will each display a layer of the setting hierarchy.
*/
public interface SettingsFragmentView {
/**
* Called by the containing Activity to notify the Fragment that an
* asynchronous load operation completed.
*
* @param settings The (possibly null) result of the ini load operation.
*/
void onSettingsFileLoaded(Settings settings);
/**
* Pass a settings HashMap to the containing activity, so that it can
* share the HashMap with other SettingsFragments; useful so that rotations
* do not require an additional load operation.
*
* @param settings An ArrayList containing all the settings HashMaps.
*/
void passSettingsToActivity(Settings settings);
/**
* Pass an ArrayList to the View so that it can be displayed on screen.
*
* @param settingsList The result of converting the HashMap to an ArrayList
*/
void showSettingsList(ArrayList<SettingsItem> settingsList);
/**
* Called by the containing Activity when an asynchronous load operation fails.
* Instructs the Fragment to load the settings screen with defaults selected.
*/
void loadDefaultSettings();
/**
* @return The Fragment's containing activity.
*/
FragmentActivity getActivity();
/**
* Tell the Fragment to tell the containing Activity to show a new
* Fragment containing a submenu of settings.
*
* @param menuKey Identifier for the settings group that should be shown.
*/
void loadSubMenu(String menuKey);
/**
* Tell the Fragment to tell the containing activity to display a toast message.
*
* @param message Text to be shown in the Toast
* @param is_long Whether this should be a long Toast or short one.
*/
void showToastMessage(String message, boolean is_long);
/**
* Have the fragment add a setting to the HashMap.
*
* @param setting The (possibly previously missing) new setting.
*/
void putSetting(Setting setting);
/**
* Have the fragment tell the containing Activity that a setting was modified.
*/
void onSettingChanged();
}

View file

@ -0,0 +1,59 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.ui
import org.citra.citra_emu.features.settings.model.AbstractSetting
import org.citra.citra_emu.features.settings.model.view.SettingsItem
/**
* Abstraction for a screen showing a list of settings. Instances of
* this type of view will each display a layer of the setting hierarchy.
*/
interface SettingsFragmentView {
/**
* Pass an ArrayList to the View so that it can be displayed on screen.
*
* @param settingsList The result of converting the HashMap to an ArrayList
*/
fun showSettingsList(settingsList: ArrayList<SettingsItem>)
/**
* Instructs the Fragment to load the settings screen.
*/
fun loadSettingsList()
/**
* @return The Fragment's containing activity.
*/
val activityView: SettingsActivityView?
/**
* Tell the Fragment to tell the containing Activity to show a new
* Fragment containing a submenu of settings.
*
* @param menuKey Identifier for the settings group that should be shown.
*/
fun loadSubMenu(menuKey: String)
/**
* Tell the Fragment to tell the containing activity to display a toast message.
*
* @param message Text to be shown in the Toast
* @param is_long Whether this should be a long Toast or short one.
*/
fun showToastMessage(message: String?, is_long: Boolean)
/**
* Have the fragment add a setting to the HashMap.
*
* @param setting The (possibly previously missing) new setting.
*/
fun putSetting(setting: AbstractSetting)
/**
* Have the fragment tell the containing Activity that a setting was modified.
*/
fun onSettingChanged()
}

View file

@ -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());
}
}

View file

@ -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());
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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());
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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);
}

View file

@ -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
}

View file

@ -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);
}
}
}

View file

@ -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
}
}

View file

@ -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());
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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);
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -1,344 +0,0 @@
package org.citra.citra_emu.features.settings.utils;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.documentfile.provider.DocumentFile;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.settings.model.FloatSetting;
import org.citra.citra_emu.features.settings.model.IntSetting;
import org.citra.citra_emu.features.settings.model.Setting;
import org.citra.citra_emu.features.settings.model.SettingSection;
import org.citra.citra_emu.features.settings.model.Settings;
import org.citra.citra_emu.features.settings.model.StringSetting;
import org.citra.citra_emu.features.settings.ui.SettingsActivityView;
import org.citra.citra_emu.utils.BiMap;
import org.citra.citra_emu.utils.DirectoryInitialization;
import org.citra.citra_emu.utils.Log;
import org.ini4j.Wini;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* Contains static methods for interacting with .ini files in which settings are stored.
*/
public final class SettingsFile {
public static final String FILE_NAME_CONFIG = "config";
public static final String KEY_CPU_JIT = "use_cpu_jit";
public static final String KEY_DESIGN = "design";
public static final String KEY_GRAPHICS_API = "graphics_api";
public static final String KEY_SPIRV_SHADER_GEN = "spirv_shader_gen";
public static final String KEY_ASYNC_SHADERS = "async_shader_compilation";
public static final String KEY_RENDERER_DEBUG = "renderer_debug";
public static final String KEY_HW_SHADER = "use_hw_shader";
public static final String KEY_SHADERS_ACCURATE_MUL = "shaders_accurate_mul";
public static final String KEY_USE_SHADER_JIT = "use_shader_jit";
public static final String KEY_USE_DISK_SHADER_CACHE = "use_disk_shader_cache";
public static final String KEY_USE_VSYNC = "use_vsync_new";
public static final String KEY_RESOLUTION_FACTOR = "resolution_factor";
public static final String KEY_FRAME_LIMIT_ENABLED = "use_frame_limit";
public static final String KEY_FRAME_LIMIT = "frame_limit";
public static final String KEY_BACKGROUND_RED = "bg_red";
public static final String KEY_BACKGROUND_BLUE = "bg_blue";
public static final String KEY_BACKGROUND_GREEN = "bg_green";
public static final String KEY_RENDER_3D = "render_3d";
public static final String KEY_FACTOR_3D = "factor_3d";
public static final String KEY_PP_SHADER_NAME = "pp_shader_name";
public static final String KEY_FILTER_MODE = "filter_mode";
public static final String KEY_TEXTURE_FILTER_NAME = "texture_filter_name";
public static final String KEY_USE_ASYNCHRONOUS_GPU_EMULATION = "use_asynchronous_gpu_emulation";
public static final String KEY_LAYOUT_OPTION = "layout_option";
public static final String KEY_SWAP_SCREEN = "swap_screen";
public static final String KEY_CARDBOARD_SCREEN_SIZE = "cardboard_screen_size";
public static final String KEY_CARDBOARD_X_SHIFT = "cardboard_x_shift";
public static final String KEY_CARDBOARD_Y_SHIFT = "cardboard_y_shift";
public static final String KEY_DUMP_TEXTURES = "dump_textures";
public static final String KEY_CUSTOM_TEXTURES = "custom_textures";
public static final String KEY_PRELOAD_TEXTURES = "preload_textures";
public static final String KEY_ASYNC_CUSTOM_LOADING = "async_custom_loading";
public static final String KEY_AUDIO_OUTPUT_TYPE = "output_type";
public static final String KEY_ENABLE_AUDIO_STRETCHING = "enable_audio_stretching";
public static final String KEY_VOLUME = "volume";
public static final String KEY_AUDIO_INPUT_TYPE = "input_type";
public static final String KEY_USE_VIRTUAL_SD = "use_virtual_sd";
public static final String KEY_IS_NEW_3DS = "is_new_3ds";
public static final String KEY_REGION_VALUE = "region_value";
public static final String KEY_LANGUAGE = "language";
public static final String KEY_PLUGIN_LOADER = "plugin_loader";
public static final String KEY_ALLOW_PLUGIN_LOADER = "allow_plugin_loader";
public static final String KEY_INIT_CLOCK = "init_clock";
public static final String KEY_INIT_TIME = "init_time";
public static final String KEY_BUTTON_A = "button_a";
public static final String KEY_BUTTON_B = "button_b";
public static final String KEY_BUTTON_X = "button_x";
public static final String KEY_BUTTON_Y = "button_y";
public static final String KEY_BUTTON_SELECT = "button_select";
public static final String KEY_BUTTON_START = "button_start";
public static final String KEY_BUTTON_UP = "button_up";
public static final String KEY_BUTTON_DOWN = "button_down";
public static final String KEY_BUTTON_LEFT = "button_left";
public static final String KEY_BUTTON_RIGHT = "button_right";
public static final String KEY_BUTTON_L = "button_l";
public static final String KEY_BUTTON_R = "button_r";
public static final String KEY_BUTTON_ZL = "button_zl";
public static final String KEY_BUTTON_ZR = "button_zr";
public static final String KEY_CIRCLEPAD_AXIS_VERTICAL = "circlepad_axis_vertical";
public static final String KEY_CIRCLEPAD_AXIS_HORIZONTAL = "circlepad_axis_horizontal";
public static final String KEY_CSTICK_AXIS_VERTICAL = "cstick_axis_vertical";
public static final String KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal";
public static final String KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical";
public static final String KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal";
public static final String KEY_CIRCLEPAD_UP = "circlepad_up";
public static final String KEY_CIRCLEPAD_DOWN = "circlepad_down";
public static final String KEY_CIRCLEPAD_LEFT = "circlepad_left";
public static final String KEY_CIRCLEPAD_RIGHT = "circlepad_right";
public static final String KEY_CSTICK_UP = "cstick_up";
public static final String KEY_CSTICK_DOWN = "cstick_down";
public static final String KEY_CSTICK_LEFT = "cstick_left";
public static final String KEY_CSTICK_RIGHT = "cstick_right";
public static final String KEY_CAMERA_OUTER_RIGHT_NAME = "camera_outer_right_name";
public static final String KEY_CAMERA_OUTER_RIGHT_CONFIG = "camera_outer_right_config";
public static final String KEY_CAMERA_OUTER_RIGHT_FLIP = "camera_outer_right_flip";
public static final String KEY_CAMERA_OUTER_LEFT_NAME = "camera_outer_left_name";
public static final String KEY_CAMERA_OUTER_LEFT_CONFIG = "camera_outer_left_config";
public static final String KEY_CAMERA_OUTER_LEFT_FLIP = "camera_outer_left_flip";
public static final String KEY_CAMERA_INNER_NAME = "camera_inner_name";
public static final String KEY_CAMERA_INNER_CONFIG = "camera_inner_config";
public static final String KEY_CAMERA_INNER_FLIP = "camera_inner_flip";
public static final String KEY_LOG_FILTER = "log_filter";
private static BiMap<String, String> sectionsMap = new BiMap<>();
static {
//TODO: Add members to sectionsMap when game-specific settings are added
}
private SettingsFile() {
}
/**
* Reads a given .ini file from disk and returns it as a HashMap of Settings, themselves
* effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
* failed.
*
* @param ini The ini file to load the settings from
* @param isCustomGame
* @param view The current view.
* @return An Observable that emits a HashMap of the file's contents, then completes.
*/
static HashMap<String, SettingSection> readFile(final DocumentFile ini, boolean isCustomGame, SettingsActivityView view) {
HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap();
BufferedReader reader = null;
try {
Context context = CitraApplication.Companion.getAppContext();
InputStream inputStream = context.getContentResolver().openInputStream(ini.getUri());
reader = new BufferedReader(new InputStreamReader(inputStream));
SettingSection current = null;
for (String line; (line = reader.readLine()) != null; ) {
if (line.startsWith("[") && line.endsWith("]")) {
current = sectionFromLine(line, isCustomGame);
sections.put(current.getName(), current);
} else if ((current != null)) {
Setting setting = settingFromLine(current, line);
if (setting != null) {
current.putSetting(setting);
}
}
}
} catch (FileNotFoundException e) {
Log.error("[SettingsFile] File not found: " + ini.getUri() + e.getMessage());
if (view != null)
view.onSettingsFileNotFound();
} catch (IOException e) {
Log.error("[SettingsFile] Error reading from: " + ini.getUri() + e.getMessage());
if (view != null)
view.onSettingsFileNotFound();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
Log.error("[SettingsFile] Error closing: " + ini.getUri() + e.getMessage());
}
}
}
return sections;
}
public static HashMap<String, SettingSection> readFile(final String fileName, SettingsActivityView view) {
return readFile(getSettingsFile(fileName), false, view);
}
/**
* Reads a given .ini file from disk and returns it as a HashMap of SettingSections, themselves
* effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
* failed.
*
* @param gameId the id of the game to load it's settings.
* @param view The current view.
*/
public static HashMap<String, SettingSection> readCustomGameSettings(final String gameId, SettingsActivityView view) {
return readFile(getCustomGameSettingsFile(gameId), true, view);
}
/**
* Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error
* telling why it failed.
*
* @param fileName The target filename without a path or extension.
* @param sections The HashMap containing the Settings we want to serialize.
* @param view The current view.
*/
public static void saveFile(final String fileName, TreeMap<String, SettingSection> sections,
SettingsActivityView view) {
DocumentFile ini = getSettingsFile(fileName);
try {
Context context = CitraApplication.Companion.getAppContext();
InputStream inputStream = context.getContentResolver().openInputStream(ini.getUri());
Wini writer = new Wini(inputStream);
Set<String> keySet = sections.keySet();
for (String key : keySet) {
SettingSection section = sections.get(key);
writeSection(writer, section);
}
inputStream.close();
OutputStream outputStream = context.getContentResolver().openOutputStream(ini.getUri(), "wt");
writer.store(outputStream);
outputStream.flush();
outputStream.close();
} catch (IOException e) {
Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.getMessage());
view.showToastMessage(CitraApplication.Companion.getAppContext().getString(R.string.error_saving, fileName, e.getMessage()), false);
}
}
private static String mapSectionNameFromIni(String generalSectionName) {
if (sectionsMap.getForward(generalSectionName) != null) {
return sectionsMap.getForward(generalSectionName);
}
return generalSectionName;
}
private static String mapSectionNameToIni(String generalSectionName) {
if (sectionsMap.getBackward(generalSectionName) != null) {
return sectionsMap.getBackward(generalSectionName);
}
return generalSectionName;
}
public static DocumentFile getSettingsFile(String fileName) {
DocumentFile root = DocumentFile.fromTreeUri(CitraApplication.Companion.getAppContext(), Uri.parse(DirectoryInitialization.INSTANCE.getUserDirectory()));
DocumentFile configDirectory = root.findFile("config");
return configDirectory.findFile(fileName + ".ini");
}
private static DocumentFile getCustomGameSettingsFile(String gameId) {
DocumentFile root = DocumentFile.fromTreeUri(CitraApplication.Companion.getAppContext(), Uri.parse(DirectoryInitialization.INSTANCE.getUserDirectory()));
DocumentFile configDirectory = root.findFile("GameSettings");
return configDirectory.findFile(gameId + ".ini");
}
private static SettingSection sectionFromLine(String line, boolean isCustomGame) {
String sectionName = line.substring(1, line.length() - 1);
if (isCustomGame) {
sectionName = mapSectionNameToIni(sectionName);
}
return new SettingSection(sectionName);
}
/**
* For a line of text, determines what type of data is being represented, and returns
* a Setting object containing this data.
*
* @param current The section currently being parsed by the consuming method.
* @param line The line of text being parsed.
* @return A typed Setting containing the key/value contained in the line.
*/
private static Setting settingFromLine(SettingSection current, String line) {
String[] splitLine = line.split("=");
if (splitLine.length != 2) {
Log.warning("Skipping invalid config line \"" + line + "\"");
return null;
}
String key = splitLine[0].trim();
String value = splitLine[1].trim();
if (value.isEmpty()) {
Log.warning("Skipping null value in config line \"" + line + "\"");
return null;
}
try {
int valueAsInt = Integer.parseInt(value);
return new IntSetting(key, current.getName(), valueAsInt);
} catch (NumberFormatException ex) {
}
try {
float valueAsFloat = Float.parseFloat(value);
return new FloatSetting(key, current.getName(), valueAsFloat);
} catch (NumberFormatException ex) {
}
return new StringSetting(key, current.getName(), value);
}
/**
* Writes the contents of a Section HashMap to disk.
*
* @param parser A Wini pointed at a file on disk.
* @param section A section containing settings to be written to the file.
*/
private static void writeSection(Wini parser, SettingSection section) {
// Write the section header.
String header = section.getName();
// Write this section's values.
HashMap<String, Setting> settings = section.getSettings();
Set<String> keySet = settings.keySet();
for (String key : keySet) {
Setting setting = settings.get(key);
parser.put(header, setting.getKey(), setting.getValueAsString());
}
}
}

View file

@ -0,0 +1,258 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.utils
import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R
import org.citra.citra_emu.features.settings.model.AbstractSetting
import org.citra.citra_emu.features.settings.model.BooleanSetting
import org.citra.citra_emu.features.settings.model.FloatSetting
import org.citra.citra_emu.features.settings.model.IntSetting
import org.citra.citra_emu.features.settings.model.ScaledFloatSetting
import org.citra.citra_emu.features.settings.model.SettingSection
import org.citra.citra_emu.features.settings.model.Settings.SettingsSectionMap
import org.citra.citra_emu.features.settings.model.StringSetting
import org.citra.citra_emu.features.settings.ui.SettingsActivityView
import org.citra.citra_emu.utils.BiMap
import org.citra.citra_emu.utils.DirectoryInitialization.userDirectory
import org.citra.citra_emu.utils.Log
import org.ini4j.Wini
import java.io.*
import java.lang.NumberFormatException
import java.util.*
/**
* Contains static methods for interacting with .ini files in which settings are stored.
*/
object SettingsFile {
const val FILE_NAME_CONFIG = "config"
private var sectionsMap = BiMap<String?, String?>()
/**
* Reads a given .ini file from disk and returns it as a HashMap of Settings, themselves
* effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
* failed.
*
* @param ini The ini file to load the settings from
* @param isCustomGame
* @param view The current view.
* @return An Observable that emits a HashMap of the file's contents, then completes.
*/
fun readFile(
ini: DocumentFile,
isCustomGame: Boolean,
view: SettingsActivityView?
): HashMap<String, SettingSection?> {
val sections: HashMap<String, SettingSection?> = SettingsSectionMap()
var reader: BufferedReader? = null
try {
val context: Context = CitraApplication.appContext
val inputStream = context.contentResolver.openInputStream(ini.uri)
reader = BufferedReader(InputStreamReader(inputStream))
var current: SettingSection? = null
var line: String?
while (reader.readLine().also { line = it } != null) {
if (line!!.startsWith("[") && line!!.endsWith("]")) {
current = sectionFromLine(line!!, isCustomGame)
sections[current.name] = current
} else if (current != null) {
val setting = settingFromLine(line!!)
if (setting != null) {
current.putSetting(setting)
}
}
}
} catch (e: FileNotFoundException) {
Log.error("[SettingsFile] File not found: " + ini.uri + e.message)
view?.onSettingsFileNotFound()
} catch (e: IOException) {
Log.error("[SettingsFile] Error reading from: " + ini.uri + e.message)
view?.onSettingsFileNotFound()
} finally {
if (reader != null) {
try {
reader.close()
} catch (e: IOException) {
Log.error("[SettingsFile] Error closing: " + ini.uri + e.message)
}
}
}
return sections
}
fun readFile(fileName: String, view: SettingsActivityView?): HashMap<String, SettingSection?> {
return readFile(getSettingsFile(fileName), false, view)
}
fun readFile(fileName: String): HashMap<String, SettingSection?> = readFile(fileName, null)
/**
* Reads a given .ini file from disk and returns it as a HashMap of SettingSections, themselves
* effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
* failed.
*
* @param gameId the id of the game to load it's settings.
* @param view The current view.
*/
fun readCustomGameSettings(
gameId: String,
view: SettingsActivityView?
): HashMap<String, SettingSection?> {
return readFile(getCustomGameSettingsFile(gameId), true, view)
}
/**
* Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error
* telling why it failed.
*
* @param fileName The target filename without a path or extension.
* @param sections The HashMap containing the Settings we want to serialize.
* @param view The current view.
*/
fun saveFile(
fileName: String,
sections: TreeMap<String, SettingSection?>,
view: SettingsActivityView
) {
val ini = getSettingsFile(fileName)
try {
val context: Context = CitraApplication.appContext
val inputStream = context.contentResolver.openInputStream(ini.uri)
val writer = Wini(inputStream)
val keySet: Set<String> = sections.keys
for (key in keySet) {
val section = sections[key]
writeSection(writer, section!!)
}
inputStream!!.close()
val outputStream = context.contentResolver.openOutputStream(ini.uri, "wt")
writer.store(outputStream)
outputStream!!.flush()
outputStream.close()
} catch (e: Exception) {
Log.error("[SettingsFile] File not found: $fileName.ini: ${e.message}")
view.showToastMessage(
CitraApplication.appContext
.getString(R.string.error_saving, fileName, e.message), false
)
}
}
private fun mapSectionNameFromIni(generalSectionName: String): String? {
return if (sectionsMap.getForward(generalSectionName) != null) {
sectionsMap.getForward(generalSectionName)
} else {
generalSectionName
}
}
private fun mapSectionNameToIni(generalSectionName: String): String {
return if (sectionsMap.getBackward(generalSectionName) != null) {
sectionsMap.getBackward(generalSectionName).toString()
} else {
generalSectionName
}
}
fun getSettingsFile(fileName: String): DocumentFile {
val root = DocumentFile.fromTreeUri(CitraApplication.appContext, Uri.parse(userDirectory))
val configDirectory = root!!.findFile("config")
return configDirectory!!.findFile("$fileName.ini")!!
}
private fun getCustomGameSettingsFile(gameId: String): DocumentFile {
val root = DocumentFile.fromTreeUri(CitraApplication.appContext, Uri.parse(userDirectory))
val configDirectory = root!!.findFile("GameSettings")
return configDirectory!!.findFile("$gameId.ini")!!
}
private fun sectionFromLine(line: String, isCustomGame: Boolean): SettingSection {
var sectionName: String = line.substring(1, line.length - 1)
if (isCustomGame) {
sectionName = mapSectionNameToIni(sectionName)
}
return SettingSection(sectionName)
}
/**
* For a line of text, determines what type of data is being represented, and returns
* a Setting object containing this data.
*
* @param line The line of text being parsed.
* @return A typed Setting containing the key/value contained in the line.
*/
private fun settingFromLine(line: String): AbstractSetting? {
val splitLine = line.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (splitLine.size != 2) {
return null
}
val key = splitLine[0].trim { it <= ' ' }
val value = splitLine[1].trim { it <= ' ' }
if (value.isEmpty()) {
return null
}
val booleanSetting = BooleanSetting.from(key)
if (booleanSetting != null) {
booleanSetting.boolean = value.toBoolean()
return booleanSetting
}
val intSetting = IntSetting.from(key)
if (intSetting != null) {
try {
intSetting.int = value.toInt()
} catch (e: NumberFormatException) {
intSetting.int = if (value.toBoolean()) 1 else 0
}
return intSetting
}
val scaledFloatSetting = ScaledFloatSetting.from(key)
if (scaledFloatSetting != null) {
scaledFloatSetting.float = value.toFloat() * scaledFloatSetting.scale
return scaledFloatSetting
}
val floatSetting = FloatSetting.from(key)
if (floatSetting != null) {
floatSetting.float = value.toFloat()
return floatSetting
}
val stringSetting = StringSetting.from(key)
if (stringSetting != null) {
stringSetting.string = value
return stringSetting
}
return null
}
/**
* Writes the contents of a Section HashMap to disk.
*
* @param parser A Wini pointed at a file on disk.
* @param section A section containing settings to be written to the file.
*/
private fun writeSection(parser: Wini, section: SettingSection) {
// Write the section header.
val header = section.name
// Write this section's values.
val settings = section.settings
val keySet: Set<String> = settings.keys
for (key in keySet) {
val setting = settings[key]
parser.put(header, setting!!.key, setting.valueAsString)
}
}
}

View file

@ -28,6 +28,7 @@ import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.R import org.citra.citra_emu.R
import org.citra.citra_emu.adapters.HomeSettingAdapter import org.citra.citra_emu.adapters.HomeSettingAdapter
import org.citra.citra_emu.databinding.FragmentHomeSettingsBinding 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.ui.SettingsActivity
import org.citra.citra_emu.features.settings.utils.SettingsFile import org.citra.citra_emu.features.settings.utils.SettingsFile
import org.citra.citra_emu.model.HomeSetting import org.citra.citra_emu.model.HomeSetting
@ -124,6 +125,12 @@ class HomeSettingsFragment : Fragment() {
{ getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) }, { getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) },
details = homeViewModel.gamesDir 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( HomeSetting(
R.string.about, R.string.about,
R.string.about_description, R.string.about_description,

View file

@ -0,0 +1,208 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.fragments
import android.content.DialogInterface
import android.os.Bundle
import android.view.InputDevice
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.citra.citra_emu.R
import org.citra.citra_emu.databinding.DialogInputBinding
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
import org.citra.citra_emu.utils.Log
import kotlin.math.abs
class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() {
private var _binding: DialogInputBinding? = null
private val binding get() = _binding!!
private var setting: InputBindingSetting? = null
private var onCancel: (() -> Unit)? = null
private var onDismiss: (() -> Unit)? = null
private val previousValues = ArrayList<Float>()
private var prevDeviceId = 0
private var waitingForEvent = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (setting == null) {
dismiss()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = DialogInputBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
BottomSheetBehavior.from<View>(view.parent as View).state =
BottomSheetBehavior.STATE_EXPANDED
isCancelable = false
view.requestFocus()
view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
if (setting!!.isButtonMappingSupported()) {
dialog?.setOnKeyListener { _, _, event -> onKeyEvent(event) }
}
if (setting!!.isAxisMappingSupported()) {
binding.root.setOnGenericMotionListener { _, event -> onMotionEvent(event) }
}
val inputTypeId = when {
setting!!.isCirclePad() -> R.string.controller_circlepad
setting!!.isCStick() -> R.string.controller_c
setting!!.isDPad() -> R.string.controller_dpad
setting!!.isTrigger() -> R.string.controller_trigger
else -> R.string.button
}
binding.textTitle.text =
String.format(
getString(R.string.input_dialog_title),
getString(inputTypeId),
getString(setting!!.nameId)
)
var messageResId: Int = R.string.input_dialog_description
if (setting!!.isAxisMappingSupported() && !setting!!.isTrigger()) {
// Use specialized message for axis left/right or up/down
messageResId = if (setting!!.isHorizontalOrientation()) {
R.string.input_binding_description_horizontal_axis
} else {
R.string.input_binding_description_vertical_axis
}
}
binding.textMessage.text = getString(messageResId)
binding.buttonClear.setOnClickListener {
setting?.removeOldMapping()
dismiss()
}
binding.buttonCancel.setOnClickListener {
onCancel?.invoke()
dismiss()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
onDismiss?.invoke()
}
private fun onKeyEvent(event: KeyEvent): Boolean {
Log.debug("[MotionBottomSheetDialogFragment] Received key event: " + event.action)
return when (event.action) {
KeyEvent.ACTION_UP -> {
setting?.onKeyInput(event)
dismiss()
// Even if we ignore the key, we still consume it. Thus return true regardless.
true
}
else -> false
}
}
private fun onMotionEvent(event: MotionEvent): Boolean {
Log.debug("[MotionBottomSheetDialogFragment] Received motion event: " + event.action)
if (event.source and InputDevice.SOURCE_CLASS_JOYSTICK == 0) return false
if (event.action != MotionEvent.ACTION_MOVE) return false
val input = event.device
val motionRanges = input.motionRanges
if (input.id != prevDeviceId) {
previousValues.clear()
}
prevDeviceId = input.id
val firstEvent = previousValues.isEmpty()
var numMovedAxis = 0
var axisMoveValue = 0.0f
var lastMovedRange: InputDevice.MotionRange? = null
var lastMovedDir = '?'
if (waitingForEvent) {
for (i in motionRanges.indices) {
val range = motionRanges[i]
val axis = range.axis
val origValue = event.getAxisValue(axis)
if (firstEvent) {
previousValues.add(origValue)
} else {
val previousValue = previousValues[i]
// Only handle the axes that are not neutral (more than 0.5)
// but ignore any axis that has a constant value (e.g. always 1)
if (abs(origValue) > 0.5f && origValue != previousValue) {
// It is common to have multiple axes with the same physical input. For example,
// shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE.
// To handle this, we ignore an axis motion that's the exact same as a motion
// we already saw. This way, we ignore axes with two names, but catch the case
// where a joystick is moved in two directions.
// ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html
if (origValue != axisMoveValue) {
axisMoveValue = origValue
numMovedAxis++
lastMovedRange = range
lastMovedDir = if (origValue < 0.0f) '-' else '+'
}
} else if (abs(origValue) < 0.25f && abs(previousValue) > 0.75f) {
// Special case for d-pads (axis value jumps between 0 and 1 without any values
// in between). Without this, the user would need to press the d-pad twice
// due to the first press being caught by the "if (firstEvent)" case further up.
numMovedAxis++
lastMovedRange = range
lastMovedDir = if (previousValue < 0.0f) '-' else '+'
}
}
previousValues[i] = origValue
}
// If only one axis moved, that's the winner.
if (numMovedAxis == 1) {
waitingForEvent = false
setting?.onMotionInput(input, lastMovedRange!!, lastMovedDir)
dismiss()
}
}
return true
}
companion object {
const val TAG = "MotionBottomSheetDialogFragment"
fun newInstance(
setting: InputBindingSetting,
onCancel: () -> Unit,
onDismiss: () -> Unit
): MotionBottomSheetDialogFragment {
val dialog = MotionBottomSheetDialogFragment()
dialog.apply {
this.setting = setting
this.onCancel = onCancel
this.onDismiss = onDismiss
}
return dialog
}
}
}

View file

@ -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"
}
}

View file

@ -25,7 +25,6 @@ import androidx.navigation.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.textfield.MaterialAutoCompleteTextView import com.google.android.material.textfield.MaterialAutoCompleteTextView
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.NativeLibrary 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.activities.EmulationActivity
import org.citra.citra_emu.databinding.FragmentSystemFilesBinding import org.citra.citra_emu.databinding.FragmentSystemFilesBinding
import org.citra.citra_emu.features.settings.model.Settings 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.GamesViewModel
import org.citra.citra_emu.viewmodel.HomeViewModel import org.citra.citra_emu.viewmodel.HomeViewModel
import org.citra.citra_emu.viewmodel.SystemFilesViewModel import org.citra.citra_emu.viewmodel.SystemFilesViewModel
@ -74,7 +74,7 @@ class SystemFilesFragment : Fragment() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
NativeLibrary.loadSystemConfig() SystemSaveGame.load()
} }
override fun onCreateView( override fun onCreateView(
@ -149,15 +149,15 @@ class SystemFilesFragment : Fragment() {
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
NativeLibrary.saveSystemConfig() SystemSaveGame.save()
} }
private fun reloadUi() { private fun reloadUi() {
val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
binding.switchRunSystemSetup.isChecked = NativeLibrary.getIsSystemSetupNeeded() binding.switchRunSystemSetup.isChecked = SystemSaveGame.getIsSystemSetupNeeded()
binding.switchRunSystemSetup.setOnCheckedChangeListener { _, isChecked -> binding.switchRunSystemSetup.setOnCheckedChangeListener { _, isChecked ->
NativeLibrary.setSystemSetupNeeded(isChecked) SystemSaveGame.setSystemSetupNeeded(isChecked)
} }
val showHomeApps = preferences.getBoolean(Settings.PREF_SHOW_HOME_APPS, false) val showHomeApps = preferences.getBoolean(Settings.PREF_SHOW_HOME_APPS, false)

View file

@ -41,6 +41,7 @@ import org.citra.citra_emu.activities.EmulationActivity
import org.citra.citra_emu.contracts.OpenFileResultContract import org.citra.citra_emu.contracts.OpenFileResultContract
import org.citra.citra_emu.databinding.ActivityMainBinding import org.citra.citra_emu.databinding.ActivityMainBinding
import org.citra.citra_emu.features.settings.model.Settings 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.ui.SettingsActivity
import org.citra.citra_emu.features.settings.utils.SettingsFile import org.citra.citra_emu.features.settings.utils.SettingsFile
import org.citra.citra_emu.fragments.SelectUserDirectoryDialogFragment 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.GamesViewModel
import org.citra.citra_emu.viewmodel.HomeViewModel import org.citra.citra_emu.viewmodel.HomeViewModel
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity(), ThemeProvider {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private val homeViewModel: HomeViewModel by viewModels() private val homeViewModel: HomeViewModel by viewModels()
private val gamesViewModel: GamesViewModel by viewModels() private val gamesViewModel: GamesViewModel by viewModels()
private val settingsViewModel: SettingsViewModel by viewModels()
override var themeId: Int = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen() val splashScreen = installSplashScreen()
@ -67,6 +71,11 @@ class MainActivity : AppCompatActivity() {
PermissionsHandler.hasWriteAccess(this) PermissionsHandler.hasWriteAccess(this)
} }
if (PermissionsHandler.hasWriteAccess(applicationContext) &&
DirectoryInitialization.areCitraDirectoriesReady()) {
settingsViewModel.settings.loadSettings()
}
ThemeUtil.setTheme(this) ThemeUtil.setTheme(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -155,6 +164,8 @@ class MainActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
checkUserPermissions() checkUserPermissions()
ThemeUtil.setCorrectTheme(this)
super.onResume() super.onResume()
} }
@ -163,6 +174,11 @@ class MainActivity : AppCompatActivity() {
super.onDestroy() super.onDestroy()
} }
override fun setTheme(resId: Int) {
super.setTheme(resId)
themeId = resId
}
private fun checkUserPermissions() { private fun checkUserPermissions() {
val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext) val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true) .getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)

View file

@ -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
}

View file

@ -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() }
}
}

View file

@ -16,6 +16,7 @@ import androidx.preference.PreferenceManager
import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.R import org.citra.citra_emu.R
import org.citra.citra_emu.features.settings.model.Settings import org.citra.citra_emu.features.settings.model.Settings
import org.citra.citra_emu.ui.main.ThemeProvider
import kotlin.math.roundToInt import kotlin.math.roundToInt
object ThemeUtil { object ThemeUtil {
@ -26,6 +27,20 @@ object ThemeUtil {
fun setTheme(activity: AppCompatActivity) { fun setTheme(activity: AppCompatActivity) {
setThemeMode(activity) 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) { fun setThemeMode(activity: AppCompatActivity) {
@ -64,6 +79,14 @@ object ThemeUtil {
windowController.isAppearanceLightNavigationBars = false windowController.isAppearanceLightNavigationBars = false
} }
fun setCorrectTheme(activity: AppCompatActivity) {
val currentTheme = (activity as ThemeProvider).themeId
setTheme(activity)
if (currentTheme != (activity as ThemeProvider).themeId) {
activity.recreate()
}
}
@ColorInt @ColorInt
fun getColorWithOpacity(@ColorInt color: Int, alphaFactor: Float): Int { fun getColorWithOpacity(@ColorInt color: Int, alphaFactor: Float): Int {
return Color.argb( return Color.argb(

View file

@ -31,6 +31,7 @@ add_library(citra-android SHARED
native.cpp native.cpp
ndk_motion.cpp ndk_motion.cpp
ndk_motion.h ndk_motion.h
system_save_game.cpp
) )
target_link_libraries(citra-android PRIVATE audio_core citra_common citra_core input_common network) target_link_libraries(citra-android PRIVATE audio_core citra_common citra_core input_common network)

View file

@ -75,13 +75,6 @@ static const std::array<int, Settings::NativeAnalog::NumAnalogs> default_analogs
InputManager::N3DS_STICK_C, InputManager::N3DS_STICK_C,
}}; }};
void Config::UpdateCFG() {
std::shared_ptr<Service::CFG::Module> cfg = std::make_shared<Service::CFG::Module>();
cfg->SetSystemLanguage(static_cast<Service::CFG::SystemLanguage>(
sdl2_config->GetInteger("System", "language", Service::CFG::SystemLanguage::LANGUAGE_EN)));
cfg->UpdateConfigNANDSavegame();
}
template <> template <>
void Config::ReadSetting(const std::string& group, Settings::Setting<std::string>& setting) { void Config::ReadSetting(const std::string& group, Settings::Setting<std::string>& setting) {
std::string setting_value = sdl2_config->Get(group, setting.GetLabel(), setting.GetDefault()); 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.region_value);
ReadSetting("System", Settings::values.init_clock); ReadSetting("System", Settings::values.init_clock);
{ {
std::tm t; std::string time = sdl2_config->GetString("System", "init_time", "946681277");
t.tm_sec = 1; try {
t.tm_min = 0; Settings::values.init_time = std::stoll(time);
t.tm_hour = 0; } catch (...) {
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");
} }
Settings::values.init_time =
std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::from_time_t(std::mktime(&t)).time_since_epoch())
.count();
} }
ReadSetting("System", Settings::values.plugin_loader_enabled); ReadSetting("System", Settings::values.plugin_loader_enabled);
ReadSetting("System", Settings::values.allow_plugin_loader); 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"); 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_username = sdl2_config->GetString("WebService", "citra_username", "");
NetSettings::values.citra_token = sdl2_config->GetString("WebService", "citra_token", ""); NetSettings::values.citra_token = sdl2_config->GetString("WebService", "citra_token", "");
// Update CFG file based on settings
UpdateCFG();
} }
void Config::Reload() { void Config::Reload() {

View file

@ -17,7 +17,6 @@ private:
bool LoadINI(const std::string& default_contents = "", bool retry = true); bool LoadINI(const std::string& default_contents = "", bool retry = true);
void ReadValues(); void ReadValues();
void UpdateCFG();
public: public:
Config(); Config();

View file

@ -64,7 +64,6 @@ ANativeWindow* s_surf;
std::shared_ptr<Common::DynamicLibrary> vulkan_library{}; std::shared_ptr<Common::DynamicLibrary> vulkan_library{};
std::unique_ptr<EmuWindow_Android> window; std::unique_ptr<EmuWindow_Android> window;
std::shared_ptr<Service::CFG::Module> cfg;
std::atomic<bool> stop_run{true}; std::atomic<bool> stop_run{true};
std::atomic<bool> pause_emulation{false}; std::atomic<bool> pause_emulation{false};
@ -732,29 +731,4 @@ void Java_org_citra_citra_1emu_NativeLibrary_logDeviceInfo([[maybe_unused]] JNIE
LOG_INFO(Frontend, "Host OS: Android API level {}", android_get_device_api_level()); LOG_INFO(Frontend, "Host OS: Android API level {}", android_get_device_api_level());
} }
void Java_org_citra_citra_1emu_NativeLibrary_loadSystemConfig([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jobject obj) {
if (Core::System::GetInstance().IsPoweredOn()) {
cfg = Service::CFG::GetModule(Core::System::GetInstance());
} else {
cfg = std::make_shared<Service::CFG::Module>();
}
}
void Java_org_citra_citra_1emu_NativeLibrary_saveSystemConfig([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jobject obj) {
cfg->UpdateConfigNANDSavegame();
}
void Java_org_citra_citra_1emu_NativeLibrary_setSystemSetupNeeded([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jobject obj,
jboolean needed) {
cfg->SetSystemSetupNeeded(needed);
}
jboolean Java_org_citra_citra_1emu_NativeLibrary_getIsSystemSetupNeeded(
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
return cfg->IsSystemSetupNeeded();
}
} // extern "C" } // extern "C"

View file

@ -0,0 +1,122 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <common/string_util.h>
#include <core/core.h>
#include <core/hle/service/cfg/cfg.h>
#include <core/hle/service/ptm/ptm.h>
#include "android_common/android_common.h"
std::shared_ptr<Service::CFG::Module> cfg;
extern "C" {
void Java_org_citra_citra_1emu_utils_SystemSaveGame_save([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jobject obj) {
cfg->UpdateConfigNANDSavegame();
}
void Java_org_citra_citra_1emu_utils_SystemSaveGame_load([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jobject obj) {
if (Core::System::GetInstance().IsPoweredOn()) {
cfg = Service::CFG::GetModule(Core::System::GetInstance());
} else {
cfg = std::make_shared<Service::CFG::Module>();
}
}
jboolean Java_org_citra_citra_1emu_utils_SystemSaveGame_getIsSystemSetupNeeded(
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
return cfg->IsSystemSetupNeeded();
}
void Java_org_citra_citra_1emu_utils_SystemSaveGame_setSystemSetupNeeded(
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj, jboolean needed) {
cfg->SetSystemSetupNeeded(needed);
}
jstring Java_org_citra_citra_1emu_utils_SystemSaveGame_getUsername([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jobject obj) {
return ToJString(env, Common::UTF16ToUTF8(cfg->GetUsername()));
}
void Java_org_citra_citra_1emu_utils_SystemSaveGame_setUsername([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jobject obj,
jstring username) {
cfg->SetUsername(Common::UTF8ToUTF16(GetJString(env, username)));
}
jshortArray Java_org_citra_citra_1emu_utils_SystemSaveGame_getBirthday(
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
jshortArray jbirthdayArray = env->NewShortArray(2);
auto birthday = cfg->GetBirthday();
jshort birthdayArray[2]{static_cast<jshort>(get<0>(birthday)),
static_cast<jshort>(get<1>(birthday))};
env->SetShortArrayRegion(jbirthdayArray, 0, 2, birthdayArray);
return jbirthdayArray;
}
void Java_org_citra_citra_1emu_utils_SystemSaveGame_setBirthday([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jobject obj,
jshort jmonth, jshort jday) {
cfg->SetBirthday(static_cast<u8>(jmonth), static_cast<u8>(jday));
}
jint Java_org_citra_citra_1emu_utils_SystemSaveGame_getSystemLanguage(
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
return cfg->GetSystemLanguage();
}
void Java_org_citra_citra_1emu_utils_SystemSaveGame_setSystemLanguage([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jobject obj,
jint jsystemLanguage) {
cfg->SetSystemLanguage(static_cast<Service::CFG::SystemLanguage>(jsystemLanguage));
}
jint Java_org_citra_citra_1emu_utils_SystemSaveGame_getSoundOutputMode(
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
return cfg->GetSoundOutputMode();
}
void Java_org_citra_citra_1emu_utils_SystemSaveGame_setSoundOutputMode([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jobject obj,
jint jmode) {
cfg->SetSoundOutputMode(static_cast<Service::CFG::SoundOutputMode>(jmode));
}
jshort Java_org_citra_citra_1emu_utils_SystemSaveGame_getCountryCode([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jobject obj) {
return cfg->GetCountryCode();
}
void Java_org_citra_citra_1emu_utils_SystemSaveGame_setCountryCode([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jobject obj,
jshort jmode) {
cfg->SetCountryCode(static_cast<u8>(jmode));
}
jint Java_org_citra_citra_1emu_utils_SystemSaveGame_getPlayCoins([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jobject obj) {
return Service::PTM::Module::GetPlayCoins();
}
void Java_org_citra_citra_1emu_utils_SystemSaveGame_setPlayCoins([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jobject obj,
jint jcoins) {
Service::PTM::Module::SetPlayCoins(static_cast<u16>(jcoins));
}
jlong Java_org_citra_citra_1emu_utils_SystemSaveGame_getConsoleId([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jobject obj) {
return cfg->GetConsoleUniqueId();
}
void Java_org_citra_citra_1emu_utils_SystemSaveGame_regenerateConsoleId(
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
const auto [random_number, console_id] = cfg->GenerateConsoleUniqueId();
cfg->SetConsoleUniqueId(random_number, console_id);
cfg->UpdateConfigNANDSavegame();
}
} // extern "C"

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M12,2C6.49,2 2,6.49 2,12s4.49,10 10,10c1.38,0 2.5,-1.12 2.5,-2.5c0,-0.61 -0.23,-1.2 -0.64,-1.67c-0.08,-0.1 -0.13,-0.21 -0.13,-0.33c0,-0.28 0.22,-0.5 0.5,-0.5H16c3.31,0 6,-2.69 6,-6C22,6.04 17.51,2 12,2zM17.5,13c-0.83,0 -1.5,-0.67 -1.5,-1.5c0,-0.83 0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5C19,12.33 18.33,13 17.5,13zM14.5,9C13.67,9 13,8.33 13,7.5C13,6.67 13.67,6 14.5,6S16,6.67 16,7.5C16,8.33 15.33,9 14.5,9zM5,11.5C5,10.67 5.67,10 6.5,10S8,10.67 8,11.5C8,12.33 7.33,13 6.5,13S5,12.33 5,11.5zM11,7.5C11,8.33 10.33,9 9.5,9S8,8.33 8,7.5C8,6.67 8.67,6 9.5,6S11,6.67 11,7.5z" />
</vector>

View file

@ -1,20 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinator_main"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator_settings"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:background="?attr/colorSurface">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_settings" android:id="@+id/appbar_settings"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true"
app:elevation="0dp">
<com.google.android.material.appbar.CollapsingToolbarLayout
style="?attr/collapsingToolbarLayoutMediumStyle"
android:id="@+id/toolbar_settings_layout"
android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_settings" android:id="@+id/toolbar_settings"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" /> android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
@ -22,6 +35,16 @@
android:id="@+id/frame_content" android:id="@+id/frame_content"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginHorizontal="12dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" /> app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<View
android:id="@+id/navigation_bar_shade"
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@android:color/transparent"
android:clickable="false"
android:focusable="false"
android:layout_gravity="bottom|center_horizontal" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp"
android:paddingBottom="24dp"
android:defaultFocusHighlightEnabled="false"
android:focusable="true"
android:focusableInTouchMode="true"
android:focusedByDefault="true"
android:orientation="vertical">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_title"
style="@style/TextAppearance.Material3.HeadlineSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/start" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_message"
style="@style/TextAppearance.Material3.BodyLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
tools:text="@string/input_binding_description" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<Button
android:id="@+id/button_cancel"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_weight="1"
android:focusable="false"
android:text="@android:string/cancel" />
<Button
android:id="@+id/button_clear"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_weight="1"
android:focusable="false"
android:text="@string/clear" />
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.textfield.TextInputLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingHorizontal="24dp"
tools:hint="@string/cheats_name"
app:errorEnabled="true"
app:layout_constraintBottom_toTopOf="@id/edit_notes"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_text_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="no"
android:inputType="text"
android:minHeight="48dp"
android:textAlignment="viewStart" />
</com.google.android.material.textfield.TextInputLayout>

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -8,34 +10,43 @@
android:focusable="true" android:focusable="true"
android:gravity="center_vertical" android:gravity="center_vertical"
android:minHeight="72dp" android:minHeight="72dp"
android:paddingTop="@dimen/spacing_large" android:padding="@dimen/spacing_large">
android:paddingBottom="@dimen/spacing_large">
<TextView <LinearLayout
android:id="@+id/text_setting_name" android:layout_width="match_parent"
style="@style/TextAppearance.AppCompat.Headline"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentStart="true" android:orientation="vertical">
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true" <com.google.android.material.textview.MaterialTextView
android:layout_marginStart="@dimen/spacing_large" android:id="@+id/text_setting_name"
android:layout_marginEnd="@dimen/spacing_large" style="@style/TextAppearance.Material3.HeadlineMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="viewStart"
android:textSize="16sp" android:textSize="16sp"
app:lineHeight="22dp"
tools:text="Setting Name" /> tools:text="Setting Name" />
<TextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/text_setting_description" android:id="@+id/text_setting_description"
android:layout_width="wrap_content" style="@style/TextAppearance.Material3.BodySmall"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/text_setting_name"
android:layout_alignStart="@+id/text_setting_name"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_marginStart="@dimen/spacing_large"
android:layout_marginTop="@dimen/spacing_small" android:layout_marginTop="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_large" android:textAlignment="viewStart"
android:visibility="visible"
tools:text="@string/app_disclaimer" /> tools:text="@string/app_disclaimer" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_setting_value"
style="@style/TextAppearance.Material3.LabelMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_small"
android:textAlignment="viewStart"
android:textStyle="bold"
android:visibility="gone"
tools:text="1x" />
</LinearLayout>
</RelativeLayout> </RelativeLayout>

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:minHeight="72dp"
android:paddingVertical="@dimen/spacing_large"
android:paddingStart="@dimen/spacing_large"
android:paddingEnd="24dp">
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_widget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/spacing_large"
android:layout_toStartOf="@+id/switch_widget"
android:gravity="center_vertical"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_setting_name"
style="@style/TextAppearance.Material3.HeadlineMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="viewStart"
android:textSize="16sp"
app:lineHeight="22dp"
tools:text="@string/frame_limit_enable" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_setting_description"
style="@style/TextAppearance.Material3.BodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_small"
android:textAlignment="viewStart"
tools:text="@string/frame_limit_enable_description" />
</LinearLayout>
</RelativeLayout>

View file

@ -1,19 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.textview.MaterialTextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="48dp">
<TextView
android:id="@+id/text_header_name" android:id="@+id/text_header_name"
android:layout_width="wrap_content" style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="start|center_vertical"
android:layout_marginStart="@dimen/spacing_large" android:paddingHorizontal="@dimen/spacing_large"
android:layout_marginBottom="@dimen/spacing_small" android:paddingVertical="16dp"
android:layout_marginTop="@dimen/spacing_small" android:focusable="false"
android:clickable="false"
android:textAlignment="viewStart"
android:textColor="?attr/colorPrimary" android:textColor="?attr/colorPrimary"
android:textStyle="bold" android:textStyle="bold"
tools:text="CPU Settings" /> tools:text="CPU Settings" />
</FrameLayout>

View file

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:gravity="center">
<DatePicker
android:id="@+id/date_picker"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:calendarViewShown="false"
android:datePickerMode="spinner"
android:spinnersShown="true" />
<TimePicker
android:id="@+id/time_picker"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:timePickerMode="spinner" />
</LinearLayout>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Citra.Main.MaterialYou" parent="Theme.Citra.Main">
<item name="colorPrimary">@color/m3_sys_color_dynamic_dark_primary</item>
<item name="colorOnPrimary">@color/m3_sys_color_dynamic_dark_on_primary</item>
<item name="colorPrimaryContainer">@color/m3_sys_color_dynamic_dark_primary_container</item>
<item name="colorOnPrimaryContainer">@color/m3_sys_color_dynamic_dark_on_primary_container</item>
<item name="colorSecondary">@color/m3_sys_color_dynamic_dark_secondary</item>
<item name="colorOnSecondary">@color/m3_sys_color_dynamic_dark_on_secondary</item>
<item name="colorSecondaryContainer">@color/m3_sys_color_dynamic_dark_secondary_container</item>
<item name="colorOnSecondaryContainer">@color/m3_sys_color_dynamic_dark_on_secondary_container</item>
<item name="colorTertiary">@color/m3_sys_color_dynamic_dark_tertiary</item>
<item name="colorOnTertiary">@color/m3_sys_color_dynamic_dark_on_tertiary</item>
<item name="colorTertiaryContainer">@color/m3_sys_color_dynamic_dark_tertiary_container</item>
<item name="colorOnTertiaryContainer">@color/m3_sys_color_dynamic_dark_on_tertiary_container</item>
<item name="android:colorBackground">@color/m3_sys_color_dynamic_dark_background</item>
<item name="colorOnBackground">@color/m3_sys_color_dynamic_dark_on_background</item>
<item name="colorSurface">@color/m3_sys_color_dynamic_dark_surface</item>
<item name="colorOnSurface">@color/m3_sys_color_dynamic_dark_on_surface</item>
<item name="colorSurfaceVariant">@color/m3_sys_color_dynamic_dark_surface_variant</item>
<item name="colorOnSurfaceVariant">@color/m3_sys_color_dynamic_dark_on_surface_variant</item>
<item name="colorOutline">@color/m3_sys_color_dynamic_dark_outline</item>
<item name="colorOnSurfaceInverse">@color/m3_sys_color_dynamic_dark_on_surface_variant</item>
<item name="colorSurfaceInverse">@color/m3_sys_color_dynamic_dark_surface_variant</item>
<item name="colorPrimaryInverse">@color/m3_sys_color_dynamic_dark_inverse_primary</item>
</style>
</resources>

Some files were not shown because too many files have changed in this diff Show more