This commit is contained in:
David Griswold 2026-03-29 15:52:09 +00:00 committed by GitHub
commit db512b7c0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 884 additions and 1135 deletions

View File

@ -38,7 +38,7 @@ import org.citra.citra_emu.display.SecondaryDisplay
import org.citra.citra_emu.features.hotkeys.HotkeyUtility
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.SettingsViewModel
import org.citra.citra_emu.features.settings.model.Settings
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
import org.citra.citra_emu.fragments.EmulationFragment
import org.citra.citra_emu.fragments.MessageDialogFragment
@ -52,14 +52,13 @@ import org.citra.citra_emu.utils.Log
import org.citra.citra_emu.utils.RefreshRateUtil
import org.citra.citra_emu.utils.ThemeUtil
import org.citra.citra_emu.viewmodel.EmulationViewModel
import org.citra.citra_emu.features.settings.utils.SettingsFile
class EmulationActivity : AppCompatActivity() {
private val preferences: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
var isActivityRecreated = false
private val emulationViewModel: EmulationViewModel by viewModels()
val settingsViewModel: SettingsViewModel by viewModels()
val emulationViewModel: EmulationViewModel by viewModels()
private lateinit var binding: ActivityEmulationBinding
private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil
private lateinit var hotkeyUtility: HotkeyUtility
@ -88,14 +87,22 @@ class EmulationActivity : AppCompatActivity() {
RefreshRateUtil.enforceRefreshRate(this, sixtyHz = true)
ThemeUtil.setTheme(this)
settingsViewModel.settings.loadSettings()
super.onCreate(savedInstanceState)
secondaryDisplay = SecondaryDisplay(this)
// load global settings if for some reason they aren't (should be loaded in MainActivity)
if (Settings.settings.getAllGlobal().isEmpty()) {
SettingsFile.loadSettings(Settings.settings)
}
// once per-game settings are added, load them here!
secondaryDisplay = SecondaryDisplay(this, Settings.settings)
secondaryDisplay.updateDisplay()
binding = ActivityEmulationBinding.inflate(layoutInflater)
screenAdjustmentUtil = ScreenAdjustmentUtil(this, windowManager, settingsViewModel.settings)
hotkeyUtility = HotkeyUtility(screenAdjustmentUtil, this)
screenAdjustmentUtil = ScreenAdjustmentUtil(this, windowManager, Settings.settings)
hotkeyUtility = HotkeyUtility(screenAdjustmentUtil, this, Settings.settings)
setContentView(binding.root)
val navHostFragment =
@ -142,7 +149,7 @@ class EmulationActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
enableFullscreenImmersive()
applyOrientationSettings() // Check for orientation settings changes on runtime
applyOrientationSettings()
}
override fun onStop() {
@ -179,6 +186,8 @@ class EmulationActivity : AppCompatActivity() {
secondaryDisplay.releasePresentation()
secondaryDisplay.releaseVD()
Settings.settings.removePerGameSettings()
super.onDestroy()
}
@ -229,11 +238,11 @@ class EmulationActivity : AppCompatActivity() {
).show()
}
private fun enableFullscreenImmersive() {
fun enableFullscreenImmersive() {
val attributes = window.attributes
attributes.layoutInDisplayCutoutMode =
if (BooleanSetting.EXPAND_TO_CUTOUT_AREA.boolean) {
if (Settings.settings.get(BooleanSetting.EXPAND_TO_CUTOUT_AREA)) {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
} else {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
@ -250,8 +259,8 @@ class EmulationActivity : AppCompatActivity() {
}
}
private fun applyOrientationSettings() {
val orientationOption = IntSetting.ORIENTATION_OPTION.int
fun applyOrientationSettings() {
val orientationOption = Settings.settings.get(IntSetting.ORIENTATION_OPTION)
screenAdjustmentUtil.changeActivityOrientation(orientationOption)
}

View File

@ -29,13 +29,13 @@ class ScreenAdjustmentUtil(
isEnabled,
windowManager.defaultDisplay.rotation
)
BooleanSetting.SWAP_SCREEN.boolean = isEnabled
settings.saveSetting(BooleanSetting.SWAP_SCREEN, SettingsFile.FILE_NAME_CONFIG)
settings.update(BooleanSetting.SWAP_SCREEN, isEnabled)
SettingsFile.saveSetting(BooleanSetting.SWAP_SCREEN, settings)
}
fun cycleLayouts() {
val landscapeLayoutsToCycle = IntListSetting.LAYOUTS_TO_CYCLE.list;
val landscapeLayoutsToCycle = settings.get(IntListSetting.LAYOUTS_TO_CYCLE)
val landscapeValues =
if (landscapeLayoutsToCycle.isNotEmpty())
landscapeLayoutsToCycle.toIntArray()
@ -45,12 +45,12 @@ class ScreenAdjustmentUtil(
val portraitValues = context.resources.getIntArray(R.array.portraitValues)
if (NativeLibrary.isPortraitMode) {
val currentLayout = IntSetting.PORTRAIT_SCREEN_LAYOUT.int
val currentLayout = settings.get(IntSetting.PORTRAIT_SCREEN_LAYOUT)
val pos = portraitValues.indexOf(currentLayout)
val layoutOption = portraitValues[(pos + 1) % portraitValues.size]
changePortraitOrientation(layoutOption)
} else {
val currentLayout = IntSetting.SCREEN_LAYOUT.int
val currentLayout = settings.get(IntSetting.SCREEN_LAYOUT)
val pos = landscapeValues.indexOf(currentLayout)
val layoutOption = landscapeValues[(pos + 1) % landscapeValues.size]
changeScreenOrientation(layoutOption)
@ -58,30 +58,30 @@ class ScreenAdjustmentUtil(
}
fun changePortraitOrientation(layoutOption: Int) {
IntSetting.PORTRAIT_SCREEN_LAYOUT.int = layoutOption
settings.saveSetting(IntSetting.PORTRAIT_SCREEN_LAYOUT, SettingsFile.FILE_NAME_CONFIG)
settings.update(IntSetting.PORTRAIT_SCREEN_LAYOUT, layoutOption)
SettingsFile.saveSetting(IntSetting.PORTRAIT_SCREEN_LAYOUT, settings)
NativeLibrary.reloadSettings()
NativeLibrary.updateFramebuffer(NativeLibrary.isPortraitMode)
}
fun changeScreenOrientation(layoutOption: Int) {
IntSetting.SCREEN_LAYOUT.int = layoutOption
settings.saveSetting(IntSetting.SCREEN_LAYOUT, SettingsFile.FILE_NAME_CONFIG)
settings.update(IntSetting.SCREEN_LAYOUT, layoutOption)
SettingsFile.saveSetting(IntSetting.SCREEN_LAYOUT, settings)
NativeLibrary.reloadSettings()
NativeLibrary.updateFramebuffer(NativeLibrary.isPortraitMode)
}
fun changeActivityOrientation(orientationOption: Int) {
val activity = context as? Activity ?: return
IntSetting.ORIENTATION_OPTION.int = orientationOption
settings.saveSetting(IntSetting.ORIENTATION_OPTION, SettingsFile.FILE_NAME_CONFIG)
settings.update(IntSetting.ORIENTATION_OPTION, orientationOption)
SettingsFile.saveSetting(IntSetting.ORIENTATION_OPTION, settings)
activity.requestedOrientation = orientationOption
}
fun toggleScreenUpright() {
val uprightBoolean = BooleanSetting.UPRIGHT_SCREEN.boolean
BooleanSetting.UPRIGHT_SCREEN.boolean = !uprightBoolean
settings.saveSetting(BooleanSetting.UPRIGHT_SCREEN, SettingsFile.FILE_NAME_CONFIG)
val uprightBoolean = settings.get(BooleanSetting.UPRIGHT_SCREEN)
settings.update(BooleanSetting.UPRIGHT_SCREEN, !uprightBoolean)
SettingsFile.saveSetting(BooleanSetting.UPRIGHT_SCREEN, settings)
NativeLibrary.reloadSettings()
NativeLibrary.updateFramebuffer(NativeLibrary.isPortraitMode)

View File

@ -16,8 +16,9 @@ import android.view.SurfaceView
import android.view.WindowManager
import org.citra.citra_emu.features.settings.model.IntSetting
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.features.settings.model.Settings
class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener {
class SecondaryDisplay(val context: Context, private val settings: Settings) : DisplayManager.DisplayListener {
private var pres: SecondaryDisplayPresentation? = null
private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
private val vd: VirtualDisplay
@ -70,8 +71,7 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener {
// decide if we are going to the external display or the internal one
var display = getExternalDisplay(context)
if (display == null ||
IntSetting.SECONDARY_DISPLAY_LAYOUT.int == SecondaryDisplayLayout.NONE.int) {
if (display == null || settings.get(IntSetting.SECONDARY_DISPLAY_LAYOUT) == SecondaryDisplayLayout.NONE.int) {
display = vd.display
}

View File

@ -19,7 +19,8 @@ import org.citra.citra_emu.features.settings.model.Settings
class HotkeyUtility(
private val screenAdjustmentUtil: ScreenAdjustmentUtil,
private val context: Context
private val context: Context,
private val settings: Settings
) {
private val hotkeyButtons = Hotkey.entries.map { it.button }
@ -112,7 +113,7 @@ class HotkeyUtility(
Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts()
Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame()
Hotkey.PAUSE_OR_RESUME.button -> EmulationLifecycleUtil.pauseOrResume()
Hotkey.TURBO_LIMIT.button -> TurboHelper.toggleTurbo(true)
Hotkey.TURBO_LIMIT.button -> TurboHelper.toggleTurbo(true, settings)
Hotkey.QUICKSAVE.button -> {
NativeLibrary.saveState(NativeLibrary.QUICKSAVE_SLOT)
Toast.makeText(

View File

@ -1,9 +0,0 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.model
interface AbstractBooleanSetting : AbstractSetting {
var boolean: Boolean
}

View File

@ -1,9 +0,0 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.model
interface AbstractFloatSetting : AbstractSetting {
var float: Float
}

View File

@ -1,9 +0,0 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.model
interface AbstractIntSetting : AbstractSetting {
var int: Int
}

View File

@ -1,9 +0,0 @@
// Copyright Citra Emulator Project / Azahar 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 AbstractListSetting<E> : AbstractSetting {
var list: List<E>
}

View File

@ -1,13 +1,16 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar 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?
interface AbstractSetting<T> {
val key: String
val section: String
val defaultValue: T
val isRuntimeEditable: Boolean
val valueAsString: String
val defaultValue: Any
fun valueToString(value: T): String = value.toString()
fun valueFromString(string: String): T?
}

View File

@ -1,9 +0,0 @@
// 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 AbstractShortSetting : AbstractSetting {
var short: Short
}

View File

@ -1,9 +0,0 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.model
interface AbstractStringSetting : AbstractSetting {
var string: String
}

View File

@ -10,7 +10,7 @@ enum class BooleanSetting(
override val key: String,
override val section: String,
override val defaultValue: Boolean
) : AbstractBooleanSetting {
) : AbstractSetting<Boolean> {
EXPAND_TO_CUTOUT_AREA(SettingKeys.expand_to_cutout_area(), Settings.SECTION_LAYOUT, false),
SPIRV_SHADER_GEN(SettingKeys.spirv_shader_gen(), Settings.SECTION_RENDERER, true),
ASYNC_SHADERS(SettingKeys.async_shader_compilation(), Settings.SECTION_RENDERER, false),
@ -58,10 +58,13 @@ enum class BooleanSetting(
APPLY_REGION_FREE_PATCH(SettingKeys.apply_region_free_patch(), Settings.SECTION_SYSTEM, true),
USE_INTEGER_SCALING(SettingKeys.use_integer_scaling(), Settings.SECTION_RENDERER, false);
override var boolean: Boolean = defaultValue
override val valueAsString: String
get() = boolean.toString()
override fun valueFromString(string: String): Boolean? {
return when (string.trim().lowercase()) {
"1", "true" -> true
"0", "false" -> false
else -> null
}
}
override val isRuntimeEditable: Boolean
get() {
@ -98,7 +101,5 @@ enum class BooleanSetting(
fun from(key: String): BooleanSetting? =
BooleanSetting.values().firstOrNull { it.key == key }
fun clear() = BooleanSetting.values().forEach { it.boolean = it.defaultValue }
}
}
}

View File

@ -9,18 +9,25 @@ import org.citra.citra_emu.features.settings.SettingKeys
enum class FloatSetting(
override val key: String,
override val section: String,
override val defaultValue: Float
) : AbstractFloatSetting {
override val defaultValue: Float,
val scale:Int = 1
) : AbstractSetting<Float> {
LARGE_SCREEN_PROPORTION(SettingKeys.large_screen_proportion(),Settings.SECTION_LAYOUT,2.25f),
SECOND_SCREEN_OPACITY(SettingKeys.custom_second_layer_opacity(), Settings.SECTION_RENDERER, 100f),
BACKGROUND_RED(SettingKeys.bg_red(), Settings.SECTION_RENDERER, 0f),
BACKGROUND_BLUE(SettingKeys.bg_blue(), Settings.SECTION_RENDERER, 0f),
BACKGROUND_GREEN(SettingKeys.bg_green(), Settings.SECTION_RENDERER, 0f);
BACKGROUND_RED(SettingKeys.bg_red(), Settings.SECTION_RENDERER, 0f, 255),
BACKGROUND_BLUE(SettingKeys.bg_blue(), Settings.SECTION_RENDERER, 0f, 255),
BACKGROUND_GREEN(SettingKeys.bg_green(), Settings.SECTION_RENDERER, 0f, 255),
AUDIO_VOLUME(SettingKeys.volume(), Settings.SECTION_AUDIO, 100f, 100);
override var float: Float = defaultValue
// valueFromString reads raw setting from file, scales up for UI
override fun valueFromString(string: String): Float? {
return string.toFloatOrNull()?.times(scale)
}
override val valueAsString: String
get() = float.toString()
// valueToString scales back down to raw for file
override fun valueToString(value: Float): String {
return (value / scale).toString()
}
override val isRuntimeEditable: Boolean
get() {
@ -36,7 +43,5 @@ enum class FloatSetting(
private val NOT_RUNTIME_EDITABLE = emptyList<FloatSetting>()
fun from(key: String): FloatSetting? = FloatSetting.values().firstOrNull { it.key == key }
fun clear() = FloatSetting.values().forEach { it.float = it.defaultValue }
}
}

View File

@ -9,28 +9,17 @@ enum class IntListSetting(
override val section: String,
override val defaultValue: List<Int>,
val canBeEmpty: Boolean = true
) : AbstractListSetting<Int> {
) : AbstractSetting<List<Int>> {
LAYOUTS_TO_CYCLE("layouts_to_cycle", Settings.SECTION_LAYOUT, listOf(0, 1, 2, 3, 4, 5), canBeEmpty = false);
private var backingList: List<Int> = defaultValue
private var lastValidList : List<Int> = defaultValue
override var list: List<Int>
get() = backingList
set(value) {
if (!canBeEmpty && value.isEmpty()) {
backingList = lastValidList
} else {
backingList = value
lastValidList = value
}
}
override val valueAsString: String
get() = list.joinToString()
override fun valueToString(value: List<Int>): String = value.joinToString()
override fun valueFromString(string: String): List<Int>? {
return string.split(",")
.mapNotNull { it.trim().toIntOrNull() }
.takeIf { canBeEmpty || it.isNotEmpty() }
}
override val isRuntimeEditable: Boolean
get() {
for (setting in NOT_RUNTIME_EDITABLE) {
@ -46,7 +35,5 @@ enum class IntListSetting(
fun from(key: String): IntListSetting? =
values().firstOrNull { it.key == key }
fun clear() = values().forEach { it.list = it.defaultValue }
}
}

View File

@ -10,7 +10,7 @@ enum class IntSetting(
override val key: String,
override val section: String,
override val defaultValue: Int
) : AbstractIntSetting {
) : AbstractSetting<Int> {
FRAME_LIMIT(SettingKeys.frame_limit(), Settings.SECTION_RENDERER, 100),
EMULATED_REGION(SettingKeys.region_value(), Settings.SECTION_SYSTEM, -1),
INIT_CLOCK(SettingKeys.init_clock(), Settings.SECTION_SYSTEM, 0),
@ -50,7 +50,6 @@ enum class IntSetting(
CPU_CLOCK_SPEED(SettingKeys.cpu_clock_percentage(), Settings.SECTION_CORE, 100),
TEXTURE_FILTER(SettingKeys.texture_filter(), Settings.SECTION_RENDERER, 0),
TEXTURE_SAMPLING(SettingKeys.texture_sampling(), Settings.SECTION_RENDERER, 0),
USE_FRAME_LIMIT(SettingKeys.use_frame_limit(), Settings.SECTION_RENDERER, 1),
DELAY_RENDER_THREAD_US(SettingKeys.delay_game_render_thread_us(), Settings.SECTION_RENDERER, 0),
ORIENTATION_OPTION(SettingKeys.screen_orientation(), Settings.SECTION_LAYOUT, 2),
TURBO_LIMIT(SettingKeys.turbo_limit(), Settings.SECTION_CORE, 200),
@ -58,10 +57,15 @@ enum class IntSetting(
RENDER_3D_WHICH_DISPLAY(SettingKeys.render_3d_which_display(),Settings.SECTION_RENDERER,0),
ASPECT_RATIO(SettingKeys.aspect_ratio(), Settings.SECTION_LAYOUT, 0);
override var int: Int = defaultValue
override val valueAsString: String
get() = int.toString()
override fun valueFromString(string: String): Int? {
return string.toIntOrNull() ?: when (string.trim().lowercase()) {
"true" -> 1
"false" -> 0
else -> null
}
}
override val isRuntimeEditable: Boolean
get() {
@ -83,6 +87,5 @@ enum class IntSetting(
fun from(key: String): IntSetting? = IntSetting.values().firstOrNull { it.key == key }
fun clear() = IntSetting.values().forEach { it.int = it.defaultValue }
}
}
}

View File

@ -1,43 +0,0 @@
// Copyright Citra Emulator Project / Azahar 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 org.citra.citra_emu.features.settings.SettingKeys
enum class ScaledFloatSetting(
override val key: String,
override val section: String,
override val defaultValue: Float,
val scale: Int
) : AbstractFloatSetting {
AUDIO_VOLUME(SettingKeys.volume(), Settings.SECTION_AUDIO, 1.0f, 100);
override var float: Float = defaultValue
get() = field * scale
set(value) {
field = value / scale
}
override val valueAsString: String get() = (float / scale).toString()
override val isRuntimeEditable: Boolean
get() {
for (setting in NOT_RUNTIME_EDITABLE) {
if (setting == this) {
return false
}
}
return true
}
companion object {
private val NOT_RUNTIME_EDITABLE = emptyList<ScaledFloatSetting>()
fun from(key: String): ScaledFloatSetting? =
ScaledFloatSetting.values().firstOrNull { it.key == key }
fun clear() = ScaledFloatSetting.values().forEach { it.float = it.defaultValue * it.scale }
}
}

View File

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

View File

@ -4,101 +4,94 @@
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
private val globalValues = HashMap<String, Any>()
private val perGameOverrides = HashMap<String, Any>()
var isLoaded = false
var gameId: String? = null
fun isPerGame(): Boolean = gameId != null && gameId != ""
fun <T> get(setting: AbstractSetting<T>): T {
@Suppress("UNCHECKED_CAST")
return (perGameOverrides[setting.key]
?: globalValues[setting.key]
?: setting.defaultValue) as T
}
fun <T> getGlobal(setting: AbstractSetting<T>): T {
@Suppress("UNCHECKED_CAST")
return (globalValues[setting.key] ?: setting.defaultValue) as T
}
fun <T> setGlobal(setting: AbstractSetting<T>, value: T) {
globalValues[setting.key] = value as Any
}
fun <T> setOverride(setting: AbstractSetting<T>, value: T) {
perGameOverrides[setting.key] = value as Any
}
/** Sets the per-game or global setting based on whether this file has ANY per-game setting.
* This should be used, for example, by the Settings Activity
*/
fun <T> set(setting: AbstractSetting<T>, value: T) {
if (isPerGame()) setOverride(setting, value) else setGlobal(setting, value)
}
/**
* A HashMap<String></String>, SettingSection> that constructs a new SettingSection instead of returning null
* when getting a key not already in the map
* Updates an existing setting honoring whether it is *currently* global or local. This will
* be used by the Quick Menu
*/
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)
fun <T> update(setting: AbstractSetting<T>, value: T) {
if (hasOverride(setting)) setOverride(setting, value) else setGlobal(setting, value)
}
/** Merge the globals from other into the current settings. Merge per-game if game id is the same. */
fun mergeSettings(other: Settings) {
other.globalValues.forEach{ (key, value) ->
globalValues[key] = value
}
if (gameId != other.gameId) return
perGameOverrides.clear()
other.perGameOverrides.forEach{ (key, value) ->
perGameOverrides[key] = value
}
}
var sections: HashMap<String, SettingSection?> = SettingsSectionMap()
fun getSection(sectionName: String): SettingSection? {
return sections[sectionName]
fun <T> clearOverride(setting: AbstractSetting<T>) {
perGameOverrides.remove(setting.key)
}
val isEmpty: Boolean
get() = sections.isEmpty()
fun loadSettings(view: SettingsActivityView? = null) {
sections = SettingsSectionMap()
loadCitraSettings(view)
if (!TextUtils.isEmpty(gameId)) {
loadCustomGameSettings(gameId!!, view)
}
isLoaded = true
fun hasOverride(setting: AbstractSetting<*>): Boolean {
return perGameOverrides.containsKey(setting.key)
}
private fun loadCitraSettings(view: SettingsActivityView?) {
for ((fileName) in configFileSectionsMap) {
sections.putAll(SettingsFile.readFile(fileName, view))
}
fun getAllOverrides(): Map<String, Any> = perGameOverrides.toMap()
fun getAllGlobal(): Map<String, Any> = globalValues.toMap()
fun clearAll() {
globalValues.clear()
perGameOverrides.clear()
}
private fun loadCustomGameSettings(gameId: String, view: SettingsActivityView?) {
// Custom game settings
mergeSections(SettingsFile.readCustomGameSettings(gameId, view))
fun clearOverrides() {
perGameOverrides.clear()
}
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 removePerGameSettings() {
clearOverrides()
gameId = null
}
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
}
}
fun saveSetting(setting: AbstractSetting, filename: String) {
SettingsFile.saveFile(filename, setting)
}
companion object {
const val SECTION_CORE = "Core"
const val SECTION_SYSTEM = "System"
const val SECTION_CAMERA = "Camera"
@ -234,20 +227,7 @@ class Settings {
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_STORAGE,
SECTION_UTILITY,
SECTION_AUDIO,
SECTION_DEBUG
)
}
/** Stores the settings as a singleton available everywhere.*/
val settings = Settings()
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -7,5 +7,7 @@ package org.citra.citra_emu.features.settings.model
import androidx.lifecycle.ViewModel
class SettingsViewModel : ViewModel() {
// the Settings Activity will always work with a local copy of settings while
// editing, to avoid issues with conflicting active/edited settings
val settings = Settings()
}

View File

@ -10,7 +10,7 @@ enum class StringSetting(
override val key: String,
override val section: String,
override val defaultValue: String
) : AbstractStringSetting {
) : AbstractSetting<String>{
INIT_TIME(SettingKeys.init_time(), Settings.SECTION_SYSTEM, "946731601"),
CAMERA_INNER_NAME(SettingKeys.camera_inner_name(), Settings.SECTION_CAMERA, "ndk"),
CAMERA_INNER_CONFIG(SettingKeys.camera_inner_config(), Settings.SECTION_CAMERA, "_front"),
@ -19,10 +19,7 @@ enum class StringSetting(
CAMERA_OUTER_RIGHT_NAME(SettingKeys.camera_outer_right_name(), Settings.SECTION_CAMERA, "ndk"),
CAMERA_OUTER_RIGHT_CONFIG(SettingKeys.camera_outer_right_config(), Settings.SECTION_CAMERA, "_back");
override var string: String = defaultValue
override val valueAsString: String
get() = string
override fun valueFromString(string: String) = string
override val isRuntimeEditable: Boolean
get() {
@ -47,6 +44,5 @@ enum class StringSetting(
fun from(key: String): StringSetting? = StringSetting.values().firstOrNull { it.key == key }
fun clear() = StringSetting.values().forEach { it.string = it.defaultValue }
}
}

View File

@ -1,33 +1,41 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar 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
import org.citra.citra_emu.features.settings.model.Settings
class DateTimeSetting(
setting: AbstractSetting?,
private val settings: Settings,
setting: AbstractSetting<String>?,
titleId: Int,
descriptionId: Int,
val key: String? = null,
private val defaultValue: String? = null,
override var isEnabled: Boolean = true
override var isEnabled: Boolean = true,
private val getValue: (()->String)? = null,
private val setValue: ((String)-> Unit)? = null
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_DATETIME_SETTING
@Suppress("UNCHECKED_CAST")
val value: String
get() = if (setting != null) {
val setting = setting as AbstractStringSetting
setting.string
} else {
defaultValue!!
}
get() = getValue?.invoke()
?: if (setting != null) {
settings.get(setting as AbstractSetting<String>)
} else {
defaultValue!!
}
fun setSelectedValue(datetime: String): AbstractStringSetting {
val stringSetting = setting as AbstractStringSetting
stringSetting.string = datetime
return stringSetting
@Suppress("UNCHECKED_CAST")
fun setSelectedValue(datetime: String) {
if (setValue != null) {
setValue(datetime)
}else {
val stringSetting = setting as AbstractSetting<String>
settings.set(stringSetting, datetime)
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.

View File

@ -17,11 +17,10 @@ import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R
import org.citra.citra_emu.features.hotkeys.Hotkey
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.Settings
class InputBindingSetting(
val abstractSetting: AbstractSetting,
val abstractSetting: AbstractSetting<*>,
titleId: Int
) : SettingsItem(abstractSetting, titleId, 0) {
private val context: Context get() = CitraApplication.appContext
@ -161,11 +160,12 @@ class InputBindingSetting(
/**
* Removes the old mapping for this key from the settings, e.g. on user clearing the setting.
*/
@Suppress("UNCHECKED_CAST")
fun removeOldMapping() {
// Try remove all possible keys we wrote for this setting
val oldKey = preferences.getString(reverseKey, "")
if (oldKey != "") {
(setting as AbstractStringSetting).string = ""
//settings.set(setting as AbstractSetting<String>,"")
preferences.edit()
.remove(abstractSetting.key) // Used for ui text
.remove(oldKey + "_GuestOrientation") // Used for axis orientation

View File

@ -5,26 +5,34 @@
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.IntListSetting
import org.citra.citra_emu.features.settings.model.Settings
class MultiChoiceSetting(
setting: AbstractSetting?,
val settings: Settings,
setting: AbstractSetting<List<Int>>?,
titleId: Int,
descriptionId: Int,
val choicesId: Int,
val valuesId: Int,
val key: String? = null,
val defaultValue: List<Int>? = null,
override var isEnabled: Boolean = true
override var isEnabled: Boolean = true,
private val getValue: (()->List<Int>)? = null,
private val setValue: ((List<Int>)-> Unit)? = null
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_MULTI_CHOICE
val selectedValues: List<Int>
get() {
if (getValue != null) {
@Suppress("UNCHECKED_CAST")
return getValue.invoke()
}
if (setting == null) {
return defaultValue!!
}
try {
val setting = setting as IntListSetting
return setting.list
return settings.get(setting as IntListSetting)
}catch (_: ClassCastException) {
}
return defaultValue!!
@ -35,12 +43,14 @@ class MultiChoiceSetting(
* 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: List<Int>): IntListSetting {
val intSetting = setting as IntListSetting
intSetting.list = selection
return intSetting
fun setSelectedValue(selection: List<Int>) {
if (setValue != null) {
setValue(selection)
}else {
val intSetting = setting as IntListSetting
settings.set(intSetting, selection)
}
}
}

View File

@ -16,7 +16,7 @@ import org.citra.citra_emu.features.settings.model.AbstractSetting
* file.)
*/
abstract class SettingsItem(
var setting: AbstractSetting?,
var setting: AbstractSetting<*>?,
val nameId: Int,
val descriptionId: Int
) {
@ -35,6 +35,8 @@ abstract class SettingsItem(
return this.isEditable && this.isEnabled
}
companion object {
const val TYPE_HEADER = 0
const val TYPE_SWITCH = 1

View File

@ -1,62 +1,48 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar 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
import org.citra.citra_emu.features.settings.model.AbstractShortSetting
import org.citra.citra_emu.features.settings.model.Settings
class SingleChoiceSetting(
setting: AbstractSetting?,
val settings: Settings,
setting: AbstractSetting<*>?,
titleId: Int,
descriptionId: Int,
val choicesId: Int,
val valuesId: Int,
val key: String? = null,
val defaultValue: Int? = null,
override var isEnabled: Boolean = true
override var isEnabled: Boolean = true,
private val getValue: (()->Int)? = null,
private val setValue: ((Int)-> Unit)? = null
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_SINGLE_CHOICE
val selectedValue: Int
get() {
if (setting == null) {
return defaultValue!!
if (getValue != null) {
return getValue.invoke()
}
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!!
@Suppress("UNCHECKED_CAST")
val s = (setting as? AbstractSetting<Int>) ?: return defaultValue!!
return settings.get(s)
}
/**
* 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.
*
* Write a value to the backing int .
* @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
fun setSelectedValue(selection: Int) {
if (setValue != null) {
setValue(selection)
}else {
@Suppress("UNCHECKED_CAST")
val backSetting = setting as AbstractSetting<Int>
settings.set(backSetting, selection)
}
}
}

View File

@ -4,15 +4,16 @@
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.features.settings.model.Settings
import org.citra.citra_emu.utils.Log
import kotlin.math.pow
import kotlin.math.roundToInt
class SliderSetting(
setting: AbstractSetting?,
val settings: Settings,
setting: AbstractSetting<*>?,
titleId: Int,
descriptionId: Int,
val min: Int,
@ -20,17 +21,28 @@ class SliderSetting(
val units: String,
val key: String? = null,
val defaultValue: Float? = null,
override var isEnabled: Boolean = true
val rounding: Int = 2,
override var isEnabled: Boolean = true,
private val getValue: (()->Float)? = null,
private val setValue: ((Float)-> Unit)? = null
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_SLIDER
val selectedFloat: Float
get() {
val setting = setting ?: return defaultValue!!.toFloat()
if (getValue != null) return getValue.invoke()
val s = setting ?: return defaultValue!!
val ret = when (s.defaultValue) {
is Int -> {
@Suppress("UNCHECKED_CAST")
settings.get(s as AbstractSetting<Int>).toFloat()
}
is Float -> {
@Suppress("UNCHECKED_CAST")
settings.get(s as AbstractSetting<Float>)
}
val ret = when (setting) {
is AbstractIntSetting -> setting.int.toFloat()
is FloatSetting -> setting.float
is ScaledFloatSetting -> setting.float
else -> {
Log.error("[SliderSetting] Error casting setting type.")
-1f
@ -38,16 +50,32 @@ class SliderSetting(
}
return ret.coerceIn(min.toFloat(), max.toFloat())
}
/**
* 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
fun roundedFloat(value: Float): Float {
val factor = 10f.pow(rounding)
return (value * factor).roundToInt() / factor
}
val valueAsString: String
get() = setting?.let {
when (it.defaultValue) {
is Int -> {
@Suppress("UNCHECKED_CAST")
settings.get(it as AbstractSetting<Int>).toString()
}
is Float -> {
@Suppress("UNCHECKED_CAST")
roundedFloat(settings.get(it as AbstractSetting<Float>)).toString()
}
else -> ""
}
} ?: defaultValue?.toString() ?: ""
fun setSelectedValue(selection: Int): AbstractSetting<Int> {
@Suppress("UNCHECKED_CAST")
val intSetting = setting as AbstractSetting<Int>
settings.set(intSetting, selection)
return intSetting
}
@ -56,15 +84,14 @@ class SliderSetting(
* 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
fun setSelectedValue(selection: Float) {
if (setValue != null) {
setValue(selection)
}else {
@Suppress("UNCHECKED_CAST")
val floatSetting = setting as AbstractSetting<Float>
settings.set(floatSetting, selection)
}
return floatSetting
}
}

View File

@ -1,28 +1,40 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar 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
import org.citra.citra_emu.features.settings.model.Settings
class StringInputSetting(
setting: AbstractSetting?,
val settings: Settings,
setting: AbstractSetting<String>?,
titleId: Int,
descriptionId: Int,
val defaultValue: String,
val characterLimit: Int = 0,
override var isEnabled: Boolean = true
override var isEnabled: Boolean = true,
private val getValue: (()->String)? = null,
private val setValue: ((String)-> Unit)? = null
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_STRING_INPUT
val selectedValue: String
get() = setting?.valueAsString ?: defaultValue
@Suppress("UNCHECKED_CAST")
get() {
if (getValue != null) return getValue.invoke()
setting ?: return defaultValue
return settings.get(setting as AbstractSetting<String>)
}
fun setSelectedValue(selection: String): AbstractStringSetting {
val stringSetting = setting as AbstractStringSetting
stringSetting.string = selection
return stringSetting
fun setSelectedValue(selection: String) {
if (setValue != null) {
setValue.invoke(selection)
}else {
@Suppress("UNCHECKED_CAST")
val stringSetting = setting as AbstractSetting<String>
settings.set(stringSetting, selection)
}
}
}

View File

@ -1,22 +1,24 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar 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.AbstractShortSetting
import org.citra.citra_emu.features.settings.model.AbstractStringSetting
import org.citra.citra_emu.features.settings.model.Settings
class StringSingleChoiceSetting(
setting: AbstractSetting?,
val settings: Settings,
setting: AbstractSetting<String>?,
titleId: Int,
descriptionId: Int,
val choices: Array<String>,
val values: Array<String>?,
val key: String? = null,
private val defaultValue: String? = null,
override var isEnabled: Boolean = true
override var isEnabled: Boolean = true,
private val getValue: (()->String)? = null,
private val setValue: ((String)-> Unit)? = null
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_STRING_SINGLE_CHOICE
@ -31,22 +33,12 @@ class StringSingleChoiceSetting(
val selectedValue: String
get() {
if (getValue != null) return getValue.invoke()
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!!
@Suppress("UNCHECKED_CAST")
return settings.get(setting as AbstractSetting<String>)
}
val selectValueIndex: Int
get() {
@ -64,17 +56,14 @@ class StringSingleChoiceSetting(
* 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
fun setSelectedValue(selection: String) {
if (setValue != null) {
setValue(selection)
}else {
@Suppress("UNCHECKED_CAST")
val stringSetting = setting as AbstractSetting<String>
settings.set(stringSetting, selection)
}
}
}

View File

@ -4,38 +4,45 @@
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
import org.citra.citra_emu.features.settings.model.Settings
class SwitchSetting(
setting: AbstractBooleanSetting,
val settings: Settings,
setting: AbstractSetting<Boolean>?,
titleId: Int,
descriptionId: Int,
val key: String? = null,
val defaultValue: Boolean = false,
override var isEnabled: Boolean = true
override var isEnabled: Boolean = true,
private val getValue: (() -> Boolean)? = null,
private val setValue: ((Boolean) -> Unit)? = null
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_SWITCH
val isChecked: Boolean
get() {
if (getValue != null) return getValue.invoke()
if (setting == null) {
return defaultValue
}
val setting = setting as AbstractBooleanSetting
return setting.boolean
@Suppress("UNCHECKED_CAST")
val setting = setting as AbstractSetting<Boolean>
return settings.get(setting)
}
/**
* Write a value to the backing boolean.
*
* @param checked Pretty self explanatory.
* @return the existing setting with the new value applied.
*/
fun setChecked(checked: Boolean): AbstractBooleanSetting {
val setting = setting as AbstractBooleanSetting
setting.boolean = checked
return setting
fun setChecked(checked: Boolean) {
if (setValue != null) {
setValue(checked)
}else {
@Suppress("UNCHECKED_CAST")
val setting = setting as AbstractSetting<Boolean>
settings.set(setting, checked)
}
}
}

View File

@ -7,7 +7,6 @@ package org.citra.citra_emu.features.settings.ui
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
@ -27,13 +26,8 @@ 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
@ -42,12 +36,13 @@ import org.citra.citra_emu.utils.RefreshRateUtil
import org.citra.citra_emu.utils.ThemeUtil
class SettingsActivity : AppCompatActivity(), SettingsActivityView {
private val presenter = SettingsActivityPresenter(this)
private lateinit var binding: ActivitySettingsBinding
val settingsViewModel: SettingsViewModel by viewModels()
private val settingsViewModel: SettingsViewModel by viewModels()
private val presenter by lazy { SettingsActivityPresenter(this, settingsViewModel) }
// the activity will work with the fresh Settings() object created and stored in the viewmodel
override val settings: Settings get() = settingsViewModel.settings
override fun onCreate(savedInstanceState: Bundle?) {
@ -63,9 +58,9 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
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!!)
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)
@ -211,13 +206,6 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
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()) {

View File

@ -12,6 +12,8 @@ import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.features.settings.model.BooleanSetting
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.utils.SettingsFile
import org.citra.citra_emu.utils.SystemSaveGame
import org.citra.citra_emu.utils.DirectoryInitialization
import org.citra.citra_emu.utils.FileUtil
@ -19,8 +21,8 @@ import org.citra.citra_emu.utils.Log
import org.citra.citra_emu.utils.PermissionsHandler
import org.citra.citra_emu.utils.TurboHelper
class SettingsActivityPresenter(private val activityView: SettingsActivityView) {
val settings: Settings get() = activityView.settings
class SettingsActivityPresenter(private val activityView: SettingsActivityView, private val viewModel: SettingsViewModel) {
val settings: Settings get() = viewModel.settings
private var shouldSave = false
private lateinit var menuTag: String
@ -29,6 +31,9 @@ class SettingsActivityPresenter(private val activityView: SettingsActivityView)
fun onCreate(savedInstanceState: Bundle?, menuTag: String, gameId: String) {
this.menuTag = menuTag
this.gameId = gameId
// merge the active settings into the local settings activity instance
settings.mergeSettings(Settings.settings)
if (savedInstanceState != null) {
shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE)
}
@ -47,13 +52,6 @@ class SettingsActivityPresenter(private val activityView: SettingsActivityView)
}
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()
}
@ -72,7 +70,8 @@ class SettingsActivityPresenter(private val activityView: SettingsActivityView)
val nomediaFileExists: Boolean
try {
dataDirTreeUri = PermissionsHandler.citraDirectory
dataDirDocument = DocumentFile.fromTreeUri(CitraApplication.appContext, dataDirTreeUri)!!
dataDirDocument =
DocumentFile.fromTreeUri(CitraApplication.appContext, dataDirTreeUri)!!
nomediaFileDocument = dataDirDocument.findFile(".nomedia")
nomediaFileExists = (nomediaFileDocument != null)
} catch (e: Exception) {
@ -80,7 +79,7 @@ class SettingsActivityPresenter(private val activityView: SettingsActivityView)
return
}
if (BooleanSetting.ANDROID_HIDE_IMAGES.boolean) {
if (settings.get(BooleanSetting.ANDROID_HIDE_IMAGES)) {
if (!nomediaFileExists) {
Log.info("[SettingsActivity]: Attempting to create .nomedia in user data directory")
FileUtil.createFile(dataDirTreeUri.toString(), ".nomedia")
@ -94,14 +93,18 @@ class SettingsActivityPresenter(private val activityView: SettingsActivityView)
fun onStop(finishing: Boolean) {
if (finishing && shouldSave) {
Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
settings.saveSettings(activityView)
//added to ensure that layout changes take effect as soon as settings window closes
if (settings.isPerGame()) {
SettingsFile.saveCustomFile(settings,activityView)
}else{
SettingsFile.saveGlobalFile(settings,activityView)
}
// merge the edited settings back into the active settings
Settings.settings.mergeSettings(settings)
NativeLibrary.reloadSettings()
NativeLibrary.updateFramebuffer(NativeLibrary.isPortraitMode)
updateAndroidImageVisibility()
TurboHelper.reloadTurbo(false) // TODO: Can this go somewhere else? -OS
TurboHelper.reloadTurbo(false, settings) // TODO: Can this go somewhere else? -OS
}
NativeLibrary.reloadSettings()
}
fun onSettingChanged() {

View File

@ -35,15 +35,8 @@ 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.IntListSetting
import org.citra.citra_emu.features.settings.model.ScaledFloatSetting
import org.citra.citra_emu.features.settings.model.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
@ -74,9 +67,9 @@ import java.text.SimpleDateFormat
import kotlin.math.roundToInt
class SettingsAdapter(
private val fragmentView: SettingsFragmentView,
public val context: Context
) : RecyclerView.Adapter<SettingViewHolder?>(), DialogInterface.OnClickListener,
val fragmentView: SettingsFragmentView,
val context: Context
) : RecyclerView.Adapter<SettingViewHolder<SettingsItem>?>(), DialogInterface.OnClickListener,
DialogInterface.OnMultiChoiceClickListener {
private var settings: ArrayList<SettingsItem>? = null
private var clickedItem: SettingsItem? = null
@ -94,7 +87,7 @@ class SettingsAdapter(
clickedPosition = -1
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder<*> {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
SettingsItem.TYPE_HEADER -> {
@ -144,7 +137,7 @@ class SettingsAdapter(
}
}
override fun onBindViewHolder(holder: SettingViewHolder, position: Int) {
override fun onBindViewHolder(holder: SettingViewHolder<SettingsItem>, position: Int) {
getItem(position)?.let { holder.bind(it) }
}
@ -226,8 +219,7 @@ class SettingsAdapter(
}
fun onBooleanClick(item: SwitchSetting, position: Int, checked: Boolean) {
val setting = item.setChecked(checked)
fragmentView.putSetting(setting)
item.setChecked(checked)
fragmentView.onSettingChanged()
// If statement is required otherwise the app will crash on activity recreate ex. theme settings
@ -338,8 +330,7 @@ class SettingsAdapter(
fragmentView.onSettingChanged()
}
notifyItemChanged(clickedPosition)
val setting = item.setSelectedValue(rtcString)
fragmentView.putSetting(setting)
item.setSelectedValue(rtcString)
fragmentView.loadSettingsList()
clickedItem = null
}
@ -359,7 +350,7 @@ class SettingsAdapter(
val sliderBinding = DialogSliderBinding.inflate(inflater)
textInputLayout = sliderBinding.textInput
textSliderValue = sliderBinding.textValue
if (item.setting is FloatSetting) {
if (item.setting?.defaultValue is Float) {
textSliderValue?.let {
it.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
it.setText(sliderProgress.toString())
@ -393,9 +384,9 @@ class SettingsAdapter(
})
addOnChangeListener { _: Slider, value: Float, _: Boolean ->
sliderProgress = (value * 100).roundToInt().toFloat() / 100f
sliderProgress = item.roundedFloat(value)
var sliderString = sliderProgress.toString()
if (item.setting !is FloatSetting) {
if (item.setting?.defaultValue !is Float) {
sliderString = sliderProgress.roundToInt().toString()
if (textSliderValue?.text.toString() != sliderString) {
textSliderValue?.setText(sliderString)
@ -418,16 +409,12 @@ class SettingsAdapter(
.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 ?: 0f
sliderBinding.slider.value = when (item.setting?.defaultValue) {
is Float -> item.setting!!.defaultValue as Float
is Int -> (item.setting!!.defaultValue as Int).toFloat()
else -> 0f
}
onClick(dialog, which)
onClick(dialog, which)
}
.show()
}
@ -478,26 +465,9 @@ class SettingsAdapter(
is SingleChoiceSetting -> {
val scSetting = clickedItem as? SingleChoiceSetting
scSetting?.let {
val setting = when (it.setting) {
is AbstractIntSetting -> {
val value = getValueForSingleChoiceSelection(it, which)
if (it.selectedValue != value) {
fragmentView?.onSettingChanged()
}
it.setSelectedValue(value)
}
is AbstractShortSetting -> {
val value = getValueForSingleChoiceSelection(it, which).toShort()
if (it.selectedValue.toShort() != value) {
fragmentView?.onSettingChanged()
}
it.setSelectedValue(value)
}
else -> throw IllegalStateException("Unrecognized type used for SingleChoiceSetting!")
}
fragmentView?.putSetting(setting)
val value = getValueForSingleChoiceSelection(it, which)
if (it.selectedValue != value) fragmentView?.onSettingChanged()
it.setSelectedValue(value)
fragmentView.loadSettingsList()
closeDialog()
}
@ -506,22 +476,9 @@ class SettingsAdapter(
is StringSingleChoiceSetting -> {
val scSetting = clickedItem as? StringSingleChoiceSetting
scSetting?.let {
val setting = when (it.setting) {
is AbstractStringSetting -> {
val value = it.getValueAt(which)
if (it.selectedValue != value) fragmentView?.onSettingChanged()
it.setSelectedValue(value ?: "")
}
is AbstractShortSetting -> {
if (it.selectValueIndex != which) fragmentView?.onSettingChanged()
it.setSelectedValue(it.getValueAt(which)?.toShort() ?: 1)
}
else -> throw IllegalStateException("Unrecognized type used for StringSingleChoiceSetting!")
}
fragmentView?.putSetting(setting)
val value = it.getValueAt(which) ?: ""
if (it.selectedValue != value) fragmentView?.onSettingChanged()
it.setSelectedValue(value)
fragmentView.loadSettingsList()
closeDialog()
}
@ -530,21 +487,12 @@ class SettingsAdapter(
is SliderSetting -> {
val sliderSetting = clickedItem as? SliderSetting
sliderSetting?.let {
val sliderval = (it.selectedFloat * 100).roundToInt().toFloat() / 100
if (sliderval != sliderProgress) {
fragmentView?.onSettingChanged()
}
when (it.setting) {
is AbstractIntSetting -> {
val value = sliderProgress.roundToInt()
val setting = it.setSelectedValue(value)
fragmentView?.putSetting(setting)
}
else -> {
val setting = it.setSelectedValue(sliderProgress)
fragmentView?.putSetting(setting)
}
val sliderval = it.roundedFloat(sliderProgress)
if (sliderval != it.selectedFloat) fragmentView.onSettingChanged()
val s = it.setting
when {
it.setting?.defaultValue is Int -> it.setSelectedValue(sliderProgress.roundToInt())
else -> it.setSelectedValue(sliderProgress)
}
fragmentView.loadSettingsList()
closeDialog()
@ -557,8 +505,7 @@ class SettingsAdapter(
if (it.selectedValue != textInputValue) {
fragmentView?.onSettingChanged()
}
val setting = it.setSelectedValue(textInputValue ?: "")
fragmentView?.putSetting(setting)
it.setSelectedValue(textInputValue)
fragmentView.loadSettingsList()
closeDialog()
}
@ -575,36 +522,19 @@ class SettingsAdapter(
mcsetting?.let {
val value = getValueForMultiChoiceSelection(it, which)
if (it.selectedValues.contains(value) != isChecked) {
val setting = it.setSelectedValue((if (isChecked) it.selectedValues + value else it.selectedValues - value).sorted())
fragmentView?.putSetting(setting)
fragmentView?.onSettingChanged()
it.setSelectedValue((if (isChecked) it.selectedValues + value else it.selectedValues - value).sorted())
fragmentView.onSettingChanged()
}
fragmentView.loadSettingsList()
}
}
fun onLongClick(setting: AbstractSetting, position: Int): Boolean {
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()
fragmentView.loadSettingsList()
resetSettingToDefault(setting, position)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
@ -612,6 +542,13 @@ class SettingsAdapter(
return true
}
fun <T> resetSettingToDefault(setting: AbstractSetting<T>, position: Int) {
fragmentView.activityView?.settings?.set(setting,setting.defaultValue)
notifyItemChanged(position)
fragmentView.onSettingChanged()
fragmentView.loadSettingsList()
}
fun onInputBindingLongClick(setting: InputBindingSetting, position: Int): Boolean {
MaterialAlertDialogBuilder(context)
.setMessage(R.string.reset_setting_confirmation)

View File

@ -13,16 +13,19 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
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.SettingsViewModel
import org.citra.citra_emu.features.settings.model.view.SettingsItem
import kotlin.getValue
class SettingsFragment : Fragment(), SettingsFragmentView {
override var activityView: SettingsActivityView? = null
private val fragmentPresenter = SettingsFragmentPresenter(this)
private val settingsViewModel: SettingsViewModel by activityViewModels()
private val fragmentPresenter by lazy { SettingsFragmentPresenter(this) }
private var settingsAdapter: SettingsAdapter? = null
private var _binding: FragmentSettingsBinding? = null
@ -37,7 +40,7 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
super.onCreate(savedInstanceState)
val menuTag = requireArguments().getString(ARGUMENT_MENU_TAG)
val gameId = requireArguments().getString(ARGUMENT_GAME_ID)
fragmentPresenter.onCreate(menuTag!!, gameId!!)
fragmentPresenter.onCreate(menuTag!!, gameId!!, settingsViewModel.settings)
}
override fun onCreateView(
@ -88,10 +91,6 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
activityView!!.showToastMessage(message!!, is_long)
}
override fun putSetting(setting: AbstractSetting) {
fragmentPresenter.putSetting(setting)
}
override fun onSettingChanged() {
activityView!!.onSettingChanged()
}

View File

@ -1,10 +1,9 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar 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
/**
@ -45,13 +44,6 @@ interface SettingsFragmentView {
*/
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.
*/

View File

@ -18,12 +18,11 @@ 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
SettingViewHolder<DateTimeSetting>(binding.root, adapter) {
override lateinit var setting: DateTimeSetting
@SuppressLint("SimpleDateFormat")
override fun bind(item: SettingsItem) {
setting = item as DateTimeSetting
setting = item as? DateTimeSetting ?: return
binding.textSettingName.setText(item.nameId)
if (item.descriptionId != 0) {
binding.textSettingDescription.visibility = View.VISIBLE

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -10,8 +10,8 @@ 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) {
SettingViewHolder<SettingsItem>(binding.root, adapter) {
override var setting = null
init {
itemView.setOnClickListener(null)
}

View File

@ -13,8 +13,8 @@ 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
SettingViewHolder<InputBindingSetting>(binding.root, adapter) {
override lateinit var setting: InputBindingSetting
override fun bind(item: SettingsItem) {
val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)

View File

@ -11,9 +11,8 @@ import org.citra.citra_emu.features.settings.model.view.MultiChoiceSetting
import org.citra.citra_emu.features.settings.ui.SettingsAdapter
class MultiChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
private lateinit var setting: SettingsItem
SettingViewHolder<SettingsItem>(binding.root, adapter) {
override lateinit var setting: SettingsItem
override fun bind(item: SettingsItem) {
setting = item
binding.textSettingName.setText(item.nameId)

View File

@ -14,8 +14,8 @@ 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
SettingViewHolder<RunnableSetting>(binding.root, adapter) {
override lateinit var setting: RunnableSetting
override fun bind(item: SettingsItem) {
setting = item as RunnableSetting

View File

@ -9,14 +9,17 @@ 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) :
abstract class SettingViewHolder<out T: SettingsItem>(itemView: View, protected val adapter: SettingsAdapter) :
RecyclerView.ViewHolder(itemView), View.OnClickListener, View.OnLongClickListener {
init {
itemView.setOnClickListener(this)
itemView.setOnLongClickListener(this)
}
/**
* The SettingsItem we are holding
*/
abstract val setting: T?
/**
* Called by the adapter to set this ViewHolder's child views to display the list item
* it must now represent.

View File

@ -12,8 +12,8 @@ import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSettin
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
SettingViewHolder<SettingsItem>(binding.root, adapter) {
override lateinit var setting: SettingsItem
override fun bind(item: SettingsItem) {
setting = item

View File

@ -6,17 +6,14 @@ 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
SettingViewHolder<SliderSetting>(binding.root, adapter) {
override lateinit var setting: SliderSetting
override fun bind(item: SettingsItem) {
setting = item as SliderSetting
@ -28,12 +25,7 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
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}"
}
binding.textSettingValue.text = "${setting.valueAsString}${setting.units}"
if (setting.isActive) {
binding.textSettingName.alpha = 1f

View File

@ -11,11 +11,10 @@ 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
SettingViewHolder<StringInputSetting>(binding.root, adapter) {
override lateinit var setting: StringInputSetting
override fun bind(item: SettingsItem) {
setting = item
setting = item as StringInputSetting
binding.textSettingName.setText(item.nameId)
if (item.descriptionId != 0) {
binding.textSettingDescription.visibility = View.VISIBLE
@ -24,7 +23,7 @@ class StringInputViewHolder(val binding: ListItemSettingBinding, adapter: Settin
binding.textSettingDescription.visibility = View.GONE
}
binding.textSettingValue.visibility = View.VISIBLE
binding.textSettingValue.text = setting.setting?.valueAsString
binding.textSettingValue.text = setting.selectedValue
if (setting.isActive) {
binding.textSettingName.alpha = 1f
@ -42,7 +41,7 @@ class StringInputViewHolder(val binding: ListItemSettingBinding, adapter: Settin
adapter.onClickDisabledSetting(!setting.isEditable)
return
}
adapter.onStringInputClick((setting as StringInputSetting), bindingAdapterPosition)
adapter.onStringInputClick(setting, bindingAdapterPosition)
}
override fun onLongClick(clicked: View): Boolean {

View File

@ -12,11 +12,11 @@ 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
SettingViewHolder<SubmenuSetting>(binding.root, adapter) {
override lateinit var setting: SubmenuSetting
override fun bind(item: SettingsItem) {
this.item = item as SubmenuSetting
setting = item as SubmenuSetting
if (item.iconId == 0) {
binding.icon.visibility = View.GONE
} else {
@ -40,7 +40,7 @@ class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAd
}
override fun onClick(clicked: View) {
adapter.onSubmenuClick(item)
adapter.onSubmenuClick(setting)
}
override fun onLongClick(clicked: View): Boolean {

View File

@ -12,9 +12,9 @@ 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) {
SettingViewHolder<SwitchSetting>(binding.root, adapter) {
private lateinit var setting: SwitchSetting
override lateinit var setting: SwitchSetting
override fun bind(item: SettingsItem) {
setting = item as SwitchSetting

View File

@ -14,12 +14,9 @@ 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.IntListSetting
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.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.userDirectory
import org.citra.citra_emu.utils.Log
import org.ini4j.Wini
@ -27,7 +24,6 @@ import java.io.BufferedReader
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStreamReader
import java.util.TreeMap
/**
@ -36,40 +32,46 @@ import java.util.TreeMap
object SettingsFile {
const val FILE_NAME_CONFIG = "config"
private var sectionsMap = BiMap<String?, String?>()
private val allSettings: List<AbstractSetting<*>> by lazy {
BooleanSetting.values().toList() +
IntSetting.values().toList() +
FloatSetting.values().toList() +
StringSetting.values().toList() +
IntListSetting.values().toList()
}
private fun findSettingByKey(key: String): AbstractSetting<*>? =
allSettings.firstOrNull { it.key == key }
/**
* 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.
* Reads a given .ini file from disk and updates a instance of the Settings class appropriately
*
* @param ini The ini file to load the settings from
* @param settings The Settings instance to edit
* @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,
settings: Settings,
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 currentSection: String? = 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)
}
if (line!!.startsWith("[") && line.endsWith("]")) {
currentSection = line.substring(1, line.length-1)
} else if (currentSection != null) {
val pair = parseLineToKeyValuePair(line) ?: continue
val (key, rawValue) = pair
val descriptor = findSettingByKey(key) ?: continue
loadSettingInto(settings, descriptor, rawValue, isCustomGame)
}
}
} catch (e: FileNotFoundException) {
@ -87,102 +89,152 @@ object SettingsFile {
}
}
}
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.
* Load global settings from the config file into the settings instance
*/
fun readCustomGameSettings(
gameId: String,
view: SettingsActivityView?
): HashMap<String, SettingSection?> {
return readFile(getCustomGameSettingsFile(gameId), true, view)
fun loadSettings(settings: Settings, view: SettingsActivityView? = null) {
readFile(getSettingsFile(FILE_NAME_CONFIG),settings,false,view)
}
/**
* Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error
* Load global settings AND custom settings into the settings instance, sets gameId
*/
fun loadSettings(settings: Settings, gameId: String, view: SettingsActivityView? = null) {
settings.gameId = gameId
loadSettings(settings, view)
val file = findCustomGameSettingsFile(gameId) ?: return
readFile(file, settings, true, view)
}
/**
* Uses the settings object to parse the raw string and store it in the correct map
*/
@Suppress("UNCHECKED_CAST")
private fun <T> loadSettingInto(
settings: Settings,
setting: AbstractSetting<T>,
rawValue: String,
isCustomGame: Boolean
) {
val value = setting.valueFromString(rawValue) ?: return
if (isCustomGame) {
settings.setOverride(setting, value)
} else {
settings.setGlobal(setting, value)
}
}
/**
* Saves a the global settings from a Settings instance
* to the global .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 settings The Settings instance we are saving
* @param view The current view.
*/
fun saveFile(
fileName: String,
sections: TreeMap<String, SettingSection?>,
view: SettingsActivityView
fun saveGlobalFile(
settings: Settings,
view: SettingsActivityView? = null
) {
val ini = getSettingsFile(fileName)
val ini = getSettingsFile(FILE_NAME_CONFIG)
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()
for (setting in allSettings) {
val value = settings.getGlobal(setting) ?: continue
writeSettingToWini(writer, setting, value)
}
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(
Log.error("[SettingsFile] File not found: $FILE_NAME_CONFIG.ini: ${e.message}")
view?.showToastMessage(
CitraApplication.appContext
.getString(R.string.error_saving, fileName, e.message), false
.getString(R.string.error_saving, FILE_NAME_CONFIG, e.message), false
)
}
}
fun saveFile(
fileName: String,
setting: AbstractSetting
/**
* Save the per-game overrides to a per-game config file
*/
fun saveCustomFile(
settings: Settings,
view: SettingsActivityView? = null
) {
val ini = getSettingsFile(fileName)
if (!settings.isPerGame()) return
val ini = getOrCreateCustomGameSettingsFile(settings.gameId!!)
try {
val context: Context = CitraApplication.appContext
val writer = Wini()
val overrides = settings.getAllOverrides()
for (descriptor in allSettings) {
val value = overrides[descriptor.key] ?: continue
writeSettingToWini(writer, descriptor, value)
}
val outputStream = context.contentResolver.openOutputStream(ini.uri, "wt")
writer.store(outputStream)
outputStream?.flush()
outputStream?.close()
} catch (e: Exception) {
Log.error("[SettingsFile] Error saving custom file for ${settings.gameId}: ${e.message}")
view?.onSettingsFileNotFound()
}
}
fun <T> saveSetting(setting: AbstractSetting<T>, settings: Settings) {
if (settings.hasOverride(setting)) {
// Currently a per-game setting, keep it that way
val ini = getOrCreateCustomGameSettingsFile(settings.gameId!!)
writeSingleSettingToFile(ini, setting, settings.get(setting))
} else {
// Currently global, save to global file
val ini = getSettingsFile(FILE_NAME_CONFIG)
writeSingleSettingToFile(ini, setting, settings.getGlobal(setting))
}
}
private fun <T> writeSingleSettingToFile(ini: DocumentFile, setting: AbstractSetting<T>, value: T) {
try {
val context = CitraApplication.appContext
val inputStream = context.contentResolver.openInputStream(ini.uri)
val writer = Wini(inputStream)
writer.put(setting.section, setting.key, setting.valueAsString)
inputStream!!.close()
val writer = if (inputStream != null) Wini(inputStream) else Wini()
inputStream?.close()
writeSettingToWini(writer, setting, value as Any)
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}")
Log.error("[SettingsFile] Error saving setting ${setting.key}: ${e.message}")
}
}
@Suppress("UNCHECKED_CAST")
private fun <T> writeSettingToWini(writer: Wini, descriptor: AbstractSetting<T>, value: Any) {
val typedValue = value as T
writer.put(descriptor.section, descriptor.key, descriptor.valueToString(typedValue))
}
private fun mapSectionNameFromIni(generalSectionName: String): String? {
return if (sectionsMap.getForward(generalSectionName) != null) {
sectionsMap.getForward(generalSectionName)
} else {
generalSectionName
}
private fun parseLineToKeyValuePair(line: String): Pair<String, String>? {
val splitLine = line.split("=".toRegex(), limit = 2)
if (splitLine.size != 2) return null
val key = splitLine[0].trim()
val value = splitLine[1].trim()
if (value.isEmpty()) return null
return Pair(key, value)
}
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))
@ -190,96 +242,20 @@ object SettingsFile {
return configDirectory!!.findFile("$fileName.ini")!!
}
private fun getCustomGameSettingsFile(gameId: String): DocumentFile {
fun customExists(gameId: String): Boolean = findCustomGameSettingsFile(gameId) != null
private fun findCustomGameSettingsFile(gameId: String): DocumentFile? {
val root = DocumentFile.fromTreeUri(CitraApplication.appContext, Uri.parse(userDirectory))
val configDirectory = root!!.findFile("GameSettings")
return configDirectory!!.findFile("$gameId.ini")!!
val configDir = root?.findFile("config") ?: return null
val customDir = configDir.findFile("custom") ?: return null
return customDir.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
}
val intListSetting = IntListSetting.from(key)
if (intListSetting != null) {
intListSetting.list = value.split(", ").map { it.toInt() }
}
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)
}
private fun getOrCreateCustomGameSettingsFile(gameId: String): DocumentFile {
val root = DocumentFile.fromTreeUri(CitraApplication.appContext, Uri.parse(userDirectory))!!
val configDir = root.findFile("config") ?: root.createDirectory("config")!!
val customDir = configDir.findFile("custom") ?: configDir.createDirectory("custom")!!
return customDir.findFile("$gameId.ini")
?: customDir.createFile("*/*", "$gameId.ini")!!
}
}

View File

@ -35,6 +35,7 @@ import android.widget.PopupMenu
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.Insets
@ -44,7 +45,6 @@ import androidx.drawerlayout.widget.DrawerLayout
import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
@ -70,7 +70,6 @@ import org.citra.citra_emu.display.ScreenLayout
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.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.model.Game
@ -78,7 +77,6 @@ import org.citra.citra_emu.utils.BuildUtil
import org.citra.citra_emu.utils.DirectoryInitialization
import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState
import org.citra.citra_emu.utils.EmulationMenuSettings
import org.citra.citra_emu.utils.FileUtil
import org.citra.citra_emu.utils.GameHelper
import org.citra.citra_emu.utils.GameIconUtils
import org.citra.citra_emu.utils.EmulationLifecycleUtil
@ -104,8 +102,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil
private val emulationViewModel: EmulationViewModel by activityViewModels()
private val settingsViewModel: SettingsViewModel by viewModels()
private val settings get() = settingsViewModel.settings
private val onPause = Runnable{ togglePause() }
private val onShutdown = Runnable{ emulationState.stop() }
@ -184,7 +180,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
retainInstance = true
emulationState = EmulationState(game.path)
emulationActivity = requireActivity() as EmulationActivity
screenAdjustmentUtil = ScreenAdjustmentUtil(requireContext(), requireActivity().windowManager, settings)
screenAdjustmentUtil = ScreenAdjustmentUtil(requireContext(), requireActivity().windowManager, Settings.settings)
EmulationLifecycleUtil.addPauseResumeHook(onPause)
EmulationLifecycleUtil.addShutdownHook(onShutdown)
}
@ -211,7 +207,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
if (requireActivity().isFinishing) {
return
}
binding.surfaceInputOverlay.initializeSettings(Settings.settings)
binding.surfaceEmulation.holder.addCallback(this)
binding.doneControlConfig.setOnClickListener {
binding.doneControlConfig.visibility = View.GONE
@ -221,7 +217,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
// Show/hide the "Stats" overlay
updateShowPerformanceOverlay()
val position = IntSetting.PERFORMANCE_OVERLAY_POSITION.int
val position = Settings.settings.get(IntSetting.PERFORMANCE_OVERLAY_POSITION)
updateStatsPosition(position)
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
@ -382,10 +378,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
R.id.menu_settings -> {
SettingsActivity.launch(
requireContext(),
SettingsFile.FILE_NAME_CONFIG,
""
SettingsFile.FILE_NAME_CONFIG, null
)
true
}
@ -508,7 +502,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
emulationState.unpause()
// If the overlay is enabled, we need to update the position if changed
val position = IntSetting.PERFORMANCE_OVERLAY_POSITION.int
val position = Settings.settings.get(IntSetting.PERFORMANCE_OVERLAY_POSITION)
updateStatsPosition(position)
binding.inGameMenu.menu.findItem(R.id.menu_emulation_pause)?.let { menuItem ->
@ -707,7 +701,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.menu.apply {
findItem(R.id.menu_show_overlay).isChecked = EmulationMenuSettings.showOverlay
findItem(R.id.menu_performance_overlay_show).isChecked =
BooleanSetting.PERF_OVERLAY_ENABLE.boolean
Settings.settings.get(BooleanSetting.PERF_OVERLAY_ENABLE)
findItem(R.id.menu_haptic_feedback).isChecked = EmulationMenuSettings.hapticFeedback
findItem(R.id.menu_emulation_joystick_rel_center).isChecked =
EmulationMenuSettings.joystickRelCenter
@ -724,8 +718,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
}
R.id.menu_performance_overlay_show -> {
BooleanSetting.PERF_OVERLAY_ENABLE.boolean = !BooleanSetting.PERF_OVERLAY_ENABLE.boolean
settings.saveSetting(BooleanSetting.PERF_OVERLAY_ENABLE, SettingsFile.FILE_NAME_CONFIG)
Settings.settings.update(BooleanSetting.PERF_OVERLAY_ENABLE,
Settings.settings.get(BooleanSetting.PERF_OVERLAY_ENABLE))
SettingsFile.saveSetting(BooleanSetting.PERF_OVERLAY_ENABLE, Settings.settings)
updateShowPerformanceOverlay()
true
}
@ -921,7 +916,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.menuInflater.inflate(R.menu.menu_landscape_screen_layout, popupMenu.menu)
val layoutOptionMenuItem = when (IntSetting.SCREEN_LAYOUT.int) {
val layoutOptionMenuItem = when (Settings.settings.get(IntSetting.SCREEN_LAYOUT)) {
ScreenLayout.ORIGINAL.int ->
R.id.menu_screen_layout_original
@ -993,7 +988,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.menuInflater.inflate(R.menu.menu_portrait_screen_layout, popupMenu.menu)
val layoutOptionMenuItem = when (IntSetting.PORTRAIT_SCREEN_LAYOUT.int) {
val layoutOptionMenuItem = when (Settings.settings.get(IntSetting.PORTRAIT_SCREEN_LAYOUT)) {
PortraitScreenLayout.TOP_FULL_WIDTH.int ->
R.id.menu_portrait_layout_top_full
PortraitScreenLayout.ORIGINAL.int ->
@ -1260,7 +1255,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!)
}
if (BooleanSetting.PERF_OVERLAY_ENABLE.boolean) {
if (Settings.settings.get(BooleanSetting.PERF_OVERLAY_ENABLE)) {
val SYSTEM_FPS = 0
val FPS = 1
val SPEED = 2
@ -1275,11 +1270,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
val perfStats = NativeLibrary.getPerfStats()
val dividerString = "\u00A0\u2502 "
if (perfStats[FPS] > 0) {
if (BooleanSetting.PERF_OVERLAY_SHOW_FPS.boolean) {
if (Settings.settings.get(BooleanSetting.PERF_OVERLAY_SHOW_FPS)) {
sb.append(String.format("FPS:\u00A0%d", (perfStats[FPS] + 0.5).toInt()))
}
if (BooleanSetting.PERF_OVERLAY_SHOW_FRAMETIME.boolean) {
if (Settings.settings.get(BooleanSetting.PERF_OVERLAY_SHOW_FRAMETIME)) {
if (sb.isNotEmpty()) sb.append(dividerString)
sb.append(
String.format(
@ -1294,7 +1289,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
)
}
if (BooleanSetting.PERF_OVERLAY_SHOW_SPEED.boolean) {
if (Settings.settings.get(BooleanSetting.PERF_OVERLAY_SHOW_SPEED)) {
if (sb.isNotEmpty()) sb.append(dividerString)
sb.append(
String.format(
@ -1304,14 +1299,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
)
}
if (BooleanSetting.PERF_OVERLAY_SHOW_APP_RAM_USAGE.boolean) {
if (Settings.settings.get(BooleanSetting.PERF_OVERLAY_SHOW_APP_RAM_USAGE)) {
if (sb.isNotEmpty()) sb.append(dividerString)
val appRamUsage =
File("/proc/self/statm").readLines()[0].split(' ')[1].toLong() * 4096 / 1000000
sb.append("Process\u00A0RAM:\u00A0$appRamUsage\u00A0MB")
}
if (BooleanSetting.PERF_OVERLAY_SHOW_AVAILABLE_RAM.boolean) {
if (Settings.settings.get(BooleanSetting.PERF_OVERLAY_SHOW_AVAILABLE_RAM)) {
if (sb.isNotEmpty()) sb.append(dividerString)
context?.let { ctx ->
val activityManager =
@ -1324,14 +1319,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
}
}
if (BooleanSetting.PERF_OVERLAY_SHOW_BATTERY_TEMP.boolean) {
if (Settings.settings.get(BooleanSetting.PERF_OVERLAY_SHOW_BATTERY_TEMP)) {
if (sb.isNotEmpty()) sb.append(dividerString)
val batteryTemp = getBatteryTemperature()
val tempF = celsiusToFahrenheit(batteryTemp)
sb.append(String.format("%.1f°C/%.1f°F", batteryTemp, tempF))
}
if (BooleanSetting.PERF_OVERLAY_BACKGROUND.boolean) {
if (Settings.settings.get(BooleanSetting.PERF_OVERLAY_BACKGROUND)) {
binding.performanceOverlayShowText.setBackgroundResource(R.color.citra_transparent_black)
} else {
binding.performanceOverlayShowText.setBackgroundResource(0)

View File

@ -24,6 +24,7 @@ import androidx.preference.PreferenceManager
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.Settings
import org.citra.citra_emu.utils.EmulationMenuSettings
import org.citra.citra_emu.utils.TurboHelper
import java.lang.NullPointerException
@ -45,7 +46,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
private var buttonBeingConfigured: InputOverlayDrawableButton? = null
private var dpadBeingConfigured: InputOverlayDrawableDpad? = null
private var joystickBeingConfigured: InputOverlayDrawableJoystick? = null
private val settingsViewModel = NativeLibrary.sEmulationActivity.get()!!.settingsViewModel
private lateinit var settings: Settings
// Stores the ID of the pointer that interacted with the 3DS touchscreen.
private var touchscreenPointerId = -1
@ -71,6 +72,10 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
requestFocus()
}
fun initializeSettings(settings: Settings) {
this.settings = settings
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
overlayButtons.forEach { it.draw(canvas) }
@ -173,7 +178,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
swapScreen()
}
else if (button.id == NativeLibrary.ButtonType.BUTTON_TURBO && button.status == NativeLibrary.ButtonState.PRESSED) {
TurboHelper.toggleTurbo(true)
TurboHelper.toggleTurbo(true, settings)
}
NativeLibrary.onGamePadEvent(

View File

@ -48,7 +48,6 @@ import org.citra.citra_emu.R
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.GrantMissingFilesystemPermissionFragment
@ -72,7 +71,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
private val homeViewModel: HomeViewModel by viewModels()
private val gamesViewModel: GamesViewModel by viewModels()
private val settingsViewModel: SettingsViewModel by viewModels()
override var themeId: Int = 0
@ -95,7 +93,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
if (PermissionsHandler.hasWriteAccess(applicationContext) &&
DirectoryInitialization.areCitraDirectoriesReady() &&
!CitraDirectoryUtils.needToUpdateManually()) {
settingsViewModel.settings.loadSettings()
// load the global settings from the config file at program launch
SettingsFile.loadSettings(Settings.settings)
}
ThemeUtil.ThemeChangeListener(this)

View File

@ -9,6 +9,7 @@ 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.IntSetting
import org.citra.citra_emu.features.settings.model.Settings
object TurboHelper {
private var turboSpeedEnabled = false
@ -17,12 +18,12 @@ object TurboHelper {
return turboSpeedEnabled
}
fun reloadTurbo(showToast: Boolean) {
fun reloadTurbo(showToast: Boolean, settings: Settings) {
val context = CitraApplication.appContext
val toastMessage: String
if (turboSpeedEnabled) {
NativeLibrary.setTemporaryFrameLimit(IntSetting.TURBO_LIMIT.int.toDouble())
NativeLibrary.setTemporaryFrameLimit(settings.get(IntSetting.TURBO_LIMIT).toDouble())
toastMessage = context.getString(R.string.turbo_enabled_toast)
} else {
NativeLibrary.disableTemporaryFrameLimit()
@ -34,12 +35,12 @@ object TurboHelper {
}
}
fun setTurboEnabled(state: Boolean, showToast: Boolean) {
fun setTurboEnabled(state: Boolean, showToast: Boolean, settings: Settings) {
turboSpeedEnabled = state
reloadTurbo(showToast)
reloadTurbo(showToast, settings)
}
fun toggleTurbo(showToast: Boolean) {
setTurboEnabled(!TurboHelper.isTurboSpeedEnabled(), showToast)
fun toggleTurbo(showToast: Boolean, settings: Settings) {
setTurboEnabled(!TurboHelper.isTurboSpeedEnabled(), showToast, settings)
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -7,9 +7,13 @@ package org.citra.citra_emu.viewmodel
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.citra.citra_emu.features.settings.model.Settings
class EmulationViewModel : ViewModel() {
val emulationStarted get() = _emulationStarted.asStateFlow()
// convenience shortcut for
val settings = Settings.settings
private val _emulationStarted = MutableStateFlow(false)
val shaderProgress get() = _shaderProgress.asStateFlow()
@ -21,6 +25,7 @@ class EmulationViewModel : ViewModel() {
val shaderMessage get() = _shaderMessage.asStateFlow()
private val _shaderMessage = MutableStateFlow("")
fun setShaderProgress(progress: Int) {
_shaderProgress.value = progress
}