Merge pull request #11380 from t895/settings-integration

android: Settings rework
This commit is contained in:
Charles Lombardo 2023-08-29 22:20:59 -04:00 committed by GitHub
commit 44bce11853
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 2221 additions and 2111 deletions

View File

@ -219,10 +219,6 @@ object NativeLibrary {
external fun reloadSettings() external fun reloadSettings()
external fun getUserSetting(gameID: String?, Section: String?, Key: String?): String?
external fun setUserSetting(gameID: String?, Section: String?, Key: String?, Value: String?)
external fun initGameIni(gameID: String?) external fun initGameIni(gameID: String?)
/** /**
@ -413,14 +409,17 @@ object NativeLibrary {
details.ifEmpty { emulationActivity.getString(R.string.system_archive_general) } details.ifEmpty { emulationActivity.getString(R.string.system_archive_general) }
) )
} }
CoreError.ErrorSavestate -> { CoreError.ErrorSavestate -> {
title = emulationActivity.getString(R.string.save_load_error) title = emulationActivity.getString(R.string.save_load_error)
message = details message = details
} }
CoreError.ErrorUnknown -> { CoreError.ErrorUnknown -> {
title = emulationActivity.getString(R.string.fatal_error) title = emulationActivity.getString(R.string.fatal_error)
message = emulationActivity.getString(R.string.fatal_error_message) message = emulationActivity.getString(R.string.fatal_error_message)
} }
else -> { else -> {
return true return true
} }
@ -454,6 +453,7 @@ object NativeLibrary {
captionId = R.string.loader_error_video_core captionId = R.string.loader_error_video_core
descriptionId = R.string.loader_error_video_core_description descriptionId = R.string.loader_error_video_core_description
} }
else -> { else -> {
captionId = R.string.loader_error_encrypted captionId = R.string.loader_error_encrypted
descriptionId = R.string.loader_error_encrypted_roms_description descriptionId = R.string.loader_error_encrypted_roms_description

View File

@ -46,7 +46,7 @@ class YuzuApplication : Application() {
super.onCreate() super.onCreate()
application = this application = this
documentsTree = DocumentsTree() documentsTree = DocumentsTree()
DirectoryInitialization.start(applicationContext) DirectoryInitialization.start()
GpuDriverHelper.initializeDriverParameters(applicationContext) GpuDriverHelper.initializeDriverParameters(applicationContext)
NativeLibrary.logDeviceInfo() NativeLibrary.logDeviceInfo()

View File

@ -28,7 +28,6 @@ import android.view.Surface
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
@ -42,7 +41,6 @@ import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.utils.ControllerMappingHelper import org.yuzu.yuzu_emu.utils.ControllerMappingHelper
import org.yuzu.yuzu_emu.utils.ForegroundService import org.yuzu.yuzu_emu.utils.ForegroundService
@ -72,8 +70,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
private val actionMute = "ACTION_EMULATOR_MUTE" private val actionMute = "ACTION_EMULATOR_MUTE"
private val actionUnmute = "ACTION_EMULATOR_UNMUTE" private val actionUnmute = "ACTION_EMULATOR_UNMUTE"
private val settingsViewModel: SettingsViewModel by viewModels()
override fun onDestroy() { override fun onDestroy() {
stopForegroundService(this) stopForegroundService(this)
super.onDestroy() super.onDestroy()
@ -82,8 +78,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
ThemeHelper.setTheme(this) ThemeHelper.setTheme(this)
settingsViewModel.settings.loadSettings()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityEmulationBinding.inflate(layoutInflater) binding = ActivityEmulationBinding.inflate(layoutInflater)
@ -91,9 +85,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
val navHostFragment = val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
val navController = navHostFragment.navController navHostFragment.navController.setGraph(R.navigation.emulation_navigation, intent.extras)
navController
.setGraph(R.navigation.emulation_navigation, intent.extras)
isActivityRecreated = savedInstanceState != null isActivityRecreated = savedInstanceState != null

View File

@ -4,5 +4,7 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
interface AbstractBooleanSetting : AbstractSetting { interface AbstractBooleanSetting : AbstractSetting {
var boolean: Boolean val boolean: Boolean
fun setBoolean(value: Boolean)
} }

View File

@ -3,8 +3,8 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
import androidx.lifecycle.ViewModel interface AbstractByteSetting : AbstractSetting {
val byte: Byte
class SettingsViewModel : ViewModel() { fun setByte(value: Byte)
val settings = Settings()
} }

View File

@ -4,5 +4,7 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
interface AbstractFloatSetting : AbstractSetting { interface AbstractFloatSetting : AbstractSetting {
var float: Float val float: Float
fun setFloat(value: Float)
} }

View File

@ -4,5 +4,7 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
interface AbstractIntSetting : AbstractSetting { interface AbstractIntSetting : AbstractSetting {
var int: Int val int: Int
fun setInt(value: Int)
} }

View File

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model
interface AbstractLongSetting : AbstractSetting {
val long: Long
fun setLong(value: Long)
}

View File

@ -3,10 +3,22 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
interface AbstractSetting { interface AbstractSetting {
val key: String? val key: String
val section: String? val category: Settings.Category
val isRuntimeEditable: Boolean
val valueAsString: String
val defaultValue: Any val defaultValue: Any
val androidDefault: Any?
get() = null
val valueAsString: String
get() = ""
val isRuntimeModifiable: Boolean
get() = NativeConfig.getIsRuntimeModifiable(key)
val pairedSettingKey: String
get() = NativeConfig.getPairedSettingKey(key)
fun reset()
} }

View File

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model
interface AbstractShortSetting : AbstractSetting {
val short: Short
fun setShort(value: Short)
}

View File

@ -4,5 +4,7 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
interface AbstractStringSetting : AbstractSetting { interface AbstractStringSetting : AbstractSetting {
var string: String val string: String
fun setString(value: String)
} }

View File

@ -3,41 +3,37 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class BooleanSetting( enum class BooleanSetting(
override val key: String, override val key: String,
override val section: String, override val category: Settings.Category,
override val defaultValue: Boolean override val androidDefault: Boolean? = null
) : AbstractBooleanSetting { ) : AbstractBooleanSetting {
CPU_DEBUG_MODE("cpu_debug_mode", Settings.SECTION_CPU, false), CPU_DEBUG_MODE("cpu_debug_mode", Settings.Category.Cpu),
FASTMEM("cpuopt_fastmem", Settings.SECTION_CPU, true), FASTMEM("cpuopt_fastmem", Settings.Category.Cpu),
FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives", Settings.SECTION_CPU, true), FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives", Settings.Category.Cpu),
PICTURE_IN_PICTURE("picture_in_picture", Settings.SECTION_GENERAL, true), RENDERER_USE_SPEED_LIMIT("use_speed_limit", Settings.Category.Core),
USE_CUSTOM_RTC("custom_rtc_enabled", Settings.SECTION_SYSTEM, false); USE_DOCKED_MODE("use_docked_mode", Settings.Category.System, false),
RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache", Settings.Category.Renderer),
RENDERER_FORCE_MAX_CLOCK("force_max_clock", Settings.Category.Renderer),
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders", Settings.Category.Renderer),
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing", Settings.Category.Renderer, false),
RENDERER_DEBUG("debug", Settings.Category.Renderer),
PICTURE_IN_PICTURE("picture_in_picture", Settings.Category.Android),
USE_CUSTOM_RTC("custom_rtc_enabled", Settings.Category.System);
override var boolean: Boolean = defaultValue override val boolean: Boolean
get() = NativeConfig.getBoolean(key, false)
override fun setBoolean(value: Boolean) = NativeConfig.setBoolean(key, value)
override val defaultValue: Boolean by lazy {
androidDefault ?: NativeConfig.getBoolean(key, true)
}
override val valueAsString: String override val valueAsString: String
get() = boolean.toString() get() = if (boolean) "1" else "0"
override val isRuntimeEditable: Boolean override fun reset() = NativeConfig.setBoolean(key, defaultValue)
get() {
for (setting in NOT_RUNTIME_EDITABLE) {
if (setting == this) {
return false
}
}
return true
}
companion object {
private val NOT_RUNTIME_EDITABLE = listOf(
PICTURE_IN_PICTURE,
USE_CUSTOM_RTC
)
fun from(key: String): BooleanSetting? =
BooleanSetting.values().firstOrNull { it.key == key }
fun clear() = BooleanSetting.values().forEach { it.boolean = it.defaultValue }
}
} }

View File

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class ByteSetting(
override val key: String,
override val category: Settings.Category
) : AbstractByteSetting {
AUDIO_VOLUME("volume", Settings.Category.Audio);
override val byte: Byte
get() = NativeConfig.getByte(key, false)
override fun setByte(value: Byte) = NativeConfig.setByte(key, value)
override val defaultValue: Byte by lazy { NativeConfig.getByte(key, true) }
override val valueAsString: String
get() = byte.toString()
override fun reset() = NativeConfig.setByte(key, defaultValue)
}

View File

@ -3,34 +3,24 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class FloatSetting( enum class FloatSetting(
override val key: String, override val key: String,
override val section: String, override val category: Settings.Category
override val defaultValue: Float
) : AbstractFloatSetting { ) : AbstractFloatSetting {
// No float settings currently exist // No float settings currently exist
EMPTY_SETTING("", "", 0f); EMPTY_SETTING("", Settings.Category.UiGeneral);
override var float: Float = defaultValue override val float: Float
get() = NativeConfig.getFloat(key, false)
override fun setFloat(value: Float) = NativeConfig.setFloat(key, value)
override val defaultValue: Float by lazy { NativeConfig.getFloat(key, true) }
override val valueAsString: String override val valueAsString: String
get() = float.toString() get() = float.toString()
override val isRuntimeEditable: Boolean override fun reset() = NativeConfig.setFloat(key, defaultValue)
get() {
for (setting in NOT_RUNTIME_EDITABLE) {
if (setting == this) {
return false
}
}
return true
}
companion object {
private val NOT_RUNTIME_EDITABLE = emptyList<FloatSetting>()
fun from(key: String): FloatSetting? = FloatSetting.values().firstOrNull { it.key == key }
fun clear() = FloatSetting.values().forEach { it.float = it.defaultValue }
}
} }

View File

@ -3,139 +3,37 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class IntSetting( enum class IntSetting(
override val key: String, override val key: String,
override val section: String, override val category: Settings.Category,
override val defaultValue: Int override val androidDefault: Int? = null
) : AbstractIntSetting { ) : AbstractIntSetting {
RENDERER_USE_SPEED_LIMIT( CPU_ACCURACY("cpu_accuracy", Settings.Category.Cpu),
"use_speed_limit", REGION_INDEX("region_index", Settings.Category.System),
Settings.SECTION_RENDERER, LANGUAGE_INDEX("language_index", Settings.Category.System),
1 RENDERER_BACKEND("backend", Settings.Category.Renderer),
), RENDERER_ACCURACY("gpu_accuracy", Settings.Category.Renderer, 0),
USE_DOCKED_MODE( RENDERER_RESOLUTION("resolution_setup", Settings.Category.Renderer),
"use_docked_mode", RENDERER_VSYNC("use_vsync", Settings.Category.Renderer),
Settings.SECTION_SYSTEM, RENDERER_SCALING_FILTER("scaling_filter", Settings.Category.Renderer),
0 RENDERER_ANTI_ALIASING("anti_aliasing", Settings.Category.Renderer),
), RENDERER_SCREEN_LAYOUT("screen_layout", Settings.Category.Android),
RENDERER_USE_DISK_SHADER_CACHE( RENDERER_ASPECT_RATIO("aspect_ratio", Settings.Category.Renderer),
"use_disk_shader_cache", AUDIO_OUTPUT_ENGINE("output_engine", Settings.Category.Audio);
Settings.SECTION_RENDERER,
1
),
RENDERER_FORCE_MAX_CLOCK(
"force_max_clock",
Settings.SECTION_RENDERER,
0
),
RENDERER_ASYNCHRONOUS_SHADERS(
"use_asynchronous_shaders",
Settings.SECTION_RENDERER,
0
),
RENDERER_REACTIVE_FLUSHING(
"use_reactive_flushing",
Settings.SECTION_RENDERER,
0
),
RENDERER_DEBUG(
"debug",
Settings.SECTION_RENDERER,
0
),
RENDERER_SPEED_LIMIT(
"speed_limit",
Settings.SECTION_RENDERER,
100
),
CPU_ACCURACY(
"cpu_accuracy",
Settings.SECTION_CPU,
0
),
REGION_INDEX(
"region_index",
Settings.SECTION_SYSTEM,
-1
),
LANGUAGE_INDEX(
"language_index",
Settings.SECTION_SYSTEM,
1
),
RENDERER_BACKEND(
"backend",
Settings.SECTION_RENDERER,
1
),
RENDERER_ACCURACY(
"gpu_accuracy",
Settings.SECTION_RENDERER,
0
),
RENDERER_RESOLUTION(
"resolution_setup",
Settings.SECTION_RENDERER,
2
),
RENDERER_VSYNC(
"use_vsync",
Settings.SECTION_RENDERER,
0
),
RENDERER_SCALING_FILTER(
"scaling_filter",
Settings.SECTION_RENDERER,
1
),
RENDERER_ANTI_ALIASING(
"anti_aliasing",
Settings.SECTION_RENDERER,
0
),
RENDERER_SCREEN_LAYOUT(
"screen_layout",
Settings.SECTION_RENDERER,
Settings.LayoutOption_MobileLandscape
),
RENDERER_ASPECT_RATIO(
"aspect_ratio",
Settings.SECTION_RENDERER,
0
),
AUDIO_VOLUME(
"volume",
Settings.SECTION_AUDIO,
100
);
override var int: Int = defaultValue override val int: Int
get() = NativeConfig.getInt(key, false)
override fun setInt(value: Int) = NativeConfig.setInt(key, value)
override val defaultValue: Int by lazy {
androidDefault ?: NativeConfig.getInt(key, true)
}
override val valueAsString: String override val valueAsString: String
get() = int.toString() get() = int.toString()
override val isRuntimeEditable: Boolean override fun reset() = NativeConfig.setInt(key, defaultValue)
get() {
for (setting in NOT_RUNTIME_EDITABLE) {
if (setting == this) {
return false
}
}
return true
}
companion object {
private val NOT_RUNTIME_EDITABLE = listOf(
RENDERER_USE_DISK_SHADER_CACHE,
RENDERER_ASYNCHRONOUS_SHADERS,
RENDERER_DEBUG,
RENDERER_BACKEND,
RENDERER_RESOLUTION,
RENDERER_VSYNC
)
fun from(key: String): IntSetting? = IntSetting.values().firstOrNull { it.key == key }
fun clear() = IntSetting.values().forEach { it.int = it.defaultValue }
}
} }

View File

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class LongSetting(
override val key: String,
override val category: Settings.Category
) : AbstractLongSetting {
CUSTOM_RTC("custom_rtc", Settings.Category.System);
override val long: Long
get() = NativeConfig.getLong(key, false)
override fun setLong(value: Long) = NativeConfig.setLong(key, value)
override val defaultValue: Long by lazy { NativeConfig.getLong(key, true) }
override val valueAsString: String
get() = long.toString()
override fun reset() = NativeConfig.setLong(key, defaultValue)
}

View File

@ -1,37 +0,0 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_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,104 +4,74 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
import android.text.TextUtils import android.text.TextUtils
import java.util.* import android.widget.Toast
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivityView
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
class Settings { object Settings {
private var gameId: String? = null private val context get() = YuzuApplication.appContext
var isLoaded = false fun saveSettings(gameId: String = "") {
/**
* A HashMap<String></String>, SettingSection> that constructs a new SettingSection instead of returning null
* when getting a key not already in the map
*/
class SettingsSectionMap : HashMap<String, SettingSection?>() {
override operator fun get(key: String): SettingSection? {
if (!super.containsKey(key)) {
val section = SettingSection(key)
super.put(key, section)
return section
}
return super.get(key)
}
}
var sections: HashMap<String, SettingSection?> = SettingsSectionMap()
fun getSection(sectionName: String): SettingSection? {
return sections[sectionName]
}
val isEmpty: Boolean
get() = sections.isEmpty()
fun loadSettings(view: SettingsActivityView? = null) {
sections = SettingsSectionMap()
loadYuzuSettings(view)
if (!TextUtils.isEmpty(gameId)) {
loadCustomGameSettings(gameId!!, view)
}
isLoaded = true
}
private fun loadYuzuSettings(view: SettingsActivityView?) {
for ((fileName) in configFileSectionsMap) {
sections.putAll(SettingsFile.readFile(fileName, view))
}
}
private fun loadCustomGameSettings(gameId: String, view: SettingsActivityView?) {
// Custom game settings
mergeSections(SettingsFile.readCustomGameSettings(gameId, view))
}
private fun mergeSections(updatedSections: HashMap<String, SettingSection?>) {
for ((key, updatedSection) in updatedSections) {
if (sections.containsKey(key)) {
val originalSection = sections[key]
originalSection!!.mergeSection(updatedSection!!)
} else {
sections[key] = updatedSection
}
}
}
fun loadSettings(gameId: String, view: SettingsActivityView) {
this.gameId = gameId
loadSettings(view)
}
fun saveSettings(view: SettingsActivityView) {
if (TextUtils.isEmpty(gameId)) { if (TextUtils.isEmpty(gameId)) {
view.showToastMessage( Toast.makeText(
YuzuApplication.appContext.getString(R.string.ini_saved), context,
false context.getString(R.string.ini_saved),
) Toast.LENGTH_SHORT
).show()
for ((fileName, sectionNames) in configFileSectionsMap) { SettingsFile.saveFile(SettingsFile.FILE_NAME_CONFIG)
val iniSections = TreeMap<String, SettingSection>()
for (section in sectionNames) {
iniSections[section] = sections[section]!!
}
SettingsFile.saveFile(fileName, iniSections, view)
}
} else { } else {
// Custom game settings // TODO: Save custom game settings
view.showToastMessage( Toast.makeText(
YuzuApplication.appContext.getString(R.string.gameid_saved, gameId), context,
false context.getString(R.string.gameid_saved, gameId),
Toast.LENGTH_SHORT
).show()
}
}
enum class Category {
Android,
Audio,
Core,
Cpu,
CpuDebug,
CpuUnsafe,
Renderer,
RendererAdvanced,
RendererDebug,
System,
SystemAudio,
DataStorage,
Debugging,
DebuggingGraphics,
Miscellaneous,
Network,
WebService,
AddOns,
Controls,
Ui,
UiGeneral,
UiLayout,
UiGameList,
Screenshots,
Shortcuts,
Multiplayer,
Services,
Paths,
MaxEnum
}
val settingsList = listOf<AbstractSetting>(
*BooleanSetting.values(),
*ByteSetting.values(),
*ShortSetting.values(),
*IntSetting.values(),
*FloatSetting.values(),
*LongSetting.values(),
*StringSetting.values()
) )
SettingsFile.saveCustomGameSettings(gameId, sections)
}
}
companion object {
const val SECTION_GENERAL = "General" const val SECTION_GENERAL = "General"
const val SECTION_SYSTEM = "System" const val SECTION_SYSTEM = "System"
const val SECTION_RENDERER = "Renderer" const val SECTION_RENDERER = "Renderer"
@ -154,8 +124,6 @@ class Settings {
const val PREF_THEME_MODE = "ThemeMode" const val PREF_THEME_MODE = "ThemeMode"
const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds" const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds"
private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap()
val overlayPreferences = listOf( val overlayPreferences = listOf(
PREF_OVERLAY_VERSION, PREF_OVERLAY_VERSION,
PREF_CONTROL_SCALE, PREF_CONTROL_SCALE,
@ -183,16 +151,4 @@ class Settings {
const val LayoutOption_Unspecified = 0 const val LayoutOption_Unspecified = 0
const val LayoutOption_MobilePortrait = 4 const val LayoutOption_MobilePortrait = 4
const val LayoutOption_MobileLandscape = 5 const val LayoutOption_MobileLandscape = 5
init {
configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] =
listOf(
SECTION_GENERAL,
SECTION_SYSTEM,
SECTION_RENDERER,
SECTION_AUDIO,
SECTION_CPU
)
}
}
} }

View File

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class ShortSetting(
override val key: String,
override val category: Settings.Category
) : AbstractShortSetting {
RENDERER_SPEED_LIMIT("speed_limit", Settings.Category.Core);
override val short: Short
get() = NativeConfig.getShort(key, false)
override fun setShort(value: Short) = NativeConfig.setShort(key, value)
override val defaultValue: Short by lazy { NativeConfig.getShort(key, true) }
override val valueAsString: String
get() = short.toString()
override fun reset() = NativeConfig.setShort(key, defaultValue)
}

View File

@ -3,36 +3,24 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class StringSetting( enum class StringSetting(
override val key: String, override val key: String,
override val section: String, override val category: Settings.Category
override val defaultValue: String
) : AbstractStringSetting { ) : AbstractStringSetting {
AUDIO_OUTPUT_ENGINE("output_engine", Settings.SECTION_AUDIO, "auto"), // No string settings currently exist
CUSTOM_RTC("custom_rtc", Settings.SECTION_SYSTEM, "0"); EMPTY_SETTING("", Settings.Category.UiGeneral);
override var string: String = defaultValue override val string: String
get() = NativeConfig.getString(key, false)
override fun setString(value: String) = NativeConfig.setString(key, value)
override val defaultValue: String by lazy { NativeConfig.getString(key, true) }
override val valueAsString: String override val valueAsString: String
get() = string get() = string
override val isRuntimeEditable: Boolean override fun reset() = NativeConfig.setString(key, defaultValue)
get() {
for (setting in NOT_RUNTIME_EDITABLE) {
if (setting == this) {
return false
}
}
return true
}
companion object {
private val NOT_RUNTIME_EDITABLE = listOf(
CUSTOM_RTC
)
fun from(key: String): StringSetting? = StringSetting.values().firstOrNull { it.key == key }
fun clear() = StringSetting.values().forEach { it.string = it.defaultValue }
}
} }

View File

@ -3,29 +3,16 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractLongSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
class DateTimeSetting( class DateTimeSetting(
setting: AbstractSetting?, private val longSetting: AbstractLongSetting,
titleId: Int, titleId: Int,
descriptionId: Int, descriptionId: Int
val key: String? = null, ) : SettingsItem(longSetting, titleId, descriptionId) {
private val defaultValue: String? = null
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_DATETIME_SETTING override val type = TYPE_DATETIME_SETTING
val value: String var value: Long
get() = if (setting != null) { get() = longSetting.long
val setting = setting as AbstractStringSetting set(value) = (setting as AbstractLongSetting).setLong(value)
setting.string
} else {
defaultValue!!
}
fun setSelectedValue(datetime: String): AbstractStringSetting {
val stringSetting = setting as AbstractStringSetting
stringSetting.string = datetime
return stringSetting
}
} }

View File

@ -5,6 +5,6 @@ package org.yuzu.yuzu_emu.features.settings.model.view
class HeaderSetting( class HeaderSetting(
titleId: Int titleId: Int
) : SettingsItem(null, titleId, 0) { ) : SettingsItem(emptySetting, titleId, 0) {
override val type = TYPE_HEADER override val type = TYPE_HEADER
} }

View File

@ -8,6 +8,6 @@ class RunnableSetting(
descriptionId: Int, descriptionId: Int,
val isRuntimeRunnable: Boolean, val isRuntimeRunnable: Boolean,
val runnable: () -> Unit val runnable: () -> Unit
) : SettingsItem(null, titleId, descriptionId) { ) : SettingsItem(emptySetting, titleId, descriptionId) {
override val type = TYPE_RUNNABLE override val type = TYPE_RUNNABLE
} }

