Android UI Overhaul Part 2 (#7147)
This commit is contained in:
parent
33a1f27a99
commit
c17ec1d1aa
@ -515,14 +515,6 @@ object NativeLibrary {
|
||||
*/
|
||||
external fun logDeviceInfo()
|
||||
|
||||
external fun loadSystemConfig()
|
||||
|
||||
external fun saveSystemConfig()
|
||||
|
||||
external fun setSystemSetupNeeded(needed: Boolean)
|
||||
|
||||
external fun getIsSystemSetupNeeded(): Boolean
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun createFile(directory: String, filename: String): Boolean =
|
||||
|
@ -535,7 +535,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
int action;
|
||||
int button = mPreferences.getInt(InputBindingSetting.getInputButtonKey(event.getKeyCode()), event.getKeyCode());
|
||||
int button = mPreferences.getInt(InputBindingSetting.Companion.getInputButtonKey(event.getKeyCode()), event.getKeyCode());
|
||||
|
||||
switch (event.getAction()) {
|
||||
case KeyEvent.ACTION_DOWN:
|
||||
@ -693,8 +693,8 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
int axis = range.getAxis();
|
||||
float origValue = event.getAxisValue(axis);
|
||||
float value = mControllerMappingHelper.scaleAxis(input, axis, origValue);
|
||||
int nextMapping = mPreferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1);
|
||||
int guestOrientation = mPreferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1);
|
||||
int nextMapping = mPreferences.getInt(InputBindingSetting.Companion.getInputAxisButtonKey(axis), -1);
|
||||
int guestOrientation = mPreferences.getInt(InputBindingSetting.Companion.getInputAxisOrientationKey(axis), -1);
|
||||
|
||||
if (nextMapping == -1 || guestOrientation == -1) {
|
||||
// Axis is unmapped
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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();
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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()
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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();
|
||||
}
|
@ -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()
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -28,6 +28,7 @@ import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.adapters.HomeSettingAdapter
|
||||
import org.citra.citra_emu.databinding.FragmentHomeSettingsBinding
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsActivity
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile
|
||||
import org.citra.citra_emu.model.HomeSetting
|
||||
@ -124,6 +125,12 @@ class HomeSettingsFragment : Fragment() {
|
||||
{ getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) },
|
||||
details = homeViewModel.gamesDir
|
||||
),
|
||||
HomeSetting(
|
||||
R.string.preferences_theme,
|
||||
R.string.theme_and_color_description,
|
||||
R.drawable.ic_palette,
|
||||
{ SettingsActivity.launch(requireContext(), Settings.SECTION_THEME, "") }
|
||||
),
|
||||
HomeSetting(
|
||||
R.string.about,
|
||||
R.string.about_description,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -25,7 +25,6 @@ import androidx.navigation.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
@ -33,6 +32,7 @@ import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.activities.EmulationActivity
|
||||
import org.citra.citra_emu.databinding.FragmentSystemFilesBinding
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
import org.citra.citra_emu.utils.SystemSaveGame
|
||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||
import org.citra.citra_emu.viewmodel.SystemFilesViewModel
|
||||
@ -74,7 +74,7 @@ class SystemFilesFragment : Fragment() {
|
||||
super.onCreate(savedInstanceState)
|
||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||
NativeLibrary.loadSystemConfig()
|
||||
SystemSaveGame.load()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
@ -149,15 +149,15 @@ class SystemFilesFragment : Fragment() {
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
NativeLibrary.saveSystemConfig()
|
||||
SystemSaveGame.save()
|
||||
}
|
||||
|
||||
private fun reloadUi() {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
|
||||
binding.switchRunSystemSetup.isChecked = NativeLibrary.getIsSystemSetupNeeded()
|
||||
binding.switchRunSystemSetup.isChecked = SystemSaveGame.getIsSystemSetupNeeded()
|
||||
binding.switchRunSystemSetup.setOnCheckedChangeListener { _, isChecked ->
|
||||
NativeLibrary.setSystemSetupNeeded(isChecked)
|
||||
SystemSaveGame.setSystemSetupNeeded(isChecked)
|
||||
}
|
||||
|
||||
val showHomeApps = preferences.getBoolean(Settings.PREF_SHOW_HOME_APPS, false)
|
||||
|
@ -41,6 +41,7 @@ import org.citra.citra_emu.activities.EmulationActivity
|
||||
import org.citra.citra_emu.contracts.OpenFileResultContract
|
||||
import org.citra.citra_emu.databinding.ActivityMainBinding
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
import org.citra.citra_emu.features.settings.model.SettingsViewModel
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsActivity
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile
|
||||
import org.citra.citra_emu.fragments.SelectUserDirectoryDialogFragment
|
||||
@ -54,11 +55,14 @@ import org.citra.citra_emu.utils.ThemeUtil
|
||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
|
||||
private val homeViewModel: HomeViewModel by viewModels()
|
||||
private val gamesViewModel: GamesViewModel by viewModels()
|
||||
private val settingsViewModel: SettingsViewModel by viewModels()
|
||||
|
||||
override var themeId: Int = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val splashScreen = installSplashScreen()
|
||||
@ -67,6 +71,11 @@ class MainActivity : AppCompatActivity() {
|
||||
PermissionsHandler.hasWriteAccess(this)
|
||||
}
|
||||
|
||||
if (PermissionsHandler.hasWriteAccess(applicationContext) &&
|
||||
DirectoryInitialization.areCitraDirectoriesReady()) {
|
||||
settingsViewModel.settings.loadSettings()
|
||||
}
|
||||
|
||||
ThemeUtil.setTheme(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@ -155,6 +164,8 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
override fun onResume() {
|
||||
checkUserPermissions()
|
||||
|
||||
ThemeUtil.setCorrectTheme(this)
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
@ -163,6 +174,11 @@ class MainActivity : AppCompatActivity() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun setTheme(resId: Int) {
|
||||
super.setTheme(resId)
|
||||
themeId = resId
|
||||
}
|
||||
|
||||
private fun checkUserPermissions() {
|
||||
val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||
.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
|
||||
|
@ -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
|
||||
}
|
@ -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() }
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ import androidx.preference.PreferenceManager
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
import org.citra.citra_emu.ui.main.ThemeProvider
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
object ThemeUtil {
|
||||
@ -26,6 +27,20 @@ object ThemeUtil {
|
||||
|
||||
fun setTheme(activity: AppCompatActivity) {
|
||||
setThemeMode(activity)
|
||||
if (preferences.getBoolean(Settings.PREF_MATERIAL_YOU, false)) {
|
||||
activity.setTheme(R.style.Theme_Citra_Main_MaterialYou)
|
||||
} else {
|
||||
activity.setTheme(R.style.Theme_Citra_Main)
|
||||
}
|
||||
|
||||
// Using a specific night mode check because this could apply incorrectly when using the
|
||||
// light app mode, dark system mode, and black backgrounds. Launching the settings activity
|
||||
// will then show light mode colors/navigation bars but with black backgrounds.
|
||||
if (preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) &&
|
||||
isNightMode(activity)
|
||||
) {
|
||||
activity.setTheme(R.style.ThemeOverlay_Citra_Dark)
|
||||
}
|
||||
}
|
||||
|
||||
fun setThemeMode(activity: AppCompatActivity) {
|
||||
@ -64,6 +79,14 @@ object ThemeUtil {
|
||||
windowController.isAppearanceLightNavigationBars = false
|
||||
}
|
||||
|
||||
fun setCorrectTheme(activity: AppCompatActivity) {
|
||||
val currentTheme = (activity as ThemeProvider).themeId
|
||||
setTheme(activity)
|
||||
if (currentTheme != (activity as ThemeProvider).themeId) {
|
||||
activity.recreate()
|
||||
}
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun getColorWithOpacity(@ColorInt color: Int, alphaFactor: Float): Int {
|
||||
return Color.argb(
|
||||
|
@ -31,6 +31,7 @@ add_library(citra-android SHARED
|
||||
native.cpp
|
||||
ndk_motion.cpp
|
||||
ndk_motion.h
|
||||
system_save_game.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(citra-android PRIVATE audio_core citra_common citra_core input_common network)
|
||||
|
@ -75,13 +75,6 @@ static const std::array<int, Settings::NativeAnalog::NumAnalogs> default_analogs
|
||||
InputManager::N3DS_STICK_C,
|
||||
}};
|
||||
|
||||
void Config::UpdateCFG() {
|
||||
std::shared_ptr<Service::CFG::Module> cfg = std::make_shared<Service::CFG::Module>();
|
||||
cfg->SetSystemLanguage(static_cast<Service::CFG::SystemLanguage>(
|
||||
sdl2_config->GetInteger("System", "language", Service::CFG::SystemLanguage::LANGUAGE_EN)));
|
||||
cfg->UpdateConfigNANDSavegame();
|
||||
}
|
||||
|
||||
template <>
|
||||
void Config::ReadSetting(const std::string& group, Settings::Setting<std::string>& setting) {
|
||||
std::string setting_value = sdl2_config->Get(group, setting.GetLabel(), setting.GetDefault());
|
||||
@ -215,24 +208,11 @@ void Config::ReadValues() {
|
||||
ReadSetting("System", Settings::values.region_value);
|
||||
ReadSetting("System", Settings::values.init_clock);
|
||||
{
|
||||
std::tm t;
|
||||
t.tm_sec = 1;
|
||||
t.tm_min = 0;
|
||||
t.tm_hour = 0;
|
||||
t.tm_mday = 1;
|
||||
t.tm_mon = 0;
|
||||
t.tm_year = 100;
|
||||
t.tm_isdst = 0;
|
||||
std::istringstream string_stream(
|
||||
sdl2_config->GetString("System", "init_time", "2000-01-01 00:00:01"));
|
||||
string_stream >> std::get_time(&t, "%Y-%m-%d %H:%M:%S");
|
||||
if (string_stream.fail()) {
|
||||
LOG_ERROR(Config, "Failed To parse init_time. Using 2000-01-01 00:00:01");
|
||||
std::string time = sdl2_config->GetString("System", "init_time", "946681277");
|
||||
try {
|
||||
Settings::values.init_time = std::stoll(time);
|
||||
} catch (...) {
|
||||
}
|
||||
Settings::values.init_time =
|
||||
std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::from_time_t(std::mktime(&t)).time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
ReadSetting("System", Settings::values.plugin_loader_enabled);
|
||||
ReadSetting("System", Settings::values.allow_plugin_loader);
|
||||
@ -286,9 +266,6 @@ void Config::ReadValues() {
|
||||
sdl2_config->GetString("WebService", "web_api_url", "https://api.citra-emu.org");
|
||||
NetSettings::values.citra_username = sdl2_config->GetString("WebService", "citra_username", "");
|
||||
NetSettings::values.citra_token = sdl2_config->GetString("WebService", "citra_token", "");
|
||||
|
||||
// Update CFG file based on settings
|
||||
UpdateCFG();
|
||||
}
|
||||
|
||||
void Config::Reload() {
|
||||
|
@ -17,7 +17,6 @@ private:
|
||||
|
||||
bool LoadINI(const std::string& default_contents = "", bool retry = true);
|
||||
void ReadValues();
|
||||
void UpdateCFG();
|
||||
|
||||
public:
|
||||
Config();
|
||||
|
@ -64,7 +64,6 @@ ANativeWindow* s_surf;
|
||||
|
||||
std::shared_ptr<Common::DynamicLibrary> vulkan_library{};
|
||||
std::unique_ptr<EmuWindow_Android> window;
|
||||
std::shared_ptr<Service::CFG::Module> cfg;
|
||||
|
||||
std::atomic<bool> stop_run{true};
|
||||
std::atomic<bool> pause_emulation{false};
|
||||
@ -732,29 +731,4 @@ void Java_org_citra_citra_1emu_NativeLibrary_logDeviceInfo([[maybe_unused]] JNIE
|
||||
LOG_INFO(Frontend, "Host OS: Android API level {}", android_get_device_api_level());
|
||||
}
|
||||
|
||||
void Java_org_citra_citra_1emu_NativeLibrary_loadSystemConfig([[maybe_unused]] JNIEnv* env,
|
||||
[[maybe_unused]] jobject obj) {
|
||||
if (Core::System::GetInstance().IsPoweredOn()) {
|
||||
cfg = Service::CFG::GetModule(Core::System::GetInstance());
|
||||
} else {
|
||||
cfg = std::make_shared<Service::CFG::Module>();
|
||||
}
|
||||
}
|
||||
|
||||
void Java_org_citra_citra_1emu_NativeLibrary_saveSystemConfig([[maybe_unused]] JNIEnv* env,
|
||||
[[maybe_unused]] jobject obj) {
|
||||
cfg->UpdateConfigNANDSavegame();
|
||||
}
|
||||
|
||||
void Java_org_citra_citra_1emu_NativeLibrary_setSystemSetupNeeded([[maybe_unused]] JNIEnv* env,
|
||||
[[maybe_unused]] jobject obj,
|
||||
jboolean needed) {
|
||||
cfg->SetSystemSetupNeeded(needed);
|
||||
}
|
||||
|
||||
jboolean Java_org_citra_citra_1emu_NativeLibrary_getIsSystemSetupNeeded(
|
||||
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
|
||||
return cfg->IsSystemSetupNeeded();
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
122
src/android/app/src/main/jni/system_save_game.cpp
Normal file
122
src/android/app/src/main/jni/system_save_game.cpp
Normal 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"
|
9
src/android/app/src/main/res/drawable/ic_palette.xml
Normal file
9
src/android/app/src/main/res/drawable/ic_palette.xml
Normal 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>
|
@ -1,20 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/coordinator_main"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/coordinator_settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar_settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
android:fitsSystemWindows="true"
|
||||
app:elevation="0dp">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar_settings"
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
style="?attr/collapsingToolbarLayoutMediumStyle"
|
||||
android:id="@+id/toolbar_settings_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize" />
|
||||
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar_settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_collapseMode="pin" />
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
@ -22,6 +35,16 @@
|
||||
android:id="@+id/frame_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="12dp"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<View
|
||||
android:id="@+id/navigation_bar_shade"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:background="@android:color/transparent"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:layout_gravity="bottom|center_horizontal" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
64
src/android/app/src/main/res/layout/dialog_input.xml
Normal file
64
src/android/app/src/main/res/layout/dialog_input.xml
Normal 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>
|
@ -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>
|
@ -1,5 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@ -8,34 +10,43 @@
|
||||
android:focusable="true"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="72dp"
|
||||
android:paddingTop="@dimen/spacing_large"
|
||||
android:paddingBottom="@dimen/spacing_large">
|
||||
android:padding="@dimen/spacing_large">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_setting_name"
|
||||
style="@style/TextAppearance.AppCompat.Headline"
|
||||
android:layout_width="0dp"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginStart="@dimen/spacing_large"
|
||||
android:layout_marginEnd="@dimen/spacing_large"
|
||||
android:textSize="16sp"
|
||||
tools:text="Setting Name" />
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_setting_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/text_setting_name"
|
||||
android:layout_alignStart="@+id/text_setting_name"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginStart="@dimen/spacing_large"
|
||||
android:layout_marginTop="@dimen/spacing_small"
|
||||
android:layout_marginEnd="@dimen/spacing_large"
|
||||
android:visibility="visible"
|
||||
tools:text="@string/app_disclaimer" />
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/text_setting_name"
|
||||
style="@style/TextAppearance.Material3.HeadlineMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="viewStart"
|
||||
android:textSize="16sp"
|
||||
app:lineHeight="22dp"
|
||||
tools:text="Setting Name" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/text_setting_description"
|
||||
style="@style/TextAppearance.Material3.BodySmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_small"
|
||||
android:textAlignment="viewStart"
|
||||
tools:text="@string/app_disclaimer" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/text_setting_value"
|
||||
style="@style/TextAppearance.Material3.LabelMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_small"
|
||||
android:textAlignment="viewStart"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
tools:text="1x" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
@ -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>
|
@ -1,19 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<com.google.android.material.textview.MaterialTextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/text_header_name"
|
||||
style="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_header_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="@dimen/spacing_large"
|
||||
android:layout_marginBottom="@dimen/spacing_small"
|
||||
android:layout_marginTop="@dimen/spacing_small"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:textStyle="bold"
|
||||
tools:text="CPU Settings" />
|
||||
|
||||
</FrameLayout>
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:paddingHorizontal="@dimen/spacing_large"
|
||||
android:paddingVertical="16dp"
|
||||
android:focusable="false"
|
||||
android:clickable="false"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:textStyle="bold"
|
||||
tools:text="CPU Settings" />
|
||||
|
@ -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>
|
29
src/android/app/src/main/res/values-night-v31/themes.xml
Normal file
29
src/android/app/src/main/res/values-night-v31/themes.xml
Normal 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
Loading…
Reference in New Issue
Block a user