mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-31 05:40:04 +00:00 
			
		
		
		
	Android UI Overhaul Part 2 (#7147)
This commit is contained in:
		
							parent
							
								
									33a1f27a99
								
							
						
					
					
						commit
						c17ec1d1aa
					
				
					 104 changed files with 5613 additions and 3752 deletions
				
			
		|  | @ -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" /> | ||||
| 
 | ||||
| </RelativeLayout> | ||||
|         <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…
	
	Add table
		Add a link
		
	
		Reference in a new issue