View File

@ -4,7 +4,15 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.ByteSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.LongSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
/** /**
* ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments. * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
@ -14,7 +22,7 @@ import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
* file.) * file.)
*/ */
abstract class SettingsItem( abstract class SettingsItem(
var setting: AbstractSetting?, val setting: AbstractSetting,
val nameId: Int, val nameId: Int,
val descriptionId: Int val descriptionId: Int
) { ) {
@ -23,7 +31,7 @@ abstract class SettingsItem(
val isEditable: Boolean val isEditable: Boolean
get() { get() {
if (!NativeLibrary.isRunning()) return true if (!NativeLibrary.isRunning()) return true
return setting?.isRuntimeEditable ?: false return setting.isRuntimeModifiable
} }
companion object { companion object {
@ -35,5 +43,240 @@ abstract class SettingsItem(
const val TYPE_STRING_SINGLE_CHOICE = 5 const val TYPE_STRING_SINGLE_CHOICE = 5
const val TYPE_DATETIME_SETTING = 6 const val TYPE_DATETIME_SETTING = 6
const val TYPE_RUNNABLE = 7 const val TYPE_RUNNABLE = 7
const val FASTMEM_COMBINED = "fastmem_combined"
val emptySetting = object : AbstractSetting {
override val key: String = ""
override val category: Settings.Category = Settings.Category.Ui
override val defaultValue: Any = false
override fun reset() {}
}
// Extension for putting SettingsItems into a hashmap without repeating yourself
fun HashMap<String, SettingsItem>.put(item: SettingsItem) {
put(item.setting.key, item)
}
// List of all general
val settingsItems = HashMap<String, SettingsItem>().apply {
put(
SwitchSetting(
BooleanSetting.RENDERER_USE_SPEED_LIMIT,
R.string.frame_limit_enable,
R.string.frame_limit_enable_description
)
)
put(
SliderSetting(
ShortSetting.RENDERER_SPEED_LIMIT,
R.string.frame_limit_slider,
R.string.frame_limit_slider_description,
1,
200,
"%"
)
)
put(
SingleChoiceSetting(
IntSetting.CPU_ACCURACY,
R.string.cpu_accuracy,
0,
R.array.cpuAccuracyNames,
R.array.cpuAccuracyValues
)
)
put(
SwitchSetting(
BooleanSetting.PICTURE_IN_PICTURE,
R.string.picture_in_picture,
R.string.picture_in_picture_description
)
)
put(
SwitchSetting(
BooleanSetting.USE_DOCKED_MODE,
R.string.use_docked_mode,
R.string.use_docked_mode_description
)
)
put(
SingleChoiceSetting(
IntSetting.REGION_INDEX,
R.string.emulated_region,
0,
R.array.regionNames,
R.array.regionValues
)
)
put(
SingleChoiceSetting(
IntSetting.LANGUAGE_INDEX,
R.string.emulated_language,
0,
R.array.languageNames,
R.array.languageValues
)
)
put(
SwitchSetting(
BooleanSetting.USE_CUSTOM_RTC,
R.string.use_custom_rtc,
R.string.use_custom_rtc_description
)
)
put(DateTimeSetting(LongSetting.CUSTOM_RTC, R.string.set_custom_rtc, 0))
put(
SingleChoiceSetting(
IntSetting.RENDERER_ACCURACY,
R.string.renderer_accuracy,
0,
R.array.rendererAccuracyNames,
R.array.rendererAccuracyValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_RESOLUTION,
R.string.renderer_resolution,
0,
R.array.rendererResolutionNames,
R.array.rendererResolutionValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_VSYNC,
R.string.renderer_vsync,
0,
R.array.rendererVSyncNames,
R.array.rendererVSyncValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_SCALING_FILTER,
R.string.renderer_scaling_filter,
0,
R.array.rendererScalingFilterNames,
R.array.rendererScalingFilterValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_ANTI_ALIASING,
R.string.renderer_anti_aliasing,
0,
R.array.rendererAntiAliasingNames,
R.array.rendererAntiAliasingValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_SCREEN_LAYOUT,
R.string.renderer_screen_layout,
0,
R.array.rendererScreenLayoutNames,
R.array.rendererScreenLayoutValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_ASPECT_RATIO,
R.string.renderer_aspect_ratio,
0,
R.array.rendererAspectRatioNames,
R.array.rendererAspectRatioValues
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE,
R.string.use_disk_shader_cache,
R.string.use_disk_shader_cache_description
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_FORCE_MAX_CLOCK,
R.string.renderer_force_max_clock,
R.string.renderer_force_max_clock_description
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS,
R.string.renderer_asynchronous_shaders,
R.string.renderer_asynchronous_shaders_description
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_REACTIVE_FLUSHING,
R.string.renderer_reactive_flushing,
R.string.renderer_reactive_flushing_description
)
)
put(
SingleChoiceSetting(
IntSetting.AUDIO_OUTPUT_ENGINE,
R.string.audio_output_engine,
0,
R.array.outputEngineEntries,
R.array.outputEngineValues
)
)
put(
SliderSetting(
ByteSetting.AUDIO_VOLUME,
R.string.audio_volume,
R.string.audio_volume_description,
0,
100,
"%"
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_BACKEND,
R.string.renderer_api,
0,
R.array.rendererApiNames,
R.array.rendererApiValues
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_DEBUG,
R.string.renderer_debug,
R.string.renderer_debug_description
)
)
put(
SwitchSetting(
BooleanSetting.CPU_DEBUG_MODE,
R.string.cpu_debug_mode,
R.string.cpu_debug_mode_description
)
)
val fastmem = object : AbstractBooleanSetting {
override val boolean: Boolean
get() =
BooleanSetting.FASTMEM.boolean && BooleanSetting.FASTMEM_EXCLUSIVES.boolean
override fun setBoolean(value: Boolean) {
BooleanSetting.FASTMEM.setBoolean(value)
BooleanSetting.FASTMEM_EXCLUSIVES.setBoolean(value)
}
override val key: String = FASTMEM_COMBINED
override val category = Settings.Category.Cpu
override val isRuntimeModifiable: Boolean = false
override val defaultValue: Boolean = true
override fun reset() = setBoolean(defaultValue)
}
put(SwitchSetting(fastmem, R.string.fastmem, 0))
}
} }
} }

View File

@ -4,36 +4,27 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
class SingleChoiceSetting( class SingleChoiceSetting(
setting: AbstractIntSetting?, setting: AbstractSetting,
titleId: Int, titleId: Int,
descriptionId: Int, descriptionId: Int,
val choicesId: Int, val choicesId: Int,
val valuesId: Int, val valuesId: Int
val key: String? = null,
val defaultValue: Int? = null
) : SettingsItem(setting, titleId, descriptionId) { ) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_SINGLE_CHOICE override val type = TYPE_SINGLE_CHOICE
val selectedValue: Int var selectedValue: Int
get() = if (setting != null) { get() {
val setting = setting as AbstractIntSetting return when (setting) {
setting.int is AbstractIntSetting -> setting.int
} else { else -> -1
defaultValue!! }
} }
set(value) {
/** when (setting) {
* Write a value to the backing int. If that int was previously null, is AbstractIntSetting -> setting.setInt(value)
* initializes a new one and returns it, so it can be added to the Hashmap. }
*
* @param selection New value of the int.
* @return the existing setting with the new value applied.
*/
fun setSelectedValue(selection: Int): AbstractIntSetting {
val intSetting = setting as AbstractIntSetting
intSetting.int = selection
return intSetting
} }
} }

View File

@ -3,60 +3,39 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import kotlin.math.roundToInt import org.yuzu.yuzu_emu.features.settings.model.AbstractByteSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
import org.yuzu.yuzu_emu.utils.Log import org.yuzu.yuzu_emu.features.settings.model.AbstractShortSetting
import kotlin.math.roundToInt
class SliderSetting( class SliderSetting(
setting: AbstractSetting?, setting: AbstractSetting,
titleId: Int, titleId: Int,
descriptionId: Int, descriptionId: Int,
val min: Int, val min: Int,
val max: Int, val max: Int,
val units: String, val units: String
val key: String? = null,
val defaultValue: Int? = null
) : SettingsItem(setting, titleId, descriptionId) { ) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_SLIDER override val type = TYPE_SLIDER
val selectedValue: Int var selectedValue: Int
get() { get() {
val setting = setting ?: return defaultValue!!
return when (setting) { return when (setting) {
is AbstractByteSetting -> setting.byte.toInt()
is AbstractShortSetting -> setting.short.toInt()
is AbstractIntSetting -> setting.int is AbstractIntSetting -> setting.int
is AbstractFloatSetting -> setting.float.roundToInt() is AbstractFloatSetting -> setting.float.roundToInt()
else -> { else -> -1
Log.error("[SliderSetting] Error casting setting type.") }
-1 }
set(value) {
when (setting) {
is AbstractByteSetting -> setting.setByte(value.toByte())
is AbstractShortSetting -> setting.setShort(value.toShort())
is AbstractIntSetting -> setting.setInt(value)
is AbstractFloatSetting -> setting.setFloat(value.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
return intSetting
}
/**
* Write a value to the backing float. If that float was previously null,
* initializes a new one and returns it, so it can be added to the Hashmap.
*
* @param selection New value of the float.
* @return the existing setting with the new value applied.
*/
fun setSelectedValue(selection: Float): AbstractFloatSetting {
val floatSetting = setting as AbstractFloatSetting
floatSetting.float = selection
return floatSetting
}
}

View File

@ -3,57 +3,31 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
class StringSingleChoiceSetting( class StringSingleChoiceSetting(
setting: AbstractSetting?, private val stringSetting: AbstractStringSetting,
titleId: Int, titleId: Int,
descriptionId: Int, descriptionId: Int,
val choices: Array<String>, val choices: Array<String>,
val values: Array<String>?, val values: Array<String>
val key: String? = null, ) : SettingsItem(stringSetting, titleId, descriptionId) {
private val defaultValue: String? = null
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_STRING_SINGLE_CHOICE override val type = TYPE_STRING_SINGLE_CHOICE
fun getValueAt(index: Int): String? { fun getValueAt(index: Int): String =
if (values == null) return null if (index >= 0 && index < values.size) values[index] else ""
return if (index >= 0 && index < values.size) {
values[index] var selectedValue: String
} else { get() = stringSetting.string
"" set(value) = stringSetting.setString(value)
}
}
val selectedValue: String
get() = if (setting != null) {
val setting = setting as AbstractStringSetting
setting.string
} else {
defaultValue!!
}
val selectValueIndex: Int val selectValueIndex: Int
get() { get() {
val selectedValue = selectedValue for (i in values.indices) {
for (i in values!!.indices) {
if (values[i] == selectedValue) { if (values[i] == selectedValue) {
return i return i
} }
} }
return -1 return -1
} }
/**
* Write a value to the backing int. If that int was previously null,
* initializes a new one and returns it, so it can be added to the Hashmap.
*
* @param selection New value of the int.
* @return the existing setting with the new value applied.
*/
fun setSelectedValue(selection: String): AbstractStringSetting {
val stringSetting = setting as AbstractStringSetting
stringSetting.string = selection
return stringSetting
}
} }

View File

@ -7,6 +7,6 @@ class SubmenuSetting(
titleId: Int, titleId: Int,
descriptionId: Int, descriptionId: Int,
val menuKey: String val menuKey: String
) : SettingsItem(null, titleId, descriptionId) { ) : SettingsItem(emptySetting, titleId, descriptionId) {
override val type = TYPE_SUBMENU override val type = TYPE_SUBMENU
} }

View File

@ -10,53 +10,22 @@ import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
class SwitchSetting( class SwitchSetting(
setting: AbstractSetting, setting: AbstractSetting,
titleId: Int, titleId: Int,
descriptionId: Int, descriptionId: Int
val key: String? = null,
val defaultValue: Any? = null
) : SettingsItem(setting, titleId, descriptionId) { ) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_SWITCH override val type = TYPE_SWITCH
val isChecked: Boolean var checked: Boolean
get() { get() {
if (setting == null) { return when (setting) {
return defaultValue as Boolean is AbstractIntSetting -> setting.int == 1
} is AbstractBooleanSetting -> setting.boolean
else -> false
// Try integer setting }
try { }
val setting = setting as AbstractIntSetting set(value) {
return setting.int == 1 when (setting) {
} catch (_: ClassCastException) { is AbstractIntSetting -> setting.setInt(if (value) 1 else 0)
} is AbstractBooleanSetting -> setting.setBoolean(value)
}
// Try boolean setting
try {
val setting = setting as AbstractBooleanSetting
return setting.boolean
} catch (_: ClassCastException) {
}
return defaultValue as Boolean
}
/**
* Write a value to the backing boolean. If that boolean was previously null,
* initializes a new one and returns it, so it can be added to the Hashmap.
*
* @param checked Pretty self explanatory.
* @return the existing setting with the new value applied.
*/
fun setChecked(checked: Boolean): AbstractSetting {
// Try integer setting
try {
val setting = setting as AbstractIntSetting
setting.int = if (checked) 1 else 0
return setting
} catch (_: ClassCastException) {
}
// Try boolean setting
val setting = setting as AbstractBooleanSetting
setting.boolean = checked
return setting
} }
} }

View File

@ -3,10 +3,7 @@
package org.yuzu.yuzu_emu.features.settings.ui package org.yuzu.yuzu_emu.features.settings.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import android.view.View import android.view.View
import android.view.ViewGroup.MarginLayoutParams import android.view.ViewGroup.MarginLayoutParams
import android.widget.Toast import android.widget.Toast
@ -16,28 +13,24 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.navArgs
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import java.io.IOException import java.io.IOException
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.FloatSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment
import org.yuzu.yuzu_emu.model.SettingsViewModel
import org.yuzu.yuzu_emu.utils.* import org.yuzu.yuzu_emu.utils.*
class SettingsActivity : AppCompatActivity(), SettingsActivityView { class SettingsActivity : AppCompatActivity() {
private val presenter = SettingsActivityPresenter(this)
private lateinit var binding: ActivitySettingsBinding private lateinit var binding: ActivitySettingsBinding
private val settingsViewModel: SettingsViewModel by viewModels() private val args by navArgs<SettingsActivityArgs>()
override val settings: Settings get() = settingsViewModel.settings private val settingsViewModel: SettingsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
ThemeHelper.setTheme(this) ThemeHelper.setTheme(this)
@ -47,16 +40,17 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
binding = ActivitySettingsBinding.inflate(layoutInflater) binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
settingsViewModel.game = args.game
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
navHostFragment.navController.setGraph(R.navigation.settings_navigation, intent.extras)
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
val launcher = intent if (savedInstanceState != null) {
val gameID = launcher.getStringExtra(ARG_GAME_ID) settingsViewModel.shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE)
val menuTag = launcher.getStringExtra(ARG_MENU_TAG) }
presenter.onCreate(savedInstanceState, menuTag!!, gameID!!)
// Show "Back" button in the action bar for navigation
setSupportActionBar(binding.toolbarSettings)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
if (InsetsHelper.getSystemGestureType(applicationContext) != if (InsetsHelper.getSystemGestureType(applicationContext) !=
InsetsHelper.GESTURE_NAVIGATION InsetsHelper.GESTURE_NAVIGATION
@ -72,6 +66,28 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
) )
} }
settingsViewModel.shouldRecreate.observe(this) {
if (it) {
settingsViewModel.setShouldRecreate(false)
recreate()
}
}
settingsViewModel.shouldNavigateBack.observe(this) {
if (it) {
settingsViewModel.setShouldNavigateBack(false)
navigateBack()
}
}
settingsViewModel.shouldShowResetSettingsDialog.observe(this) {
if (it) {
settingsViewModel.setShouldShowResetSettingsDialog(false)
ResetSettingsDialogFragment().show(
supportFragmentManager,
ResetSettingsDialogFragment.TAG
)
}
}
onBackPressedDispatcher.addCallback( onBackPressedDispatcher.addCallback(
this, this,
object : OnBackPressedCallback(true) { object : OnBackPressedCallback(true) {
@ -82,34 +98,28 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
setInsets() setInsets()
} }
override fun onSupportNavigateUp(): Boolean { fun navigateBack() {
navigateBack() val navHostFragment =
return true supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
} if (navHostFragment.childFragmentManager.backStackEntryCount > 0) {
navHostFragment.navController.popBackStack()
private fun navigateBack() {
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStack()
} else { } else {
finish() finish()
} }
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.menu_settings, menu)
return true
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
// Critical: If super method is not called, rotations will be busted. // Critical: If super method is not called, rotations will be busted.
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
presenter.saveState(outState) outState.putBoolean(KEY_SHOULD_SAVE, settingsViewModel.shouldSave)
} }
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
presenter.onStart() // TODO: Load custom settings contextually
if (!DirectoryInitialization.areDirectoriesReady) {
DirectoryInitialization.start()
}
} }
/** /**
@ -119,131 +129,51 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
*/ */
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
presenter.onStop(isFinishing) if (isFinishing && settingsViewModel.shouldSave) {
Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
Settings.saveSettings()
}
} }
override fun showSettingsFragment(menuTag: String, addToStack: Boolean, gameId: String) { override fun onDestroy() {
if (!addToStack && settingsFragment != null) { settingsViewModel.clear()
return super.onDestroy()
}
val transaction = supportFragmentManager.beginTransaction()
if (addToStack) {
if (areSystemAnimationsEnabled()) {
transaction.setCustomAnimations(
R.anim.anim_settings_fragment_in,
R.anim.anim_settings_fragment_out,
0,
R.anim.anim_pop_settings_fragment_out
)
}
transaction.addToBackStack(null)
}
transaction.replace(
R.id.frame_content,
SettingsFragment.newInstance(menuTag, gameId),
FRAGMENT_TAG
)
transaction.commit()
}
private fun areSystemAnimationsEnabled(): Boolean {
val duration = android.provider.Settings.Global.getFloat(
contentResolver,
android.provider.Settings.Global.ANIMATOR_DURATION_SCALE,
1f
)
val transition = android.provider.Settings.Global.getFloat(
contentResolver,
android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE,
1f
)
return duration != 0f && transition != 0f
}
override fun onSettingsFileLoaded() {
val fragment: SettingsFragmentView? = settingsFragment
fragment?.loadSettingsList()
}
override fun onSettingsFileNotFound() {
val fragment: SettingsFragmentView? = settingsFragment
fragment?.loadSettingsList()
}
override fun showToastMessage(message: String, is_long: Boolean) {
Toast.makeText(
this,
message,
if (is_long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
).show()
}
override fun onSettingChanged() {
presenter.onSettingChanged()
} }
fun onSettingsReset() { fun onSettingsReset() {
// Prevents saving to a non-existent settings file // Prevents saving to a non-existent settings file
presenter.onSettingsReset() settingsViewModel.shouldSave = false
// Reset the static memory representation of each setting
BooleanSetting.clear()
FloatSetting.clear()
IntSetting.clear()
StringSetting.clear()
// Delete settings file because the user may have changed values that do not exist in the UI // 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) val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
if (!settingsFile.delete()) { if (!settingsFile.delete()) {
throw IOException("Failed to delete $settingsFile") throw IOException("Failed to delete $settingsFile")
} }
Settings.settingsList.forEach { it.reset() }
showToastMessage(getString(R.string.settings_reset), true) Toast.makeText(
applicationContext,
getString(R.string.settings_reset),
Toast.LENGTH_LONG
).show()
finish() finish()
} }
fun setToolbarTitle(title: String) {
binding.toolbarSettingsLayout.title = title
}
private val settingsFragment: SettingsFragment?
get() = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as SettingsFragment?
private fun setInsets() { private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener( ViewCompat.setOnApplyWindowInsetsListener(
binding.frameContent binding.navigationBarShade
) { view: View, windowInsets: WindowInsetsCompat -> ) { view: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
view.updatePadding(
left = barInsets.left + cutoutInsets.left,
right = barInsets.right + cutoutInsets.right
)
val mlpAppBar = binding.appbarSettings.layoutParams as MarginLayoutParams val mlpShade = view.layoutParams as MarginLayoutParams
mlpAppBar.leftMargin = barInsets.left + cutoutInsets.left
mlpAppBar.rightMargin = barInsets.right + cutoutInsets.right
binding.appbarSettings.layoutParams = mlpAppBar
val mlpShade = binding.navigationBarShade.layoutParams as MarginLayoutParams
mlpShade.height = barInsets.bottom mlpShade.height = barInsets.bottom
binding.navigationBarShade.layoutParams = mlpShade view.layoutParams = mlpShade
windowInsets windowInsets
} }
} }
companion object { companion object {
private const val ARG_MENU_TAG = "menu_tag" private const val KEY_SHOULD_SAVE = "should_save"
private const val ARG_GAME_ID = "game_id"
private const val FRAGMENT_TAG = "settings"
fun launch(context: Context, menuTag: String?, gameId: String?) {
val settings = Intent(context, SettingsActivity::class.java)
settings.putExtra(ARG_MENU_TAG, menuTag)
settings.putExtra(ARG_GAME_ID, gameId)
context.startActivity(settings)
}
} }
} }

View File

@ -1,90 +0,0 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
import android.content.Context
import android.os.Bundle
import android.text.TextUtils
import java.io.File
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.Log
class SettingsActivityPresenter(private val activityView: SettingsActivityView) {
val settings: Settings get() = activityView.settings
private var shouldSave = false
private lateinit var menuTag: String
private lateinit var gameId: String
fun onCreate(savedInstanceState: Bundle?, menuTag: String, gameId: String) {
this.menuTag = menuTag
this.gameId = gameId
if (savedInstanceState != null) {
shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE)
}
}
fun onStart() {
prepareDirectoriesIfNeeded()
}
private fun loadSettingsUI() {
if (!settings.isLoaded) {
if (!TextUtils.isEmpty(gameId)) {
settings.loadSettings(gameId, activityView)
} else {
settings.loadSettings(activityView)
}
}
activityView.showSettingsFragment(menuTag, false, gameId)
activityView.onSettingsFileLoaded()
}
private fun prepareDirectoriesIfNeeded() {
val configFile =
File(
"${DirectoryInitialization.userDirectory}/config/" +
"${SettingsFile.FILE_NAME_CONFIG}.ini"
)
if (!configFile.exists()) {
Log.error(
"${DirectoryInitialization.userDirectory}/config/" +
"${SettingsFile.FILE_NAME_CONFIG}.ini"
)
Log.error("yuzu config file could not be found!")
}
if (!DirectoryInitialization.areDirectoriesReady) {
DirectoryInitialization.start(activityView as Context)
}
loadSettingsUI()
}
fun onStop(finishing: Boolean) {
if (finishing && shouldSave) {
Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
settings.saveSettings(activityView)
}
NativeLibrary.reloadSettings()
}
fun onSettingChanged() {
shouldSave = true
}
fun onSettingsReset() {
shouldSave = false
}
fun saveState(outState: Bundle) {
outState.putBoolean(KEY_SHOULD_SAVE, shouldSave)
}
companion object {
private const val KEY_SHOULD_SAVE = "should_save"
}
}

View File

@ -1,57 +0,0 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
import org.yuzu.yuzu_emu.features.settings.model.Settings
/**
* Abstraction for the Activity that manages SettingsFragments.
*/
interface SettingsActivityView {
/**
* Show a new SettingsFragment.
*
* @param menuTag Identifier for the settings group that should be displayed.
* @param addToStack Whether or not this fragment should replace a previous one.
*/
fun showSettingsFragment(menuTag: String, addToStack: Boolean, gameId: String)
/**
* Called by a contained Fragment to get access to the Setting HashMap
* loaded from disk, so that each Fragment doesn't need to perform its own
* read operation.
*
* @return A HashMap of Settings.
*/
val settings: Settings
/**
* Called when a load operation completes.
*/
fun onSettingsFileLoaded()
/**
* Called when a load operation fails.
*/
fun onSettingsFileNotFound()
/**
* Display a popup text message on screen.
*
* @param message The contents of the onscreen message.
* @param is_long Whether this should be a long Toast or short one.
*/
fun showToastMessage(message: String, is_long: Boolean)
/**
* End the activity.
*/
fun finish()
/**
* Called by a containing Fragment to tell the Activity that a setting was changed;
* unless this has been called, the Activity will not save to disk.
*/
fun onSettingChanged()
}

View File

@ -4,51 +4,54 @@
package org.yuzu.yuzu_emu.features.settings.ui package org.yuzu.yuzu_emu.features.settings.ui
import android.content.Context import android.content.Context
import android.content.DialogInterface
import android.icu.util.Calendar import android.icu.util.Calendar
import android.icu.util.TimeZone import android.icu.util.TimeZone
import android.text.format.DateFormat import android.text.format.DateFormat
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import androidx.fragment.app.Fragment
import androidx.appcompat.app.AlertDialog import androidx.lifecycle.Lifecycle
import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.RecyclerView import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.slider.Slider
import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat import com.google.android.material.timepicker.TimeFormat
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogSliderBinding import org.yuzu.yuzu_emu.SettingsNavigationDirections
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
import org.yuzu.yuzu_emu.features.settings.model.FloatSetting
import org.yuzu.yuzu_emu.features.settings.model.view.* import org.yuzu.yuzu_emu.features.settings.model.view.*
import org.yuzu.yuzu_emu.features.settings.ui.viewholder.* import org.yuzu.yuzu_emu.features.settings.ui.viewholder.*
import org.yuzu.yuzu_emu.fragments.SettingsDialogFragment
import org.yuzu.yuzu_emu.model.SettingsViewModel
class SettingsAdapter( class SettingsAdapter(
private val fragmentView: SettingsFragmentView, private val fragment: Fragment,
private val context: Context private val context: Context
) : RecyclerView.Adapter<SettingViewHolder?>(), DialogInterface.OnClickListener { ) : ListAdapter<SettingsItem, SettingViewHolder>(
private var settings: ArrayList<SettingsItem>? = null AsyncDifferConfig.Builder(DiffCallback()).build()
private var clickedItem: SettingsItem? = null ) {
private var clickedPosition: Int private val settingsViewModel: SettingsViewModel
private var dialog: AlertDialog? = null get() = ViewModelProvider(fragment.requireActivity())[SettingsViewModel::class.java]
private var sliderProgress = 0
private var textSliderValue: TextView? = null
private var defaultCancelListener =
DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> closeDialog() }
init { init {
clickedPosition = -1 fragment.viewLifecycleOwner.lifecycleScope.launch {
fragment.repeatOnLifecycle(Lifecycle.State.STARTED) {
settingsViewModel.adapterItemChanged.collect {
if (it != -1) {
notifyItemChanged(it)
settingsViewModel.setAdapterItemChanged(-1)
}
}
}
}
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder {
@ -90,67 +93,41 @@ class SettingsAdapter(
} }
override fun onBindViewHolder(holder: SettingViewHolder, position: Int) { override fun onBindViewHolder(holder: SettingViewHolder, position: Int) {
holder.bind(getItem(position)) holder.bind(currentList[position])
} }
private fun getItem(position: Int): SettingsItem { override fun getItemCount(): Int = currentList.size
return settings!![position]
}
override fun getItemCount(): Int {
return if (settings != null) {
settings!!.size
} else {
0
}
}
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
return getItem(position).type return currentList[position].type
} }
fun setSettingsList(settings: ArrayList<SettingsItem>?) { fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
this.settings = settings item.checked = checked
notifyDataSetChanged() settingsViewModel.setShouldReloadSettingsList(true)
} settingsViewModel.shouldSave = true
fun onBooleanClick(item: SwitchSetting, position: Int, checked: Boolean) {
val setting = item.setChecked(checked)
fragmentView.putSetting(setting)
fragmentView.onSettingChanged()
}
private fun onSingleChoiceClick(item: SingleChoiceSetting) {
clickedItem = item
val value = getSelectionForSingleChoiceValue(item)
dialog = MaterialAlertDialogBuilder(context)
.setTitle(item.nameId)
.setSingleChoiceItems(item.choicesId, value, this)
.show()
} }
fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) { fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
clickedPosition = position SettingsDialogFragment.newInstance(
onSingleChoiceClick(item) settingsViewModel,
} item,
SettingsItem.TYPE_SINGLE_CHOICE,
private fun onStringSingleChoiceClick(item: StringSingleChoiceSetting) { position
clickedItem = item ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
dialog = MaterialAlertDialogBuilder(context)
.setTitle(item.nameId)
.setSingleChoiceItems(item.choices, item.selectValueIndex, this)
.show()
} }
fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) { fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) {
clickedPosition = position SettingsDialogFragment.newInstance(
onStringSingleChoiceClick(item) settingsViewModel,
item,
SettingsItem.TYPE_STRING_SINGLE_CHOICE,
position
).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
} }
fun onDateTimeClick(item: DateTimeSetting, position: Int) { fun onDateTimeClick(item: DateTimeSetting, position: Int) {
clickedItem = item val storedTime = item.value * 1000
clickedPosition = position
val storedTime = java.lang.Long.decode(item.value) * 1000
// Helper to extract hour and minute from epoch time // Helper to extract hour and minute from epoch time
val calendar: Calendar = Calendar.getInstance() val calendar: Calendar = Calendar.getInstance()
@ -158,7 +135,7 @@ class SettingsAdapter(
calendar.timeZone = TimeZone.getTimeZone("UTC") calendar.timeZone = TimeZone.getTimeZone("UTC")
var timeFormat: Int = TimeFormat.CLOCK_12H var timeFormat: Int = TimeFormat.CLOCK_12H
if (DateFormat.is24HourFormat(fragmentView.activityView as AppCompatActivity)) { if (DateFormat.is24HourFormat(context)) {
timeFormat = TimeFormat.CLOCK_24H timeFormat = TimeFormat.CLOCK_24H
} }
@ -175,7 +152,7 @@ class SettingsAdapter(
datePicker.addOnPositiveButtonClickListener { datePicker.addOnPositiveButtonClickListener {
timePicker.show( timePicker.show(
(fragmentView.activityView as AppCompatActivity).supportFragmentManager, fragment.childFragmentManager,
"TimePicker" "TimePicker"
) )
} }
@ -183,160 +160,50 @@ class SettingsAdapter(
var epochTime: Long = datePicker.selection!! / 1000 var epochTime: Long = datePicker.selection!! / 1000
epochTime += timePicker.hour.toLong() * 60 * 60 epochTime += timePicker.hour.toLong() * 60 * 60
epochTime += timePicker.minute.toLong() * 60 epochTime += timePicker.minute.toLong() * 60
val rtcString = epochTime.toString() if (item.value != epochTime) {
if (item.value != rtcString) { settingsViewModel.shouldSave = true
fragmentView.onSettingChanged() notifyItemChanged(position)
item.value = epochTime
} }
notifyItemChanged(clickedPosition)
val setting = item.setSelectedValue(rtcString)
fragmentView.putSetting(setting)
clickedItem = null
} }
datePicker.show( datePicker.show(
(fragmentView.activityView as AppCompatActivity).supportFragmentManager, fragment.childFragmentManager,
"DatePicker" "DatePicker"
) )
} }
fun onSliderClick(item: SliderSetting, position: Int) { fun onSliderClick(item: SliderSetting, position: Int) {
clickedItem = item SettingsDialogFragment.newInstance(
clickedPosition = position settingsViewModel,
sliderProgress = item.selectedValue item,
SettingsItem.TYPE_SLIDER,
val inflater = LayoutInflater.from(context) position
val sliderBinding = DialogSliderBinding.inflate(inflater) ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
textSliderValue = sliderBinding.textValue
textSliderValue!!.text = String.format(
context.getString(R.string.value_with_units),
sliderProgress.toString(),
item.units
)
sliderBinding.slider.apply {
valueFrom = item.min.toFloat()
valueTo = item.max.toFloat()
value = sliderProgress.toFloat()
addOnChangeListener { _: Slider, value: Float, _: Boolean ->
sliderProgress = value.toInt()
textSliderValue!!.text = String.format(
context.getString(R.string.value_with_units),
sliderProgress.toString(),
item.units
)
}
}
dialog = MaterialAlertDialogBuilder(context)
.setTitle(item.nameId)
.setView(sliderBinding.root)
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, defaultCancelListener)
.show()
} }
fun onSubmenuClick(item: SubmenuSetting) { fun onSubmenuClick(item: SubmenuSetting) {
fragmentView.loadSubMenu(item.menuKey) val action = SettingsNavigationDirections.actionGlobalSettingsFragment(item.menuKey, null)
fragment.view?.findNavController()?.navigate(action)
} }
override fun onClick(dialog: DialogInterface, which: Int) { fun onLongClick(item: SettingsItem, position: Int): Boolean {
when (clickedItem) { SettingsDialogFragment.newInstance(
is SingleChoiceSetting -> { settingsViewModel,
val scSetting = clickedItem as SingleChoiceSetting item,
val value = getValueForSingleChoiceSelection(scSetting, which) SettingsDialogFragment.TYPE_RESET_SETTING,
if (scSetting.selectedValue != value) { position
fragmentView.onSettingChanged() ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
}
// Get the backing Setting, which may be null (if for example it was missing from the file)
val setting = scSetting.setSelectedValue(value)
fragmentView.putSetting(setting)
closeDialog()
}
is StringSingleChoiceSetting -> {
val scSetting = clickedItem as StringSingleChoiceSetting
val value = scSetting.getValueAt(which)
if (scSetting.selectedValue != value) fragmentView.onSettingChanged()
val setting = scSetting.setSelectedValue(value!!)
fragmentView.putSetting(setting)
closeDialog()
}
is SliderSetting -> {
val sliderSetting = clickedItem as SliderSetting
if (sliderSetting.selectedValue != sliderProgress) {
fragmentView.onSettingChanged()
}
if (sliderSetting.setting is FloatSetting) {
val value = sliderProgress.toFloat()
val setting = sliderSetting.setSelectedValue(value)
fragmentView.putSetting(setting)
} else {
val setting = sliderSetting.setSelectedValue(sliderProgress)
fragmentView.putSetting(setting)
}
closeDialog()
}
}
clickedItem = null
sliderProgress = -1
}
fun onLongClick(setting: AbstractSetting, position: Int): Boolean {
MaterialAlertDialogBuilder(context)
.setMessage(R.string.reset_setting_confirmation)
.setPositiveButton(android.R.string.ok) { dialog: DialogInterface, which: Int ->
when (setting) {
is AbstractBooleanSetting -> setting.boolean = setting.defaultValue as Boolean
is AbstractFloatSetting -> setting.float = setting.defaultValue as Float
is AbstractIntSetting -> setting.int = setting.defaultValue as Int
is AbstractStringSetting -> setting.string = setting.defaultValue as String
}
notifyItemChanged(position)
fragmentView.onSettingChanged()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
return true return true
} }
fun closeDialog() { private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() {
if (dialog != null) { override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
if (clickedPosition != -1) { return oldItem.setting.key == newItem.setting.key
notifyItemChanged(clickedPosition)
clickedPosition = -1
}
dialog!!.dismiss()
dialog = null
}
} }
private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int { override fun areContentsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
val valuesId = item.valuesId return oldItem.setting.key == newItem.setting.key
return if (valuesId > 0) {
val valuesArray = context.resources.getIntArray(valuesId)
valuesArray[which]
} else {
which
} }
} }
private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
val value = item.selectedValue
val valuesId = item.valuesId
if (valuesId > 0) {
val valuesArray = context.resources.getIntArray(valuesId)
for (index in valuesArray.indices) {
val current = valuesArray[index]
if (current == value) {
return index
}
}
} else {
return value
}
return -1
}
} }

View File

@ -3,40 +3,43 @@
package org.yuzu.yuzu_emu.features.settings.ui package org.yuzu.yuzu_emu.features.settings.ui
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.divider.MaterialDividerItemDecoration import com.google.android.material.divider.MaterialDividerItemDecoration
import com.google.android.material.transition.MaterialSharedAxis
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.model.SettingsViewModel
class SettingsFragment : Fragment(), SettingsFragmentView { class SettingsFragment : Fragment() {
override var activityView: SettingsActivityView? = null private lateinit var presenter: SettingsFragmentPresenter
private val fragmentPresenter = SettingsFragmentPresenter(this)
private var settingsAdapter: SettingsAdapter? = null private var settingsAdapter: SettingsAdapter? = null
private var _binding: FragmentSettingsBinding? = null private var _binding: FragmentSettingsBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun onAttach(context: Context) { private val args by navArgs<SettingsFragmentArgs>()
super.onAttach(context)
activityView = requireActivity() as SettingsActivityView private val settingsViewModel: SettingsViewModel by activityViewModels()
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val menuTag = requireArguments().getString(ARGUMENT_MENU_TAG) enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
val gameId = requireArguments().getString(ARGUMENT_GAME_ID) returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
fragmentPresenter.onCreate(menuTag!!, gameId!!) reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
} }
override fun onCreateView( override fun onCreateView(
@ -49,7 +52,14 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
settingsAdapter = SettingsAdapter(this, requireActivity()) settingsAdapter = SettingsAdapter(this, requireContext())
presenter = SettingsFragmentPresenter(
settingsViewModel,
settingsAdapter!!,
args.menuTag,
args.game?.gameId ?: ""
)
val dividerDecoration = MaterialDividerItemDecoration( val dividerDecoration = MaterialDividerItemDecoration(
requireContext(), requireContext(),
LinearLayoutManager.VERTICAL LinearLayoutManager.VERTICAL
@ -57,71 +67,86 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
dividerDecoration.isLastItemDecorated = false dividerDecoration.isLastItemDecorated = false
binding.listSettings.apply { binding.listSettings.apply {
adapter = settingsAdapter adapter = settingsAdapter
layoutManager = LinearLayoutManager(activity) layoutManager = LinearLayoutManager(requireContext())
addItemDecoration(dividerDecoration) addItemDecoration(dividerDecoration)
} }
fragmentPresenter.onViewCreated()
binding.toolbarSettings.setNavigationOnClickListener {
settingsViewModel.setShouldNavigateBack(true)
}
settingsViewModel.toolbarTitle.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) binding.toolbarSettingsLayout.title = it
}
settingsViewModel.shouldReloadSettingsList.observe(viewLifecycleOwner) {
if (it) {
settingsViewModel.setShouldReloadSettingsList(false)
presenter.loadSettingsList()
}
}
settingsViewModel.isUsingSearch.observe(viewLifecycleOwner) {
if (it) {
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
} else {
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
}
}
if (args.menuTag == SettingsFile.FILE_NAME_CONFIG) {
binding.toolbarSettings.inflateMenu(R.menu.menu_settings)
binding.toolbarSettings.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_search -> {
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
view.findNavController()
.navigate(R.id.action_settingsFragment_to_settingsSearchFragment)
true
}
else -> false
}
}
}
presenter.onViewCreated()
setInsets() setInsets()
} }
override fun onDetach() { override fun onResume() {
super.onDetach() super.onResume()
activityView = null settingsViewModel.setIsUsingSearch(false)
if (settingsAdapter != null) {
settingsAdapter!!.closeDialog()
}
}
override fun showSettingsList(settingsList: ArrayList<SettingsItem>) {
settingsAdapter!!.setSettingsList(settingsList)
}
override fun loadSettingsList() {
fragmentPresenter.loadSettingsList()
}
override fun loadSubMenu(menuKey: String) {
activityView!!.showSettingsFragment(
menuKey,
true,
requireArguments().getString(ARGUMENT_GAME_ID)!!
)
}
override fun showToastMessage(message: String?, is_long: Boolean) {
activityView!!.showToastMessage(message!!, is_long)
}
override fun putSetting(setting: AbstractSetting) {
fragmentPresenter.putSetting(setting)
}
override fun onSettingChanged() {
activityView!!.onSettingChanged()
} }
private fun setInsets() { private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener( ViewCompat.setOnApplyWindowInsetsListener(
binding.listSettings binding.root
) { view: View, windowInsets: WindowInsetsCompat -> ) { _: View, windowInsets: WindowInsetsCompat ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.updatePadding(bottom = insets.bottom) val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right
val sideMargin = resources.getDimensionPixelSize(R.dimen.spacing_medlarge)
val mlpSettingsList = binding.listSettings.layoutParams as MarginLayoutParams
mlpSettingsList.leftMargin = sideMargin + leftInsets
mlpSettingsList.rightMargin = sideMargin + rightInsets
binding.listSettings.layoutParams = mlpSettingsList
binding.listSettings.updatePadding(
bottom = barInsets.bottom
)
val mlpAppBar = binding.appbarSettings.layoutParams as MarginLayoutParams
mlpAppBar.leftMargin = leftInsets
mlpAppBar.rightMargin = rightInsets
binding.appbarSettings.layoutParams = mlpAppBar
windowInsets windowInsets
} }
} }
companion object {
private const val ARGUMENT_MENU_TAG = "menu_tag"
private const val ARGUMENT_GAME_ID = "game_id"
fun newInstance(menuTag: String?, gameId: String?): Fragment {
val fragment = SettingsFragment()
val arguments = Bundle()
arguments.putString(ARGUMENT_MENU_TAG, menuTag)
arguments.putString(ARGUMENT_GAME_ID, gameId)
fragment.arguments = arguments
return fragment
}
}
} }

View File

@ -3,63 +3,66 @@
package org.yuzu.yuzu_emu.features.settings.ui package org.yuzu.yuzu_emu.features.settings.ui
import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build import android.os.Build
import android.text.TextUtils import android.text.TextUtils
import android.widget.Toast
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.ByteSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.LongSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.StringSetting import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
import org.yuzu.yuzu_emu.features.settings.model.view.* import org.yuzu.yuzu_emu.features.settings.model.view.*
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment import org.yuzu.yuzu_emu.model.SettingsViewModel
import org.yuzu.yuzu_emu.utils.ThemeHelper import org.yuzu.yuzu_emu.utils.NativeConfig
class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) { class SettingsFragmentPresenter(
private var menuTag: String? = null private val settingsViewModel: SettingsViewModel,
private lateinit var gameId: String private val adapter: SettingsAdapter,
private var settingsList: ArrayList<SettingsItem>? = null private var menuTag: String,
private var gameId: String
) {
private var settingsList = ArrayList<SettingsItem>()
private val settingsActivity get() = fragmentView.activityView as SettingsActivity private val preferences: SharedPreferences
private val settings get() = fragmentView.activityView!!.settings get() = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
private lateinit var preferences: SharedPreferences private val context: Context get() = YuzuApplication.appContext
fun onCreate(menuTag: String, gameId: String) { // Extension for populating settings list based on paired settings
this.gameId = gameId fun ArrayList<SettingsItem>.add(key: String) {
this.menuTag = menuTag val item = SettingsItem.settingsItems[key]!!
val pairedSettingKey = item.setting.pairedSettingKey
if (pairedSettingKey.isNotEmpty()) {
val pairedSettingValue = NativeConfig.getBoolean(pairedSettingKey, false)
if (!pairedSettingValue) return
}
add(item)
} }
fun onViewCreated() { fun onViewCreated() {
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
loadSettingsList() loadSettingsList()
} }
fun putSetting(setting: AbstractSetting) {
if (setting.section == null || setting.key == null) {
return
}
val section = settings.getSection(setting.section!!)!!
if (section.getSetting(setting.key!!) == null) {
section.putSetting(setting)
}
}
fun loadSettingsList() { fun loadSettingsList() {
if (!TextUtils.isEmpty(gameId)) { if (!TextUtils.isEmpty(gameId)) {
settingsActivity.setToolbarTitle("Game Settings: $gameId") settingsViewModel.setToolbarTitle(
context.getString(
R.string.advanced_settings_game,
gameId
)
)
} }
val sl = ArrayList<SettingsItem>() val sl = ArrayList<SettingsItem>()
if (menuTag == null) {
return
}
when (menuTag) { when (menuTag) {
SettingsFile.FILE_NAME_CONFIG -> addConfigSettings(sl) SettingsFile.FILE_NAME_CONFIG -> addConfigSettings(sl)
Settings.SECTION_GENERAL -> addGeneralSettings(sl) Settings.SECTION_GENERAL -> addGeneralSettings(sl)
@ -69,335 +72,104 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
Settings.SECTION_THEME -> addThemeSettings(sl) Settings.SECTION_THEME -> addThemeSettings(sl)
Settings.SECTION_DEBUG -> addDebugSettings(sl) Settings.SECTION_DEBUG -> addDebugSettings(sl)
else -> { else -> {
fragmentView.showToastMessage("Unimplemented menu", false) val context = YuzuApplication.appContext
Toast.makeText(
context,
context.getString(R.string.unimplemented_menu),
Toast.LENGTH_SHORT
).show()
return return
} }
} }
settingsList = sl settingsList = sl
fragmentView.showSettingsList(settingsList!!) adapter.submitList(settingsList)
} }
private fun addConfigSettings(sl: ArrayList<SettingsItem>) { private fun addConfigSettings(sl: ArrayList<SettingsItem>) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.advanced_settings)) settingsViewModel.setToolbarTitle(context.getString(R.string.advanced_settings))
sl.apply { sl.apply {
add(SubmenuSetting(R.string.preferences_general, 0, Settings.SECTION_GENERAL))
add(SubmenuSetting(R.string.preferences_system, 0, Settings.SECTION_SYSTEM))
add(SubmenuSetting(R.string.preferences_graphics, 0, Settings.SECTION_RENDERER))
add(SubmenuSetting(R.string.preferences_audio, 0, Settings.SECTION_AUDIO))
add(SubmenuSetting(R.string.preferences_debug, 0, Settings.SECTION_DEBUG))
add( add(
SubmenuSetting( RunnableSetting(R.string.reset_to_default, 0, false) {
R.string.preferences_general, settingsViewModel.setShouldShowResetSettingsDialog(true)
0,
Settings.SECTION_GENERAL
)
)
add(
SubmenuSetting(
R.string.preferences_system,
0,
Settings.SECTION_SYSTEM
)
)
add(
SubmenuSetting(
R.string.preferences_graphics,
0,
Settings.SECTION_RENDERER
)
)
add(
SubmenuSetting(
R.string.preferences_audio,
0,
Settings.SECTION_AUDIO
)
)
add(
SubmenuSetting(
R.string.preferences_debug,
0,
Settings.SECTION_DEBUG
)
)
add(
RunnableSetting(
R.string.reset_to_default,
0,
false
) {
ResetSettingsDialogFragment().show(
settingsActivity.supportFragmentManager,
ResetSettingsDialogFragment.TAG
)
} }
) )
} }
} }
private fun addGeneralSettings(sl: ArrayList<SettingsItem>) { private fun addGeneralSettings(sl: ArrayList<SettingsItem>) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_general)) settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_general))
sl.apply { sl.apply {
add( add(BooleanSetting.RENDERER_USE_SPEED_LIMIT.key)
SwitchSetting( add(ShortSetting.RENDERER_SPEED_LIMIT.key)
IntSetting.RENDERER_USE_SPEED_LIMIT, add(IntSetting.CPU_ACCURACY.key)
R.string.frame_limit_enable, add(BooleanSetting.PICTURE_IN_PICTURE.key)
R.string.frame_limit_enable_description,
IntSetting.RENDERER_USE_SPEED_LIMIT.key,
IntSetting.RENDERER_USE_SPEED_LIMIT.defaultValue
)
)
add(
SliderSetting(
IntSetting.RENDERER_SPEED_LIMIT,
R.string.frame_limit_slider,
R.string.frame_limit_slider_description,
1,
200,
"%",
IntSetting.RENDERER_SPEED_LIMIT.key,
IntSetting.RENDERER_SPEED_LIMIT.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.CPU_ACCURACY,
R.string.cpu_accuracy,
0,
R.array.cpuAccuracyNames,
R.array.cpuAccuracyValues,
IntSetting.CPU_ACCURACY.key,
IntSetting.CPU_ACCURACY.defaultValue
)
)
add(
SwitchSetting(
BooleanSetting.PICTURE_IN_PICTURE,
R.string.picture_in_picture,
R.string.picture_in_picture_description,
BooleanSetting.PICTURE_IN_PICTURE.key,
BooleanSetting.PICTURE_IN_PICTURE.defaultValue
)
)
} }
} }
private fun addSystemSettings(sl: ArrayList<SettingsItem>) { private fun addSystemSettings(sl: ArrayList<SettingsItem>) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_system)) settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_system))
sl.apply { sl.apply {
add( add(BooleanSetting.USE_DOCKED_MODE.key)
SwitchSetting( add(IntSetting.REGION_INDEX.key)
IntSetting.USE_DOCKED_MODE, add(IntSetting.LANGUAGE_INDEX.key)
R.string.use_docked_mode, add(BooleanSetting.USE_CUSTOM_RTC.key)
R.string.use_docked_mode_description, add(LongSetting.CUSTOM_RTC.key)
IntSetting.USE_DOCKED_MODE.key,
IntSetting.USE_DOCKED_MODE.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.REGION_INDEX,
R.string.emulated_region,
0,
R.array.regionNames,
R.array.regionValues,
IntSetting.REGION_INDEX.key,
IntSetting.REGION_INDEX.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.LANGUAGE_INDEX,
R.string.emulated_language,
0,
R.array.languageNames,
R.array.languageValues,
IntSetting.LANGUAGE_INDEX.key,
IntSetting.LANGUAGE_INDEX.defaultValue
)
)
add(
SwitchSetting(
BooleanSetting.USE_CUSTOM_RTC,
R.string.use_custom_rtc,
R.string.use_custom_rtc_description,
BooleanSetting.USE_CUSTOM_RTC.key,
BooleanSetting.USE_CUSTOM_RTC.defaultValue
)
)
add(
DateTimeSetting(
StringSetting.CUSTOM_RTC,
R.string.set_custom_rtc,
0,
StringSetting.CUSTOM_RTC.key,
StringSetting.CUSTOM_RTC.defaultValue
)
)
} }
} }
private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) { private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_graphics)) settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_graphics))
sl.apply { sl.apply {
add( add(IntSetting.RENDERER_ACCURACY.key)
SingleChoiceSetting( add(IntSetting.RENDERER_RESOLUTION.key)
IntSetting.RENDERER_ACCURACY, add(IntSetting.RENDERER_VSYNC.key)
R.string.renderer_accuracy, add(IntSetting.RENDERER_SCALING_FILTER.key)
0, add(IntSetting.RENDERER_ANTI_ALIASING.key)
R.array.rendererAccuracyNames, add(IntSetting.RENDERER_SCREEN_LAYOUT.key)
R.array.rendererAccuracyValues, add(IntSetting.RENDERER_ASPECT_RATIO.key)
IntSetting.RENDERER_ACCURACY.key, add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.key)
IntSetting.RENDERER_ACCURACY.defaultValue add(BooleanSetting.RENDERER_FORCE_MAX_CLOCK.key)
) add(BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS.key)
) add(BooleanSetting.RENDERER_REACTIVE_FLUSHING.key)
add(
SingleChoiceSetting(
IntSetting.RENDERER_RESOLUTION,
R.string.renderer_resolution,
0,
R.array.rendererResolutionNames,
R.array.rendererResolutionValues,
IntSetting.RENDERER_RESOLUTION.key,
IntSetting.RENDERER_RESOLUTION.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.RENDERER_VSYNC,
R.string.renderer_vsync,
0,
R.array.rendererVSyncNames,
R.array.rendererVSyncValues,
IntSetting.RENDERER_VSYNC.key,
IntSetting.RENDERER_VSYNC.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.RENDERER_SCALING_FILTER,
R.string.renderer_scaling_filter,
0,
R.array.rendererScalingFilterNames,
R.array.rendererScalingFilterValues,
IntSetting.RENDERER_SCALING_FILTER.key,
IntSetting.RENDERER_SCALING_FILTER.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.RENDERER_ANTI_ALIASING,
R.string.renderer_anti_aliasing,
0,
R.array.rendererAntiAliasingNames,
R.array.rendererAntiAliasingValues,
IntSetting.RENDERER_ANTI_ALIASING.key,
IntSetting.RENDERER_ANTI_ALIASING.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.RENDERER_SCREEN_LAYOUT,
R.string.renderer_screen_layout,
0,
R.array.rendererScreenLayoutNames,
R.array.rendererScreenLayoutValues,
IntSetting.RENDERER_SCREEN_LAYOUT.key,
IntSetting.RENDERER_SCREEN_LAYOUT.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.RENDERER_ASPECT_RATIO,
R.string.renderer_aspect_ratio,
0,
R.array.rendererAspectRatioNames,
R.array.rendererAspectRatioValues,
IntSetting.RENDERER_ASPECT_RATIO.key,
IntSetting.RENDERER_ASPECT_RATIO.defaultValue
)
)
add(
SwitchSetting(
IntSetting.RENDERER_USE_DISK_SHADER_CACHE,
R.string.use_disk_shader_cache,
R.string.use_disk_shader_cache_description,
IntSetting.RENDERER_USE_DISK_SHADER_CACHE.key,
IntSetting.RENDERER_USE_DISK_SHADER_CACHE.defaultValue
)
)
add(
SwitchSetting(
IntSetting.RENDERER_FORCE_MAX_CLOCK,
R.string.renderer_force_max_clock,
R.string.renderer_force_max_clock_description,
IntSetting.RENDERER_FORCE_MAX_CLOCK.key,
IntSetting.RENDERER_FORCE_MAX_CLOCK.defaultValue
)
)
add(
SwitchSetting(
IntSetting.RENDERER_ASYNCHRONOUS_SHADERS,
R.string.renderer_asynchronous_shaders,
R.string.renderer_asynchronous_shaders_description,
IntSetting.RENDERER_ASYNCHRONOUS_SHADERS.key,
IntSetting.RENDERER_ASYNCHRONOUS_SHADERS.defaultValue
)
)
add(
SwitchSetting(
IntSetting.RENDERER_REACTIVE_FLUSHING,
R.string.renderer_reactive_flushing,
R.string.renderer_reactive_flushing_description,
IntSetting.RENDERER_REACTIVE_FLUSHING.key,
IntSetting.RENDERER_REACTIVE_FLUSHING.defaultValue
)
)
} }
} }
private fun addAudioSettings(sl: ArrayList<SettingsItem>) { private fun addAudioSettings(sl: ArrayList<SettingsItem>) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_audio)) settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_audio))
sl.apply { sl.apply {
add( add(IntSetting.AUDIO_OUTPUT_ENGINE.key)
StringSingleChoiceSetting( add(ByteSetting.AUDIO_VOLUME.key)
StringSetting.AUDIO_OUTPUT_ENGINE,
R.string.audio_output_engine,
0,
settingsActivity.resources.getStringArray(R.array.outputEngineEntries),
settingsActivity.resources.getStringArray(R.array.outputEngineValues),
StringSetting.AUDIO_OUTPUT_ENGINE.key,
StringSetting.AUDIO_OUTPUT_ENGINE.defaultValue
)
)
add(
SliderSetting(
IntSetting.AUDIO_VOLUME,
R.string.audio_volume,
R.string.audio_volume_description,
0,
100,
"%",
IntSetting.AUDIO_VOLUME.key,
IntSetting.AUDIO_VOLUME.defaultValue
)
)
} }
} }
private fun addThemeSettings(sl: ArrayList<SettingsItem>) { private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_theme)) settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_theme))
sl.apply { sl.apply {
val theme: AbstractIntSetting = object : AbstractIntSetting { val theme: AbstractIntSetting = object : AbstractIntSetting {
override var int: Int override val int: Int
get() = preferences.getInt(Settings.PREF_THEME, 0) get() = preferences.getInt(Settings.PREF_THEME, 0)
set(value) {
override fun setInt(value: Int) {
preferences.edit() preferences.edit()
.putInt(Settings.PREF_THEME, value) .putInt(Settings.PREF_THEME, value)
.apply() .apply()
settingsActivity.recreate() settingsViewModel.setShouldRecreate(true)
}
override val key: String = Settings.PREF_THEME
override val category = Settings.Category.UiGeneral
override val isRuntimeModifiable: Boolean = false
override val defaultValue: Int = 0
override fun reset() {
preferences.edit()
.putInt(Settings.PREF_THEME, defaultValue)
.apply()
} }
override val key: String? = null
override val section: String? = null
override val isRuntimeEditable: Boolean = false
override val valueAsString: String
get() = preferences.getInt(Settings.PREF_THEME, 0).toString()
override val defaultValue: Any = 0
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@ -423,20 +195,26 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
} }
val themeMode: AbstractIntSetting = object : AbstractIntSetting { val themeMode: AbstractIntSetting = object : AbstractIntSetting {
override var int: Int override val int: Int
get() = preferences.getInt(Settings.PREF_THEME_MODE, -1) get() = preferences.getInt(Settings.PREF_THEME_MODE, -1)
set(value) {
override fun setInt(value: Int) {
preferences.edit() preferences.edit()
.putInt(Settings.PREF_THEME_MODE, value) .putInt(Settings.PREF_THEME_MODE, value)
.apply() .apply()
ThemeHelper.setThemeMode(settingsActivity) settingsViewModel.setShouldRecreate(true)
}
override val key: String = Settings.PREF_THEME_MODE
override val category = Settings.Category.UiGeneral
override val isRuntimeModifiable: Boolean = false
override val defaultValue: Int = -1
override fun reset() {
preferences.edit()
.putInt(Settings.PREF_BLACK_BACKGROUNDS, defaultValue)
.apply()
settingsViewModel.setShouldRecreate(true)
} }
override val key: String? = null
override val section: String? = null
override val isRuntimeEditable: Boolean = false
override val valueAsString: String
get() = preferences.getInt(Settings.PREF_THEME_MODE, -1).toString()
override val defaultValue: Any = -1
} }
add( add(
@ -450,21 +228,26 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
) )
val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting { val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting {
override var boolean: Boolean override val boolean: Boolean
get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
set(value) {
override fun setBoolean(value: Boolean) {
preferences.edit() preferences.edit()
.putBoolean(Settings.PREF_BLACK_BACKGROUNDS, value) .putBoolean(Settings.PREF_BLACK_BACKGROUNDS, value)
.apply() .apply()
settingsActivity.recreate() settingsViewModel.setShouldRecreate(true)
}
override val key: String = Settings.PREF_BLACK_BACKGROUNDS
override val category = Settings.Category.UiGeneral
override val isRuntimeModifiable: Boolean = false
override val defaultValue: Boolean = false
override fun reset() {
preferences.edit()
.putBoolean(Settings.PREF_BLACK_BACKGROUNDS, defaultValue)
.apply()
settingsViewModel.setShouldRecreate(true)
} }
override val key: String? = null
override val section: String? = null
override val isRuntimeEditable: Boolean = false
override val valueAsString: String
get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
.toString()
override val defaultValue: Any = false
} }
add( add(
@ -478,62 +261,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
} }
private fun addDebugSettings(sl: ArrayList<SettingsItem>) { private fun addDebugSettings(sl: ArrayList<SettingsItem>) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_debug)) settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_debug))
sl.apply { sl.apply {
add(HeaderSetting(R.string.gpu)) add(HeaderSetting(R.string.gpu))
add( add(IntSetting.RENDERER_BACKEND.key)
SingleChoiceSetting( add(BooleanSetting.RENDERER_DEBUG.key)
IntSetting.RENDERER_BACKEND,
R.string.renderer_api,
0,
R.array.rendererApiNames,
R.array.rendererApiValues,
IntSetting.RENDERER_BACKEND.key,
IntSetting.RENDERER_BACKEND.defaultValue
)
)
add(
SwitchSetting(
IntSetting.RENDERER_DEBUG,
R.string.renderer_debug,
R.string.renderer_debug_description,
IntSetting.RENDERER_DEBUG.key,
IntSetting.RENDERER_DEBUG.defaultValue
)
)
add(HeaderSetting(R.string.cpu)) add(HeaderSetting(R.string.cpu))
add( add(BooleanSetting.CPU_DEBUG_MODE.key)
SwitchSetting( add(SettingsItem.FASTMEM_COMBINED)
BooleanSetting.CPU_DEBUG_MODE,
R.string.cpu_debug_mode,
R.string.cpu_debug_mode_description,
BooleanSetting.CPU_DEBUG_MODE.key,
BooleanSetting.CPU_DEBUG_MODE.defaultValue
)
)
val fastmem = object : AbstractBooleanSetting {
override var boolean: Boolean
get() =
BooleanSetting.FASTMEM.boolean && BooleanSetting.FASTMEM_EXCLUSIVES.boolean
set(value) {
BooleanSetting.FASTMEM.boolean = value
BooleanSetting.FASTMEM_EXCLUSIVES.boolean = value
}
override val key: String? = null
override val section: String = Settings.SECTION_CPU
override val isRuntimeEditable: Boolean = false
override val valueAsString: String = ""
override val defaultValue: Any = true
}
add(
SwitchSetting(
fastmem,
R.string.fastmem,
0
)
)
} }
} }
} }

View File

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

View File

@ -29,7 +29,7 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
} }
binding.textSettingValue.visibility = View.VISIBLE binding.textSettingValue.visibility = View.VISIBLE
val epochTime = setting.value.toLong() val epochTime = setting.value
val instant = Instant.ofEpochMilli(epochTime * 1000) val instant = Instant.ofEpochMilli(epochTime * 1000)
val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")) val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
@ -46,7 +46,7 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
override fun onLongClick(clicked: View): Boolean { override fun onLongClick(clicked: View): Boolean {
if (setting.isEditable) { if (setting.isEditable) {
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) return adapter.onLongClick(setting, bindingAdapterPosition)
} }
return false return false
} }

View File

@ -35,7 +35,7 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
} }
} }
} else if (item is StringSingleChoiceSetting) { } else if (item is StringSingleChoiceSetting) {
for (i in item.values!!.indices) { for (i in item.values.indices) {
if (item.values[i] == item.selectedValue) { if (item.values[i] == item.selectedValue) {
binding.textSettingValue.text = item.choices[i] binding.textSettingValue.text = item.choices[i]
break break
@ -66,7 +66,7 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
override fun onLongClick(clicked: View): Boolean { override fun onLongClick(clicked: View): Boolean {
if (setting.isEditable) { if (setting.isEditable) {
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) return adapter.onLongClick(setting, bindingAdapterPosition)
} }
return false return false
} }

View File

@ -41,7 +41,7 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
override fun onLongClick(clicked: View): Boolean { override fun onLongClick(clicked: View): Boolean {
if (setting.isEditable) { if (setting.isEditable) {
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) return adapter.onLongClick(setting, bindingAdapterPosition)
} }
return false return false
} }

View File

@ -25,10 +25,12 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
binding.textSettingDescription.text = "" binding.textSettingDescription.text = ""
binding.textSettingDescription.visibility = View.GONE binding.textSettingDescription.visibility = View.GONE
} }
binding.switchWidget.setOnCheckedChangeListener(null)
binding.switchWidget.isChecked = setting.checked
binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
adapter.onBooleanClick(item, bindingAdapterPosition, binding.switchWidget.isChecked) adapter.onBooleanClick(item, binding.switchWidget.isChecked)
} }
binding.switchWidget.isChecked = setting.isChecked
setStyle(setting.isEditable, binding) setStyle(setting.isEditable, binding)
} }
@ -41,7 +43,7 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
override fun onLongClick(clicked: View): Boolean { override fun onLongClick(clicked: View): Boolean {
if (setting.isEditable) { if (setting.isEditable) {
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) return adapter.onLongClick(setting, bindingAdapterPosition)
} }
return false return false
} }

View File

@ -3,18 +3,15 @@
package org.yuzu.yuzu_emu.features.settings.utils package org.yuzu.yuzu_emu.features.settings.utils
import android.widget.Toast
import java.io.* import java.io.*
import java.util.*
import org.ini4j.Wini import org.ini4j.Wini
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.model.* import org.yuzu.yuzu_emu.features.settings.model.*
import org.yuzu.yuzu_emu.features.settings.model.Settings.SettingsSectionMap
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivityView
import org.yuzu.yuzu_emu.utils.BiMap
import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.Log import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.NativeConfig
/** /**
* Contains static methods for interacting with .ini files in which settings are stored. * Contains static methods for interacting with .ini files in which settings are stored.
@ -22,243 +19,41 @@ import org.yuzu.yuzu_emu.utils.Log
object SettingsFile { object SettingsFile {
const val FILE_NAME_CONFIG = "config" const val FILE_NAME_CONFIG = "config"
private var sectionsMap = BiMap<String?, String?>()
/**
* Reads a given .ini file from disk and returns it as a HashMap of Settings, themselves
* effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
* failed.
*
* @param ini The ini file to load the settings from
* @param isCustomGame
* @param view The current view.
* @return An Observable that emits a HashMap of the file's contents, then completes.
*/
private fun readFile(
ini: File?,
isCustomGame: Boolean,
view: SettingsActivityView? = null
): HashMap<String, SettingSection?> {
val sections: HashMap<String, SettingSection?> = SettingsSectionMap()
var reader: BufferedReader? = null
try {
reader = BufferedReader(FileReader(ini))
var current: SettingSection? = null
var line: String?
while (reader.readLine().also { line = it } != null) {
if (line!!.startsWith("[") && line!!.endsWith("]")) {
current = sectionFromLine(line!!, isCustomGame)
sections[current.name] = current
} else if (current != null) {
val setting = settingFromLine(line!!)
if (setting != null) {
current.putSetting(setting)
}
}
}
} catch (e: FileNotFoundException) {
Log.error("[SettingsFile] File not found: " + e.message)
view?.onSettingsFileNotFound()
} catch (e: IOException) {
Log.error("[SettingsFile] Error reading from: " + e.message)
view?.onSettingsFileNotFound()
} finally {
if (reader != null) {
try {
reader.close()
} catch (e: IOException) {
Log.error("[SettingsFile] Error closing: " + e.message)
}
}
}
return sections
}
fun readFile(fileName: String, view: SettingsActivityView?): HashMap<String, SettingSection?> {
return readFile(getSettingsFile(fileName), false, view)
}
fun readFile(fileName: String): HashMap<String, SettingSection?> =
readFile(getSettingsFile(fileName), false)
/**
* Reads a given .ini file from disk and returns it as a HashMap of SettingSections, themselves
* effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
* failed.
*
* @param gameId the id of the game to load it's settings.
* @param view The current view.
*/
fun readCustomGameSettings(
gameId: String,
view: SettingsActivityView?
): HashMap<String, SettingSection?> {
return readFile(getCustomGameSettingsFile(gameId), true, view)
}
/** /**
* Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error * Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error
* telling why it failed. * telling why it failed.
* *
* @param fileName The target filename without a path or extension. * @param fileName The target filename without a path or extension.
* @param sections The HashMap containing the Settings we want to serialize.
* @param view The current view.
*/ */
fun saveFile( fun saveFile(fileName: String) {
fileName: String,
sections: TreeMap<String, SettingSection>,
view: SettingsActivityView
) {
val ini = getSettingsFile(fileName) val ini = getSettingsFile(fileName)
try { try {
val writer = Wini(ini) val wini = Wini(ini)
val keySet: Set<String> = sections.keys for (specificCategory in Settings.Category.values()) {
for (key in keySet) { val categoryHeader = NativeConfig.getConfigHeader(specificCategory.ordinal)
val section = sections[key] for (setting in Settings.settingsList) {
writeSection(writer, section!!) if (setting.key!!.isEmpty()) continue
val settingCategoryHeader =
NativeConfig.getConfigHeader(setting.category.ordinal)
val iniSetting: String? = wini.get(categoryHeader, setting.key)
if (iniSetting != null || settingCategoryHeader == categoryHeader) {
wini.put(settingCategoryHeader, setting.key, setting.valueAsString)
} }
writer.store() }
}
wini.store()
} catch (e: IOException) { } catch (e: IOException) {
Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.message) Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.message)
view.showToastMessage( val context = YuzuApplication.appContext
YuzuApplication.appContext Toast.makeText(
.getString(R.string.error_saving, fileName, e.message), context,
false context.getString(R.string.error_saving, fileName, e.message),
) Toast.LENGTH_SHORT
).show()
} }
} }
fun saveCustomGameSettings(gameId: String?, sections: HashMap<String, SettingSection?>) { fun getSettingsFile(fileName: String): File =
val sortedSections: Set<String> = TreeSet(sections.keys) File(DirectoryInitialization.userDirectory + "/config/" + fileName + ".ini")
for (sectionKey in sortedSections) {
val section = sections[sectionKey]
val settings = section!!.settings
val sortedKeySet: Set<String> = TreeSet(settings.keys)
for (settingKey in sortedKeySet) {
val setting = settings[settingKey]
NativeLibrary.setUserSetting(
gameId,
mapSectionNameFromIni(
section.name
),
setting!!.key,
setting.valueAsString
)
}
}
}
private fun mapSectionNameFromIni(generalSectionName: String): String? {
return if (sectionsMap.getForward(generalSectionName) != null) {
sectionsMap.getForward(generalSectionName)
} else {
generalSectionName
}
}
private fun mapSectionNameToIni(generalSectionName: String): String {
return if (sectionsMap.getBackward(generalSectionName) != null) {
sectionsMap.getBackward(generalSectionName).toString()
} else {
generalSectionName
}
}
fun getSettingsFile(fileName: String): File {
return File(
DirectoryInitialization.userDirectory + "/config/" + fileName + ".ini"
)
}
private fun getCustomGameSettingsFile(gameId: String): File {
return File(DirectoryInitialization.userDirectory + "/GameSettings/" + 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) {
intSetting.int = value.toInt()
return intSetting
}
val floatSetting = FloatSetting.from(key)
if (floatSetting != null) {
floatSetting.float = value.toFloat()
return floatSetting
}
val stringSetting = StringSetting.from(key)
if (stringSetting != null) {
stringSetting.string = value
return stringSetting
}
return null
}
/**
* Writes the contents of a Section HashMap to disk.
*
* @param parser A Wini pointed at a file on disk.
* @param section A section containing settings to be written to the file.
*/
private fun writeSection(parser: Wini, section: SettingSection) {
// Write the section header.
val header = section.name
// Write this section's values.
val settings = section.settings
val keySet: Set<String> = settings.keys
for (key in keySet) {
val setting = settings[key]
parser.put(header, setting!!.key, setting.valueAsString)
}
BooleanSetting.values().forEach {
if (!keySet.contains(it.key)) {
parser.put(header, it.key, it.valueAsString)
}
}
IntSetting.values().forEach {
if (!keySet.contains(it.key)) {
parser.put(header, it.key, it.valueAsString)
}
}
StringSetting.values().forEach {
if (!keySet.contains(it.key)) {
parser.put(header, it.key, it.valueAsString)
}
}
}
} }

View File

@ -29,6 +29,7 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.window.layout.FoldingFeature import androidx.window.layout.FoldingFeature
@ -38,6 +39,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
@ -46,7 +48,6 @@ import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.overlay.InputOverlay import org.yuzu.yuzu_emu.overlay.InputOverlay
@ -158,7 +159,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
R.id.menu_settings -> { R.id.menu_settings -> {
SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") val action = HomeNavigationDirections.actionGlobalSettingsActivity(
null,
SettingsFile.FILE_NAME_CONFIG
)
binding.root.findNavController().navigate(action)
true true
} }
@ -230,7 +235,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (!DirectoryInitialization.areDirectoriesReady) { if (!DirectoryInitialization.areDirectoriesReady) {
DirectoryInitialization.start(requireContext()) DirectoryInitialization.start()
} }
updateScreenLayout() updateScreenLayout()

View File

@ -25,17 +25,18 @@ import androidx.core.view.updatePadding
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
import org.yuzu.yuzu_emu.BuildConfig import org.yuzu.yuzu_emu.BuildConfig
import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter
import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding
import org.yuzu.yuzu_emu.features.DocumentProvider import org.yuzu.yuzu_emu.features.DocumentProvider
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.model.HomeSetting import org.yuzu.yuzu_emu.model.HomeSetting
import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.model.HomeViewModel
@ -74,7 +75,13 @@ class HomeSettingsFragment : Fragment() {
R.string.advanced_settings, R.string.advanced_settings,
R.string.settings_description, R.string.settings_description,
R.drawable.ic_settings, R.drawable.ic_settings,
{ SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") } {
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
null,
SettingsFile.FILE_NAME_CONFIG
)
binding.root.findNavController().navigate(action)
}
) )
) )
add( add(
@ -90,7 +97,13 @@ class HomeSettingsFragment : Fragment() {
R.string.preferences_theme, R.string.preferences_theme,
R.string.theme_and_color_description, R.string.theme_and_color_description,
R.drawable.ic_palette, R.drawable.ic_palette,
{ SettingsActivity.launch(requireContext(), Settings.SECTION_THEME, "") } {
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
null,
Settings.SECTION_THEME
)
binding.root.findNavController().navigate(action)
}
) )
) )
add( add(

View File

@ -0,0 +1,235 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.slider.Slider
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogSliderBinding
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
import org.yuzu.yuzu_emu.model.SettingsViewModel
class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener {
private var type = 0
private var position = 0
private var defaultCancelListener =
DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> closeDialog() }
private val settingsViewModel: SettingsViewModel by activityViewModels()
private lateinit var sliderBinding: DialogSliderBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
type = requireArguments().getInt(TYPE)
position = requireArguments().getInt(POSITION)
if (settingsViewModel.clickedItem == null) dismiss()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return when (type) {
TYPE_RESET_SETTING -> {
MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.reset_setting_confirmation)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
settingsViewModel.clickedItem!!.setting.reset()
settingsViewModel.setAdapterItemChanged(position)
settingsViewModel.shouldSave = true
}
.setNegativeButton(android.R.string.cancel, null)
.create()
}
SettingsItem.TYPE_SINGLE_CHOICE -> {
val item = settingsViewModel.clickedItem as SingleChoiceSetting
val value = getSelectionForSingleChoiceValue(item)
MaterialAlertDialogBuilder(requireContext())
.setTitle(item.nameId)
.setSingleChoiceItems(item.choicesId, value, this)
.create()
}
SettingsItem.TYPE_SLIDER -> {
sliderBinding = DialogSliderBinding.inflate(layoutInflater)
val item = settingsViewModel.clickedItem as SliderSetting
settingsViewModel.setSliderTextValue(item.selectedValue.toFloat(), item.units)
sliderBinding.slider.apply {
valueFrom = item.min.toFloat()
valueTo = item.max.toFloat()
value = settingsViewModel.sliderProgress.value.toFloat()
addOnChangeListener { _: Slider, value: Float, _: Boolean ->
settingsViewModel.setSliderTextValue(value, item.units)
}
}
MaterialAlertDialogBuilder(requireContext())
.setTitle(item.nameId)
.setView(sliderBinding.root)
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, defaultCancelListener)
.create()
}
SettingsItem.TYPE_STRING_SINGLE_CHOICE -> {
val item = settingsViewModel.clickedItem as StringSingleChoiceSetting
MaterialAlertDialogBuilder(requireContext())
.setTitle(item.nameId)
.setSingleChoiceItems(item.choices, item.selectValueIndex, this)
.create()
}
else -> super.onCreateDialog(savedInstanceState)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return when (type) {
SettingsItem.TYPE_SLIDER -> sliderBinding.root
else -> super.onCreateView(inflater, container, savedInstanceState)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
when (type) {
SettingsItem.TYPE_SLIDER -> {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
settingsViewModel.sliderTextValue.collect {
sliderBinding.textValue.text = it
}
}
repeatOnLifecycle(Lifecycle.State.CREATED) {
settingsViewModel.sliderProgress.collect {
sliderBinding.slider.value = it.toFloat()
}
}
}
}
}
}
override fun onClick(dialog: DialogInterface, which: Int) {
when (settingsViewModel.clickedItem) {
is SingleChoiceSetting -> {
val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
val value = getValueForSingleChoiceSelection(scSetting, which)
if (scSetting.selectedValue != value) {
settingsViewModel.shouldSave = true
}
scSetting.selectedValue = value
}
is StringSingleChoiceSetting -> {
val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
val value = scSetting.getValueAt(which)
if (scSetting.selectedValue != value) settingsViewModel.shouldSave = true
scSetting.selectedValue = value
}
is SliderSetting -> {
val sliderSetting = settingsViewModel.clickedItem as SliderSetting
if (sliderSetting.selectedValue != settingsViewModel.sliderProgress.value) {
settingsViewModel.shouldSave = true
}
sliderSetting.selectedValue = settingsViewModel.sliderProgress.value
}
}
closeDialog()
}
private fun closeDialog() {
settingsViewModel.setAdapterItemChanged(position)
settingsViewModel.clickedItem = null
settingsViewModel.setSliderProgress(-1f)
dismiss()
}
private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int {
val valuesId = item.valuesId
return if (valuesId > 0) {
val valuesArray = requireContext().resources.getIntArray(valuesId)
valuesArray[which]
} else {
which
}
}
private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
val value = item.selectedValue
val valuesId = item.valuesId
if (valuesId > 0) {
val valuesArray = requireContext().resources.getIntArray(valuesId)
for (index in valuesArray.indices) {
val current = valuesArray[index]
if (current == value) {
return index
}
}
} else {
return value
}
return -1
}
companion object {
const val TAG = "SettingsDialogFragment"
const val TYPE_RESET_SETTING = -1
const val TITLE = "Title"
const val TYPE = "Type"
const val POSITION = "Position"
fun newInstance(
settingsViewModel: SettingsViewModel,
clickedItem: SettingsItem,
type: Int,
position: Int
): SettingsDialogFragment {
when (type) {
SettingsItem.TYPE_HEADER,
SettingsItem.TYPE_SWITCH,
SettingsItem.TYPE_SUBMENU,
SettingsItem.TYPE_DATETIME_SETTING,
SettingsItem.TYPE_RUNNABLE ->
throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!")
SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress(
(clickedItem as SliderSetting).selectedValue.toFloat()
)
}
settingsViewModel.clickedItem = clickedItem
val args = Bundle()
args.putInt(TYPE, type)
args.putInt(POSITION, position)
val fragment = SettingsDialogFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -0,0 +1,184 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.divider.MaterialDividerItemDecoration
import com.google.android.material.transition.MaterialSharedAxis
import info.debatty.java.stringsimilarity.Cosine
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.FragmentSettingsSearchBinding
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.model.SettingsViewModel
import org.yuzu.yuzu_emu.utils.NativeConfig
class SettingsSearchFragment : Fragment() {
private var _binding: FragmentSettingsSearchBinding? = null
private val binding get() = _binding!!
private var settingsAdapter: SettingsAdapter? = null
private val settingsViewModel: SettingsViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSettingsSearchBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
settingsViewModel.setIsUsingSearch(true)
if (savedInstanceState != null) {
binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
}
settingsAdapter = SettingsAdapter(this, requireContext())
val dividerDecoration = MaterialDividerItemDecoration(
requireContext(),
LinearLayoutManager.VERTICAL
)
dividerDecoration.isLastItemDecorated = false
binding.settingsList.apply {
adapter = settingsAdapter
layoutManager = LinearLayoutManager(requireContext())
addItemDecoration(dividerDecoration)
}
focusSearch()
binding.backButton.setOnClickListener { settingsViewModel.setShouldNavigateBack(true) }
binding.searchBackground.setOnClickListener { focusSearch() }
binding.clearButton.setOnClickListener { binding.searchText.setText("") }
binding.searchText.doOnTextChanged { _, _, _, _ ->
search()
binding.settingsList.smoothScrollToPosition(0)
}
settingsViewModel.shouldReloadSettingsList.observe(viewLifecycleOwner) {
if (it) {
settingsViewModel.setShouldReloadSettingsList(false)
search()
}
}
search()
setInsets()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(SEARCH_TEXT, binding.searchText.text.toString())
}
private fun search() {
val searchTerm = binding.searchText.text.toString().lowercase()
binding.clearButton.visibility =
if (searchTerm.isEmpty()) View.INVISIBLE else View.VISIBLE
if (searchTerm.isEmpty()) {
binding.noResultsView.visibility = View.VISIBLE
settingsAdapter?.submitList(emptyList())
return
}
val baseList = SettingsItem.settingsItems
val similarityAlgorithm = if (searchTerm.length > 2) Cosine() else Cosine(1)
val sortedList: List<SettingsItem> = baseList.mapNotNull { item ->
val title = getString(item.value.nameId).lowercase()
val similarity = similarityAlgorithm.similarity(searchTerm, title)
if (similarity > 0.08) {
Pair(similarity, item)
} else {
null
}
}.sortedByDescending { it.first }.mapNotNull {
val item = it.second.value
val pairedSettingKey = item.setting.pairedSettingKey
val optionalSetting: SettingsItem? = if (pairedSettingKey.isNotEmpty()) {
val pairedSettingValue = NativeConfig.getBoolean(pairedSettingKey, false)
if (pairedSettingValue) it.second.value else null
} else {
it.second.value
}
optionalSetting
}
settingsAdapter?.submitList(sortedList)
binding.noResultsView.visibility =
if (sortedList.isEmpty()) View.VISIBLE else View.INVISIBLE
}
private fun focusSearch() {
binding.searchText.requestFocus()
val imm = requireActivity()
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT)
}
private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { _: View, windowInsets: WindowInsetsCompat ->
val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med)
val sideMargin = resources.getDimensionPixelSize(R.dimen.spacing_medlarge)
val topMargin = resources.getDimensionPixelSize(R.dimen.spacing_chip)
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right
binding.settingsList.updatePadding(bottom = barInsets.bottom + extraListSpacing)
binding.frameSearch.updatePadding(
left = leftInsets + sideMargin,
top = barInsets.top + topMargin,
right = rightInsets + sideMargin
)
binding.noResultsView.updatePadding(
left = leftInsets,
right = rightInsets,
bottom = barInsets.bottom
)
val mlpSettingsList = binding.settingsList.layoutParams as ViewGroup.MarginLayoutParams
mlpSettingsList.leftMargin = leftInsets + sideMargin
mlpSettingsList.rightMargin = rightInsets + sideMargin
binding.settingsList.layoutParams = mlpSettingsList
val mlpDivider = binding.divider.layoutParams as ViewGroup.MarginLayoutParams
mlpDivider.leftMargin = leftInsets + sideMargin
mlpDivider.rightMargin = rightInsets + sideMargin
binding.divider.layoutParams = mlpDivider
windowInsets
}
companion object {
const val SEARCH_TEXT = "SearchText"
}
}

View File

@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.model
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
class SettingsViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
var game: Game? = null
var shouldSave = false
var clickedItem: SettingsItem? = null
private val _toolbarTitle = MutableLiveData("")
val toolbarTitle: LiveData<String> get() = _toolbarTitle
private val _shouldRecreate = MutableLiveData(false)
val shouldRecreate: LiveData<Boolean> get() = _shouldRecreate
private val _shouldNavigateBack = MutableLiveData(false)
val shouldNavigateBack: LiveData<Boolean> get() = _shouldNavigateBack
private val _shouldShowResetSettingsDialog = MutableLiveData(false)
val shouldShowResetSettingsDialog: LiveData<Boolean> get() = _shouldShowResetSettingsDialog
private val _shouldReloadSettingsList = MutableLiveData(false)
val shouldReloadSettingsList: LiveData<Boolean> get() = _shouldReloadSettingsList
private val _isUsingSearch = MutableLiveData(false)
val isUsingSearch: LiveData<Boolean> get() = _isUsingSearch
val sliderProgress = savedStateHandle.getStateFlow(KEY_SLIDER_PROGRESS, -1)
val sliderTextValue = savedStateHandle.getStateFlow(KEY_SLIDER_TEXT_VALUE, "")
val adapterItemChanged = savedStateHandle.getStateFlow(KEY_ADAPTER_ITEM_CHANGED, -1)
fun setToolbarTitle(value: String) {
_toolbarTitle.value = value
}
fun setShouldRecreate(value: Boolean) {
_shouldRecreate.value = value
}
fun setShouldNavigateBack(value: Boolean) {
_shouldNavigateBack.value = value
}
fun setShouldShowResetSettingsDialog(value: Boolean) {
_shouldShowResetSettingsDialog.value = value
}
fun setShouldReloadSettingsList(value: Boolean) {
_shouldReloadSettingsList.value = value
}
fun setIsUsingSearch(value: Boolean) {
_isUsingSearch.value = value
}
fun setSliderTextValue(value: Float, units: String) {
savedStateHandle[KEY_SLIDER_PROGRESS] = value
savedStateHandle[KEY_SLIDER_TEXT_VALUE] = String.format(
YuzuApplication.appContext.getString(R.string.value_with_units),
value.toInt().toString(),
units
)
}
fun setSliderProgress(value: Float) {
savedStateHandle[KEY_SLIDER_PROGRESS] = value
}
fun setAdapterItemChanged(value: Int) {
savedStateHandle[KEY_ADAPTER_ITEM_CHANGED] = value
}
fun clear() {
game = null
shouldSave = false
}
companion object {
const val KEY_SLIDER_TEXT_VALUE = "SliderTextValue"
const val KEY_SLIDER_PROGRESS = "SliderProgress"
const val KEY_ADAPTER_ITEM_CHANGED = "AdapterItemChanged"
}
}

View File

@ -33,14 +33,13 @@ import java.io.IOException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.activities.EmulationActivity import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
import org.yuzu.yuzu_emu.fragments.LongMessageDialogFragment import org.yuzu.yuzu_emu.fragments.LongMessageDialogFragment
@ -54,7 +53,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
private val homeViewModel: HomeViewModel by viewModels() private val homeViewModel: HomeViewModel by viewModels()
private val gamesViewModel: GamesViewModel by viewModels() private val gamesViewModel: GamesViewModel by viewModels()
private val settingsViewModel: SettingsViewModel by viewModels()
override var themeId: Int = 0 override var themeId: Int = 0
@ -62,8 +60,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
val splashScreen = installSplashScreen() val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
settingsViewModel.settings.loadSettings()
ThemeHelper.setTheme(this) ThemeHelper.setTheme(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -109,11 +105,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
when (it.itemId) { when (it.itemId) {
R.id.gamesFragment -> gamesViewModel.setShouldScrollToTop(true) R.id.gamesFragment -> gamesViewModel.setShouldScrollToTop(true)
R.id.searchFragment -> gamesViewModel.setSearchFocused(true) R.id.searchFragment -> gamesViewModel.setSearchFocused(true)
R.id.homeSettingsFragment -> SettingsActivity.launch( R.id.homeSettingsFragment -> {
this, val action = HomeNavigationDirections.actionGlobalSettingsActivity(
SettingsFile.FILE_NAME_CONFIG, null,
"" SettingsFile.FILE_NAME_CONFIG
) )
navHostFragment.navController.navigate(action)
}
} }
} }

View File

@ -1,25 +0,0 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.utils
class BiMap<K, V> {
private val forward: MutableMap<K, V> = HashMap()
private val backward: MutableMap<V, K> = HashMap()
@Synchronized
fun add(key: K, value: V) {
forward[key] = value
backward[value] = key
}
@Synchronized
fun getForward(key: K): V? {
return forward[key]
}
@Synchronized
fun getBackward(key: V): K? {
return backward[key]
}
}

View File

@ -3,18 +3,18 @@
package org.yuzu.yuzu_emu.utils package org.yuzu.yuzu_emu.utils
import android.content.Context
import java.io.IOException import java.io.IOException
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.YuzuApplication
object DirectoryInitialization { object DirectoryInitialization {
private var userPath: String? = null private var userPath: String? = null
var areDirectoriesReady: Boolean = false var areDirectoriesReady: Boolean = false
fun start(context: Context) { fun start() {
if (!areDirectoriesReady) { if (!areDirectoriesReady) {
initializeInternalStorage(context) initializeInternalStorage()
NativeLibrary.initializeEmulation() NativeLibrary.initializeEmulation()
areDirectoriesReady = true areDirectoriesReady = true
} }
@ -26,9 +26,9 @@ object DirectoryInitialization {
return userPath return userPath
} }
private fun initializeInternalStorage(context: Context) { private fun initializeInternalStorage() {
try { try {
userPath = context.getExternalFilesDir(null)!!.canonicalPath userPath = YuzuApplication.appContext.getExternalFilesDir(null)!!.canonicalPath
NativeLibrary.setAppDirectory(userPath!!) NativeLibrary.setAppDirectory(userPath!!)
} catch (e: IOException) { } catch (e: IOException) {
e.printStackTrace() e.printStackTrace()

View File

@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.utils
object NativeConfig {
external fun getBoolean(key: String, getDefault: Boolean): Boolean
external fun setBoolean(key: String, value: Boolean)
external fun getByte(key: String, getDefault: Boolean): Byte
external fun setByte(key: String, value: Byte)
external fun getShort(key: String, getDefault: Boolean): Short
external fun setShort(key: String, value: Short)
external fun getInt(key: String, getDefault: Boolean): Int
external fun setInt(key: String, value: Int)
external fun getFloat(key: String, getDefault: Boolean): Float
external fun setFloat(key: String, value: Float)
external fun getLong(key: String, getDefault: Boolean): Long
external fun setLong(key: String, value: Long)
external fun getString(key: String, getDefault: Boolean): String
external fun setString(key: String, value: String)
external fun getIsRuntimeModifiable(key: String): Boolean
external fun getConfigHeader(category: Int): String
external fun getPairedSettingKey(key: String): String
}

View File

@ -14,6 +14,8 @@ add_library(yuzu-android SHARED
id_cache.cpp id_cache.cpp
id_cache.h id_cache.h
native.cpp native.cpp
native_config.cpp
uisettings.cpp
) )
set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR}) set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR})

View File

@ -16,18 +16,20 @@
#include "input_common/main.h" #include "input_common/main.h"
#include "jni/config.h" #include "jni/config.h"
#include "jni/default_ini.h" #include "jni/default_ini.h"
#include "uisettings.h"
namespace FS = Common::FS; namespace FS = Common::FS;
Config::Config(std::optional<std::filesystem::path> config_path) Config::Config(const std::string& config_name, ConfigType config_type)
: config_loc{config_path.value_or(FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "config.ini")}, : type(config_type), global{config_type == ConfigType::GlobalConfig} {
config{std::make_unique<INIReader>(FS::PathToUTF8String(config_loc))} { Initialize(config_name);
Reload();
} }
Config::~Config() = default; Config::~Config() = default;
bool Config::LoadINI(const std::string& default_contents, bool retry) { bool Config::LoadINI(const std::string& default_contents, bool retry) {
void(FS::CreateParentDir(config_loc));
config = std::make_unique<INIReader>(FS::PathToUTF8String(config_loc));
const auto config_loc_str = FS::PathToUTF8String(config_loc); const auto config_loc_str = FS::PathToUTF8String(config_loc);
if (config->ParseError() < 0) { if (config->ParseError() < 0) {
if (retry) { if (retry) {
@ -301,9 +303,28 @@ void Config::ReadValues() {
// Network // Network
ReadSetting("Network", Settings::values.network_interface); ReadSetting("Network", Settings::values.network_interface);
// Android
ReadSetting("Android", AndroidSettings::values.picture_in_picture);
ReadSetting("Android", AndroidSettings::values.screen_layout);
} }
void Config::Reload() { void Config::Initialize(const std::string& config_name) {
const auto fs_config_loc = FS::GetYuzuPath(FS::YuzuPath::ConfigDir);
const auto config_file = fmt::format("{}.ini", config_name);
switch (type) {
case ConfigType::GlobalConfig:
config_loc = FS::PathToUTF8String(fs_config_loc / config_file);
break;
case ConfigType::PerGameConfig:
config_loc = FS::PathToUTF8String(fs_config_loc / "custom" / FS::ToU8String(config_file));
break;
case ConfigType::InputProfile:
config_loc = FS::PathToUTF8String(fs_config_loc / "input" / config_file);
LoadINI(DefaultINI::android_config_file);
return;
}
LoadINI(DefaultINI::android_config_file); LoadINI(DefaultINI::android_config_file);
ReadValues(); ReadValues();
} }

View File

@ -13,25 +13,35 @@
class INIReader; class INIReader;
class Config { class Config {
std::filesystem::path config_loc;
std::unique_ptr<INIReader> config;
bool LoadINI(const std::string& default_contents = "", bool retry = true); bool LoadINI(const std::string& default_contents = "", bool retry = true);
void ReadValues();
public: public:
explicit Config(std::optional<std::filesystem::path> config_path = std::nullopt); enum class ConfigType {
GlobalConfig,
PerGameConfig,
InputProfile,
};
explicit Config(const std::string& config_name = "config",
ConfigType config_type = ConfigType::GlobalConfig);
~Config(); ~Config();
void Reload(); void Initialize(const std::string& config_name);
private: private:
/** /**
* Applies a value read from the sdl2_config to a Setting. * Applies a value read from the config to a Setting.
* *
* @param group The name of the INI group * @param group The name of the INI group
* @param setting The yuzu setting to modify * @param setting The yuzu setting to modify
*/ */
template <typename Type, bool ranged> template <typename Type, bool ranged>
void ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting); void ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting);
void ReadValues();
const ConfigType type;
std::unique_ptr<INIReader> config;
std::string config_loc;
const bool global;
}; };

View File

@ -824,34 +824,6 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_reloadSettings(JNIEnv* env, jclass cl
Config{}; Config{};
} }
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getUserSetting(JNIEnv* env, jclass clazz,
jstring j_game_id, jstring j_section,
jstring j_key) {
std::string_view game_id = env->GetStringUTFChars(j_game_id, 0);
std::string_view section = env->GetStringUTFChars(j_section, 0);
std::string_view key = env->GetStringUTFChars(j_key, 0);
env->ReleaseStringUTFChars(j_game_id, game_id.data());
env->ReleaseStringUTFChars(j_section, section.data());
env->ReleaseStringUTFChars(j_key, key.data());
return env->NewStringUTF("");
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setUserSetting(JNIEnv* env, jclass clazz,
jstring j_game_id, jstring j_section,
jstring j_key, jstring j_value) {
std::string_view game_id = env->GetStringUTFChars(j_game_id, 0);
std::string_view section = env->GetStringUTFChars(j_section, 0);
std::string_view key = env->GetStringUTFChars(j_key, 0);
std::string_view value = env->GetStringUTFChars(j_value, 0);
env->ReleaseStringUTFChars(j_game_id, game_id.data());
env->ReleaseStringUTFChars(j_section, section.data());
env->ReleaseStringUTFChars(j_key, key.data());
env->ReleaseStringUTFChars(j_value, value.data());
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_initGameIni(JNIEnv* env, jclass clazz, void Java_org_yuzu_yuzu_1emu_NativeLibrary_initGameIni(JNIEnv* env, jclass clazz,
jstring j_game_id) { jstring j_game_id) {
std::string_view game_id = env->GetStringUTFChars(j_game_id, 0); std::string_view game_id = env->GetStringUTFChars(j_game_id, 0);

View File

@ -0,0 +1,237 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <string>
#include <jni.h>
#include "common/logging/log.h"
#include "common/settings.h"
#include "jni/android_common/android_common.h"
#include "jni/config.h"
#include "uisettings.h"
template <typename T>
Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) {
auto key = GetJString(env, jkey);
auto basicSetting = Settings::values.linkage.by_key[key];
auto basicAndroidSetting = AndroidSettings::values.linkage.by_key[key];
if (basicSetting != 0) {
return static_cast<Settings::Setting<T>*>(basicSetting);
}
if (basicAndroidSetting != 0) {
return static_cast<Settings::Setting<T>*>(basicAndroidSetting);
}
LOG_ERROR(Frontend, "[Android Native] Could not find setting - {}", key);
return nullptr;
}
extern "C" {
jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getBoolean(JNIEnv* env, jobject obj,
jstring jkey, jboolean getDefault) {
auto setting = getSetting<bool>(env, jkey);
if (setting == nullptr) {
return false;
}
setting->SetGlobal(true);
if (static_cast<bool>(getDefault)) {
return setting->GetDefault();
}
return setting->GetValue();
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setBoolean(JNIEnv* env, jobject obj, jstring jkey,
jboolean value) {
auto setting = getSetting<bool>(env, jkey);
if (setting == nullptr) {
return;
}
setting->SetGlobal(true);
setting->SetValue(static_cast<bool>(value));
}
jbyte Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getByte(JNIEnv* env, jobject obj, jstring jkey,
jboolean getDefault) {
auto setting = getSetting<u8>(env, jkey);
if (setting == nullptr) {
return -1;
}
setting->SetGlobal(true);
if (static_cast<bool>(getDefault)) {
return setting->GetDefault();
}
return setting->GetValue();
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setByte(JNIEnv* env, jobject obj, jstring jkey,
jbyte value) {
auto setting = getSetting<u8>(env, jkey);
if (setting == nullptr) {
return;
}
setting->SetGlobal(true);
setting->SetValue(value);
}
jshort Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getShort(JNIEnv* env, jobject obj, jstring jkey,
jboolean getDefault) {
auto setting = getSetting<u16>(env, jkey);
if (setting == nullptr) {
return -1;
}
setting->SetGlobal(true);
if (static_cast<bool>(getDefault)) {
return setting->GetDefault();
}
return setting->GetValue();
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setShort(JNIEnv* env, jobject obj, jstring jkey,
jshort value) {
auto setting = getSetting<u16>(env, jkey);
if (setting == nullptr) {
return;
}
setting->SetGlobal(true);
setting->SetValue(value);
}
jint Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInt(JNIEnv* env, jobject obj, jstring jkey,
jboolean getDefault) {
auto setting = getSetting<int>(env, jkey);
if (setting == nullptr) {
return -1;
}
setting->SetGlobal(true);
if (static_cast<bool>(getDefault)) {
return setting->GetDefault();
}
return setting->GetValue();
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInt(JNIEnv* env, jobject obj, jstring jkey,
jint value) {
auto setting = getSetting<int>(env, jkey);
if (setting == nullptr) {
return;
}
setting->SetGlobal(true);
setting->SetValue(value);
}
jfloat Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getFloat(JNIEnv* env, jobject obj, jstring jkey,
jboolean getDefault) {
auto setting = getSetting<float>(env, jkey);
if (setting == nullptr) {
return -1;
}
setting->SetGlobal(true);
if (static_cast<bool>(getDefault)) {
return setting->GetDefault();
}
return setting->GetValue();
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setFloat(JNIEnv* env, jobject obj, jstring jkey,
jfloat value) {
auto setting = getSetting<float>(env, jkey);
if (setting == nullptr) {
return;
}
setting->SetGlobal(true);
setting->SetValue(value);
}
jlong Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getLong(JNIEnv* env, jobject obj, jstring jkey,
jboolean getDefault) {
auto setting = getSetting<long>(env, jkey);
if (setting == nullptr) {
return -1;
}
setting->SetGlobal(true);
if (static_cast<bool>(getDefault)) {
return setting->GetDefault();
}
return setting->GetValue();
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setLong(JNIEnv* env, jobject obj, jstring jkey,
jlong value) {
auto setting = getSetting<long>(env, jkey);
if (setting == nullptr) {
return;
}
setting->SetGlobal(true);
setting->SetValue(value);
}
jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getString(JNIEnv* env, jobject obj, jstring jkey,
jboolean getDefault) {
auto setting = getSetting<std::string>(env, jkey);
if (setting == nullptr) {
return ToJString(env, "");
}
setting->SetGlobal(true);
if (static_cast<bool>(getDefault)) {
return ToJString(env, setting->GetDefault());
}
return ToJString(env, setting->GetValue());
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setString(JNIEnv* env, jobject obj, jstring jkey,
jstring value) {
auto setting = getSetting<std::string>(env, jkey);
if (setting == nullptr) {
return;
}
setting->SetGlobal(true);
setting->SetValue(GetJString(env, value));
}
jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsRuntimeModifiable(JNIEnv* env, jobject obj,
jstring jkey) {
auto key = GetJString(env, jkey);
auto setting = Settings::values.linkage.by_key[key];
if (setting != 0) {
return setting->RuntimeModfiable();
}
LOG_ERROR(Frontend, "[Android Native] Could not find setting - {}", key);
return true;
}
jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getConfigHeader(JNIEnv* env, jobject obj,
jint jcategory) {
auto category = static_cast<Settings::Category>(jcategory);
return ToJString(env, Settings::TranslateCategory(category));
}
jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* env, jobject obj,
jstring jkey) {
auto setting = getSetting<std::string>(env, jkey);
if (setting == nullptr) {
return ToJString(env, "");
}
if (setting->PairedSetting() == nullptr) {
return ToJString(env, "");
}
return ToJString(env, setting->PairedSetting()->GetLabel());
}
} // extern "C"

View File

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "uisettings.h"
namespace AndroidSettings {
Values values;
} // namespace AndroidSettings

View File

@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <common/settings_common.h>
#include "common/common_types.h"
#include "common/settings_setting.h"
namespace AndroidSettings {
struct Values {
Settings::Linkage linkage;
// Android
Settings::Setting<bool> picture_in_picture{linkage, true, "picture_in_picture",
Settings::Category::Android};
Settings::Setting<s32> screen_layout{linkage,
5,
"screen_layout",
Settings::Category::Android,
Settings::Specialization::Default,
true,
true};
};
extern Values values;
} // namespace AndroidSettings

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="125"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="1"
android:toAlpha="0" />
<translate
android:duration="125"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromXDelta="0"
android:toXDelta="-75" />
</set>

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="0"
android:toAlpha="1" />
<translate
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromXDelta="-200"
android:toXDelta="0" />
</set>

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="125"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="1"
android:toAlpha="0" />
<translate
android:duration="125"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromXDelta="0"
android:toXDelta="75" />
</set>

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="0"
android:toAlpha="1" />
<translate
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromXDelta="200"
android:toXDelta="0" />
</set>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="1"
android:toAlpha="0" />
</set>

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:propertyName="translationX"
android:valueType="floatType"
android:valueFrom="-1280dp"
android:valueTo="0"
android:interpolator="@android:interpolator/decelerate_quad"
android:duration="300"/>
<objectAnimator
android:propertyName="alpha"
android:valueType="floatType"
android:valueFrom="0"
android:valueTo="1"
android:interpolator="@android:interpolator/accelerate_quad"
android:duration="300"/>
</set>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- This animation is used ONLY when a submenu is replaced. -->
<objectAnimator
android:propertyName="translationX"
android:valueType="floatType"
android:valueFrom="0"
android:valueTo="-1280dp"
android:interpolator="@android:interpolator/decelerate_quad"
android:duration="200"/>
<objectAnimator
android:propertyName="alpha"
android:valueType="floatType"
android:valueFrom="1"
android:valueTo="0"
android:interpolator="@android:interpolator/decelerate_quad"
android:duration="200"/>
</set>

View File

@ -1,42 +1,24 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/coordinator_main"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraint_settings"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSurface"> android:background="?attr/colorSurface">
<com.google.android.material.appbar.AppBarLayout <androidx.fragment.app.FragmentContainerView
android:id="@+id/appbar_settings" android:id="@+id/fragment_container"
android:layout_width="match_parent" android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_height="wrap_content" android:layout_width="0dp"
android:fitsSystemWindows="true" android:layout_height="0dp"
app:elevation="0dp"> app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
<com.google.android.material.appbar.CollapsingToolbarLayout app:layout_constraintLeft_toLeftOf="parent"
style="?attr/collapsingToolbarLayoutMediumStyle" app:layout_constraintRight_toRightOf="parent"
android:id="@+id/toolbar_settings_layout" app:layout_constraintTop_toTopOf="parent"
android:layout_width="match_parent" tools:layout="@layout/fragment_settings" />
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_settings"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="@+id/frame_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="12dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<View <View
android:id="@+id/navigation_bar_shade" android:id="@+id/navigation_bar_shade"
@ -45,6 +27,8 @@
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:clickable="false" android:clickable="false"
android:focusable="false" android:focusable="false"
android:layout_gravity="bottom|center_horizontal" /> app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,14 +1,41 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator_main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:background="?attr/colorSurface">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:elevation="0dp">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/toolbar_settings_layout"
style="?attr/collapsingToolbarLayoutMediumStyle"
android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_settings"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_back" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_settings" android:id="@+id/list_settings"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSurface" android:clipToPadding="false"
android:clipToPadding="false" /> app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</FrameLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:id="@+id/relativeLayout"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider">
<LinearLayout
android:id="@+id/no_results_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/icon_no_results"
android:layout_width="match_parent"
android:layout_height="80dp"
android:src="@drawable/ic_search" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/notice_text"
style="@style/TextAppearance.Material3.TitleLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="8dp"
android:text="@string/search_settings"
tools:visibility="visible" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/settings_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />
</RelativeLayout>
<FrameLayout
android:id="@+id/frame_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.card.MaterialCardView
android:id="@+id/search_background"
style="?attr/materialCardViewFilledStyle"
android:layout_width="match_parent"
android:layout_height="56dp"
app:cardCornerRadius="28dp">
<LinearLayout
android:id="@+id/search_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="56dp"
android:orientation="horizontal">
<Button
android:id="@+id/back_button"
style="?attr/materialIconButtonFilledTonalStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
app:backgroundTint="@android:color/transparent"
app:icon="@drawable/ic_back" />
<EditText
android:id="@+id/search_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:hint="@string/search_settings"
android:imeOptions="flagNoFullscreen"
android:inputType="text"
android:maxLines="1" />
</LinearLayout>
<Button
android:id="@+id/clear_button"
style="?attr/materialIconButtonFilledTonalStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:layout_marginEnd="8dp"
android:visibility="invisible"
app:backgroundTint="@android:color/transparent"
app:icon="@drawable/ic_clear"
tools:visibility="visible" />
</com.google.android.material.card.MaterialCardView>
</FrameLayout>
<com.google.android.material.divider.MaterialDivider
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/frame_search" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,2 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu /> <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search"
android:title="@string/home_search"
app:showAsAction="always" />
</menu>

View File

@ -17,4 +17,21 @@
android:defaultValue="@null" /> android:defaultValue="@null" />
</fragment> </fragment>
<activity
android:id="@+id/settingsActivity"
android:name="org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity"
android:label="SettingsActivity">
<argument
android:name="game"
app:argType="org.yuzu.yuzu_emu.model.Game"
app:nullable="true" />
<argument
android:name="menuTag"
app:argType="string" />
</activity>
<action
android:id="@+id/action_global_settingsActivity"
app:destination="@id/settingsActivity" />
</navigation> </navigation>

View File

@ -72,4 +72,21 @@
app:destination="@id/emulationActivity" app:destination="@id/emulationActivity"
app:launchSingleTop="true" /> app:launchSingleTop="true" />
<activity
android:id="@+id/settingsActivity"
android:name="org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity"
android:label="SettingsActivity">
<argument
android:name="game"
app:argType="org.yuzu.yuzu_emu.model.Game"
app:nullable="true" />
<argument
android:name="menuTag"
app:argType="string" />
</activity>
<action
android:id="@+id/action_global_settingsActivity"
app:destination="@id/settingsActivity" />
</navigation> </navigation>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/settings_navigation"
app:startDestination="@id/settingsFragment">
<fragment
android:id="@+id/settingsFragment"
android:name="org.yuzu.yuzu_emu.features.settings.ui.SettingsFragment"
android:label="SettingsFragment">
<argument
android:name="menuTag"
app:argType="string" />
<argument
android:name="game"
app:argType="org.yuzu.yuzu_emu.model.Game"
app:nullable="true" />
<action
android:id="@+id/action_settingsFragment_to_settingsSearchFragment"
app:destination="@id/settingsSearchFragment" />
</fragment>
<action
android:id="@+id/action_global_settingsFragment"
app:destination="@id/settingsFragment" />
<fragment
android:id="@+id/settingsSearchFragment"
android:name="org.yuzu.yuzu_emu.fragments.SettingsSearchFragment"
android:label="SettingsSearchFragment" />
</navigation>

View File

@ -243,10 +243,10 @@
<item>@string/cubeb</item> <item>@string/cubeb</item>
<item>@string/string_null</item> <item>@string/string_null</item>
</string-array> </string-array>
<string-array name="outputEngineValues"> <integer-array name="outputEngineValues">
<item>auto</item> <item>0</item>
<item>cubeb</item> <item>1</item>
<item>null</item> <item>3</item>
</string-array> </integer-array>
</resources> </resources>

View File

@ -43,6 +43,7 @@
<string name="add_games_warning_description">Games won\'t be displayed in the Games list if a folder isn\'t selected.</string> <string name="add_games_warning_description">Games won\'t be displayed in the Games list if a folder isn\'t selected.</string>
<string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string> <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string>
<string name="home_search_games">Search games</string> <string name="home_search_games">Search games</string>
<string name="search_settings">Search settings</string>
<string name="games_dir_selected">Games directory selected</string> <string name="games_dir_selected">Games directory selected</string>
<string name="install_prod_keys">Install prod.keys</string> <string name="install_prod_keys">Install prod.keys</string>
<string name="install_prod_keys_description">Required to decrypt retail games</string> <string name="install_prod_keys_description">Required to decrypt retail games</string>
@ -74,6 +75,7 @@
<string name="install_gpu_driver">Install GPU driver</string> <string name="install_gpu_driver">Install GPU driver</string>
<string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string> <string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
<string name="advanced_settings">Advanced settings</string> <string name="advanced_settings">Advanced settings</string>
<string name="advanced_settings_game">Advanced settings: %1$s</string>
<string name="settings_description">Configure emulator settings</string> <string name="settings_description">Configure emulator settings</string>
<string name="search_recently_played">Recently played</string> <string name="search_recently_played">Recently played</string>
<string name="search_recently_added">Recently added</string> <string name="search_recently_added">Recently added</string>
@ -200,6 +202,7 @@
<string name="ini_saved">Saved settings</string> <string name="ini_saved">Saved settings</string>
<string name="gameid_saved">Saved settings for %1$s</string> <string name="gameid_saved">Saved settings for %1$s</string>
<string name="error_saving">Error saving %1$s.ini: %2$s</string> <string name="error_saving">Error saving %1$s.ini: %2$s</string>
<string name="unimplemented_menu">Unimplemented Menu</string>
<string name="loading">Loading…</string> <string name="loading">Loading…</string>
<string name="reset_setting_confirmation">Do you want to reset this setting back to its default value?</string> <string name="reset_setting_confirmation">Do you want to reset this setting back to its default value?</string>
<string name="reset_to_default">Reset to default</string> <string name="reset_to_default">Reset to default</string>

View File

@ -159,6 +159,8 @@ float Volume() {
const char* TranslateCategory(Category category) { const char* TranslateCategory(Category category) {
switch (category) { switch (category) {
case Category::Android:
return "Android";
case Category::Audio: case Category::Audio:
return "Audio"; return "Audio";
case Category::Core: case Category::Core:

View File

@ -14,6 +14,7 @@ BasicSetting::BasicSetting(Linkage& linkage, const std::string& name, enum Categ
: label{name}, category{category_}, id{linkage.count}, save{save_}, : label{name}, category{category_}, id{linkage.count}, save{save_},
runtime_modifiable{runtime_modifiable_}, specialization{specialization_}, runtime_modifiable{runtime_modifiable_}, specialization{specialization_},
other_setting{other_setting_} { other_setting{other_setting_} {
linkage.by_key.insert({name, this});
linkage.by_category[category].push_back(this); linkage.by_category[category].push_back(this);
linkage.count++; linkage.count++;
} }

View File

@ -12,6 +12,7 @@
namespace Settings { namespace Settings {
enum class Category : u32 { enum class Category : u32 {
Android,
Audio, Audio,
Core, Core,
Cpu, Cpu,
@ -68,6 +69,7 @@ public:
explicit Linkage(u32 initial_count = 0); explicit Linkage(u32 initial_count = 0);
~Linkage(); ~Linkage();
std::map<Category, std::vector<BasicSetting*>> by_category{}; std::map<Category, std::vector<BasicSetting*>> by_category{};
std::map<std::string, Settings::BasicSetting*> by_key{};
std::vector<std::function<void()>> restore_functions{}; std::vector<std::function<void()>> restore_functions{};
u32 count; u32 count;
}; };