diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index 9c32e044c..5a7cf4ed7 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt @@ -219,10 +219,6 @@ object NativeLibrary { 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?) /** @@ -413,14 +409,17 @@ object NativeLibrary { details.ifEmpty { emulationActivity.getString(R.string.system_archive_general) } ) } + CoreError.ErrorSavestate -> { title = emulationActivity.getString(R.string.save_load_error) message = details } + CoreError.ErrorUnknown -> { title = emulationActivity.getString(R.string.fatal_error) message = emulationActivity.getString(R.string.fatal_error_message) } + else -> { return true } @@ -454,6 +453,7 @@ object NativeLibrary { captionId = R.string.loader_error_video_core descriptionId = R.string.loader_error_video_core_description } + else -> { captionId = R.string.loader_error_encrypted descriptionId = R.string.loader_error_encrypted_roms_description diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt index 04ab6a220..9561748cb 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt @@ -46,7 +46,7 @@ class YuzuApplication : Application() { super.onCreate() application = this documentsTree = DocumentsTree() - DirectoryInitialization.start(applicationContext) + DirectoryInitialization.start() GpuDriverHelper.initializeDriverParameters(applicationContext) NativeLibrary.logDeviceInfo() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt index 7461fb093..dbd602a1d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt @@ -28,7 +28,6 @@ import android.view.Surface import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.Toast -import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat 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.IntSetting 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.utils.ControllerMappingHelper import org.yuzu.yuzu_emu.utils.ForegroundService @@ -72,8 +70,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { private val actionMute = "ACTION_EMULATOR_MUTE" private val actionUnmute = "ACTION_EMULATOR_UNMUTE" - private val settingsViewModel: SettingsViewModel by viewModels() - override fun onDestroy() { stopForegroundService(this) super.onDestroy() @@ -82,8 +78,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { override fun onCreate(savedInstanceState: Bundle?) { ThemeHelper.setTheme(this) - settingsViewModel.settings.loadSettings() - super.onCreate(savedInstanceState) binding = ActivityEmulationBinding.inflate(layoutInflater) @@ -91,9 +85,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment - val navController = navHostFragment.navController - navController - .setGraph(R.navigation.emulation_navigation, intent.extras) + navHostFragment.navController.setGraph(R.navigation.emulation_navigation, intent.extras) isActivityRecreated = savedInstanceState != null diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt index a6e9833ee..aeda8d222 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt @@ -4,5 +4,7 @@ package org.yuzu.yuzu_emu.features.settings.model interface AbstractBooleanSetting : AbstractSetting { - var boolean: Boolean + val boolean: Boolean + + fun setBoolean(value: Boolean) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt similarity index 59% rename from src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingsViewModel.kt rename to src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt index bd9233d62..606519ad8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingsViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt @@ -3,8 +3,8 @@ package org.yuzu.yuzu_emu.features.settings.model -import androidx.lifecycle.ViewModel +interface AbstractByteSetting : AbstractSetting { + val byte: Byte -class SettingsViewModel : ViewModel() { - val settings = Settings() + fun setByte(value: Byte) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt index 6fe4bc263..974925eed 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt @@ -4,5 +4,7 @@ package org.yuzu.yuzu_emu.features.settings.model interface AbstractFloatSetting : AbstractSetting { - var float: Float + val float: Float + + fun setFloat(value: Float) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt index 892b7dcfe..89b285b10 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt @@ -4,5 +4,7 @@ package org.yuzu.yuzu_emu.features.settings.model interface AbstractIntSetting : AbstractSetting { - var int: Int + val int: Int + + fun setInt(value: Int) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt new file mode 100644 index 000000000..4873942db --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt @@ -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) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt index 258580209..8b6d29fe5 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt @@ -3,10 +3,22 @@ package org.yuzu.yuzu_emu.features.settings.model +import org.yuzu.yuzu_emu.utils.NativeConfig + interface AbstractSetting { - val key: String? - val section: String? - val isRuntimeEditable: Boolean - val valueAsString: String + val key: String + val category: Settings.Category 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() } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt new file mode 100644 index 000000000..91407ccbb --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt @@ -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) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt index 0d02c5997..c8935cc48 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt @@ -4,5 +4,7 @@ package org.yuzu.yuzu_emu.features.settings.model interface AbstractStringSetting : AbstractSetting { - var string: String + val string: String + + fun setString(value: String) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt index d41933766..e0c0538c7 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt @@ -3,41 +3,37 @@ package org.yuzu.yuzu_emu.features.settings.model +import org.yuzu.yuzu_emu.utils.NativeConfig + enum class BooleanSetting( override val key: String, - override val section: String, - override val defaultValue: Boolean + override val category: Settings.Category, + override val androidDefault: Boolean? = null ) : AbstractBooleanSetting { - CPU_DEBUG_MODE("cpu_debug_mode", Settings.SECTION_CPU, false), - FASTMEM("cpuopt_fastmem", Settings.SECTION_CPU, true), - FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives", Settings.SECTION_CPU, true), - PICTURE_IN_PICTURE("picture_in_picture", Settings.SECTION_GENERAL, true), - USE_CUSTOM_RTC("custom_rtc_enabled", Settings.SECTION_SYSTEM, false); + CPU_DEBUG_MODE("cpu_debug_mode", Settings.Category.Cpu), + FASTMEM("cpuopt_fastmem", Settings.Category.Cpu), + FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives", Settings.Category.Cpu), + RENDERER_USE_SPEED_LIMIT("use_speed_limit", Settings.Category.Core), + 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 - get() = boolean.toString() + get() = if (boolean) "1" else "0" - override val isRuntimeEditable: Boolean - get() { - for (setting in NOT_RUNTIME_EDITABLE) { - if (setting == this) { - return false - } - } - return true - } - - companion object { - private val NOT_RUNTIME_EDITABLE = listOf( - 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 } - } + override fun reset() = NativeConfig.setBoolean(key, defaultValue) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt new file mode 100644 index 000000000..6ec0a765e --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt @@ -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) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt index e5545a916..0181d06f2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt @@ -3,34 +3,24 @@ package org.yuzu.yuzu_emu.features.settings.model +import org.yuzu.yuzu_emu.utils.NativeConfig + enum class FloatSetting( override val key: String, - override val section: String, - override val defaultValue: Float + override val category: Settings.Category ) : AbstractFloatSetting { // 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 get() = float.toString() - override val isRuntimeEditable: Boolean - get() { - for (setting in NOT_RUNTIME_EDITABLE) { - if (setting == this) { - return false - } - } - return true - } - - companion object { - private val NOT_RUNTIME_EDITABLE = emptyList() - - fun from(key: String): FloatSetting? = FloatSetting.values().firstOrNull { it.key == key } - - fun clear() = FloatSetting.values().forEach { it.float = it.defaultValue } - } + override fun reset() = NativeConfig.setFloat(key, defaultValue) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt index 4427a7d9d..151362124 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt @@ -3,139 +3,37 @@ package org.yuzu.yuzu_emu.features.settings.model +import org.yuzu.yuzu_emu.utils.NativeConfig + enum class IntSetting( override val key: String, - override val section: String, - override val defaultValue: Int + override val category: Settings.Category, + override val androidDefault: Int? = null ) : AbstractIntSetting { - RENDERER_USE_SPEED_LIMIT( - "use_speed_limit", - Settings.SECTION_RENDERER, - 1 - ), - USE_DOCKED_MODE( - "use_docked_mode", - Settings.SECTION_SYSTEM, - 0 - ), - RENDERER_USE_DISK_SHADER_CACHE( - "use_disk_shader_cache", - 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 - ); + CPU_ACCURACY("cpu_accuracy", Settings.Category.Cpu), + REGION_INDEX("region_index", Settings.Category.System), + LANGUAGE_INDEX("language_index", Settings.Category.System), + RENDERER_BACKEND("backend", Settings.Category.Renderer), + RENDERER_ACCURACY("gpu_accuracy", Settings.Category.Renderer, 0), + RENDERER_RESOLUTION("resolution_setup", Settings.Category.Renderer), + RENDERER_VSYNC("use_vsync", Settings.Category.Renderer), + RENDERER_SCALING_FILTER("scaling_filter", Settings.Category.Renderer), + RENDERER_ANTI_ALIASING("anti_aliasing", Settings.Category.Renderer), + RENDERER_SCREEN_LAYOUT("screen_layout", Settings.Category.Android), + RENDERER_ASPECT_RATIO("aspect_ratio", Settings.Category.Renderer), + AUDIO_OUTPUT_ENGINE("output_engine", Settings.Category.Audio); - 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 get() = int.toString() - override val isRuntimeEditable: Boolean - get() { - for (setting in NOT_RUNTIME_EDITABLE) { - if (setting == this) { - return false - } - } - return true - } - - companion object { - private val NOT_RUNTIME_EDITABLE = listOf( - 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 } - } + override fun reset() = NativeConfig.setInt(key, defaultValue) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt new file mode 100644 index 000000000..c526fc4cf --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt @@ -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) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingSection.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingSection.kt deleted file mode 100644 index 474f598a9..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/SettingSection.kt +++ /dev/null @@ -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() - - /** - * 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) - } - } -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt index a6251bafd..0702236e8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt @@ -4,195 +4,151 @@ package org.yuzu.yuzu_emu.features.settings.model import android.text.TextUtils -import java.util.* +import android.widget.Toast import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication -import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivityView import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile -class Settings { - private var gameId: String? = null +object Settings { + private val context get() = YuzuApplication.appContext - var isLoaded = false - - /** - * A HashMap, SettingSection> that constructs a new SettingSection instead of returning null - * when getting a key not already in the map - */ - class SettingsSectionMap : HashMap() { - 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 = 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) { - 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) { + fun saveSettings(gameId: String = "") { if (TextUtils.isEmpty(gameId)) { - view.showToastMessage( - YuzuApplication.appContext.getString(R.string.ini_saved), - false - ) - - for ((fileName, sectionNames) in configFileSectionsMap) { - val iniSections = TreeMap() - for (section in sectionNames) { - iniSections[section] = sections[section]!! - } - - SettingsFile.saveFile(fileName, iniSections, view) - } + Toast.makeText( + context, + context.getString(R.string.ini_saved), + Toast.LENGTH_SHORT + ).show() + SettingsFile.saveFile(SettingsFile.FILE_NAME_CONFIG) } else { - // Custom game settings - view.showToastMessage( - YuzuApplication.appContext.getString(R.string.gameid_saved, gameId), - false - ) - - SettingsFile.saveCustomGameSettings(gameId, sections) + // TODO: Save custom game settings + Toast.makeText( + context, + context.getString(R.string.gameid_saved, gameId), + Toast.LENGTH_SHORT + ).show() } } - companion object { - const val SECTION_GENERAL = "General" - const val SECTION_SYSTEM = "System" - const val SECTION_RENDERER = "Renderer" - const val SECTION_AUDIO = "Audio" - const val SECTION_CPU = "Cpu" - const val SECTION_THEME = "Theme" - const val SECTION_DEBUG = "Debug" - - const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown" - - const val PREF_OVERLAY_VERSION = "OverlayVersion" - const val PREF_LANDSCAPE_OVERLAY_VERSION = "LandscapeOverlayVersion" - const val PREF_PORTRAIT_OVERLAY_VERSION = "PortraitOverlayVersion" - const val PREF_FOLDABLE_OVERLAY_VERSION = "FoldableOverlayVersion" - val overlayLayoutPrefs = listOf( - PREF_LANDSCAPE_OVERLAY_VERSION, - PREF_PORTRAIT_OVERLAY_VERSION, - PREF_FOLDABLE_OVERLAY_VERSION - ) - - const val PREF_CONTROL_SCALE = "controlScale" - const val PREF_CONTROL_OPACITY = "controlOpacity" - const val PREF_TOUCH_ENABLED = "isTouchEnabled" - const val PREF_BUTTON_A = "buttonToggle0" - const val PREF_BUTTON_B = "buttonToggle1" - const val PREF_BUTTON_X = "buttonToggle2" - const val PREF_BUTTON_Y = "buttonToggle3" - const val PREF_BUTTON_L = "buttonToggle4" - const val PREF_BUTTON_R = "buttonToggle5" - const val PREF_BUTTON_ZL = "buttonToggle6" - const val PREF_BUTTON_ZR = "buttonToggle7" - const val PREF_BUTTON_PLUS = "buttonToggle8" - const val PREF_BUTTON_MINUS = "buttonToggle9" - const val PREF_BUTTON_DPAD = "buttonToggle10" - const val PREF_STICK_L = "buttonToggle11" - const val PREF_STICK_R = "buttonToggle12" - const val PREF_BUTTON_STICK_L = "buttonToggle13" - const val PREF_BUTTON_STICK_R = "buttonToggle14" - const val PREF_BUTTON_HOME = "buttonToggle15" - const val PREF_BUTTON_SCREENSHOT = "buttonToggle16" - - const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter" - const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable" - const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics" - const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps" - const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay" - - const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch" - const val PREF_THEME = "Theme" - const val PREF_THEME_MODE = "ThemeMode" - const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds" - - private val configFileSectionsMap: MutableMap> = HashMap() - - val overlayPreferences = listOf( - PREF_OVERLAY_VERSION, - PREF_CONTROL_SCALE, - PREF_CONTROL_OPACITY, - PREF_TOUCH_ENABLED, - PREF_BUTTON_A, - PREF_BUTTON_B, - PREF_BUTTON_X, - PREF_BUTTON_Y, - PREF_BUTTON_L, - PREF_BUTTON_R, - PREF_BUTTON_ZL, - PREF_BUTTON_ZR, - PREF_BUTTON_PLUS, - PREF_BUTTON_MINUS, - PREF_BUTTON_DPAD, - PREF_STICK_L, - PREF_STICK_R, - PREF_BUTTON_HOME, - PREF_BUTTON_SCREENSHOT, - PREF_BUTTON_STICK_L, - PREF_BUTTON_STICK_R - ) - - const val LayoutOption_Unspecified = 0 - const val LayoutOption_MobilePortrait = 4 - const val LayoutOption_MobileLandscape = 5 - - init { - configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] = - listOf( - SECTION_GENERAL, - SECTION_SYSTEM, - SECTION_RENDERER, - SECTION_AUDIO, - SECTION_CPU - ) - } + 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( + *BooleanSetting.values(), + *ByteSetting.values(), + *ShortSetting.values(), + *IntSetting.values(), + *FloatSetting.values(), + *LongSetting.values(), + *StringSetting.values() + ) + + const val SECTION_GENERAL = "General" + const val SECTION_SYSTEM = "System" + const val SECTION_RENDERER = "Renderer" + const val SECTION_AUDIO = "Audio" + const val SECTION_CPU = "Cpu" + const val SECTION_THEME = "Theme" + const val SECTION_DEBUG = "Debug" + + const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown" + + const val PREF_OVERLAY_VERSION = "OverlayVersion" + const val PREF_LANDSCAPE_OVERLAY_VERSION = "LandscapeOverlayVersion" + const val PREF_PORTRAIT_OVERLAY_VERSION = "PortraitOverlayVersion" + const val PREF_FOLDABLE_OVERLAY_VERSION = "FoldableOverlayVersion" + val overlayLayoutPrefs = listOf( + PREF_LANDSCAPE_OVERLAY_VERSION, + PREF_PORTRAIT_OVERLAY_VERSION, + PREF_FOLDABLE_OVERLAY_VERSION + ) + + const val PREF_CONTROL_SCALE = "controlScale" + const val PREF_CONTROL_OPACITY = "controlOpacity" + const val PREF_TOUCH_ENABLED = "isTouchEnabled" + const val PREF_BUTTON_A = "buttonToggle0" + const val PREF_BUTTON_B = "buttonToggle1" + const val PREF_BUTTON_X = "buttonToggle2" + const val PREF_BUTTON_Y = "buttonToggle3" + const val PREF_BUTTON_L = "buttonToggle4" + const val PREF_BUTTON_R = "buttonToggle5" + const val PREF_BUTTON_ZL = "buttonToggle6" + const val PREF_BUTTON_ZR = "buttonToggle7" + const val PREF_BUTTON_PLUS = "buttonToggle8" + const val PREF_BUTTON_MINUS = "buttonToggle9" + const val PREF_BUTTON_DPAD = "buttonToggle10" + const val PREF_STICK_L = "buttonToggle11" + const val PREF_STICK_R = "buttonToggle12" + const val PREF_BUTTON_STICK_L = "buttonToggle13" + const val PREF_BUTTON_STICK_R = "buttonToggle14" + const val PREF_BUTTON_HOME = "buttonToggle15" + const val PREF_BUTTON_SCREENSHOT = "buttonToggle16" + + const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter" + const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable" + const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics" + const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps" + const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay" + + const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch" + const val PREF_THEME = "Theme" + const val PREF_THEME_MODE = "ThemeMode" + const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds" + + val overlayPreferences = listOf( + PREF_OVERLAY_VERSION, + PREF_CONTROL_SCALE, + PREF_CONTROL_OPACITY, + PREF_TOUCH_ENABLED, + PREF_BUTTON_A, + PREF_BUTTON_B, + PREF_BUTTON_X, + PREF_BUTTON_Y, + PREF_BUTTON_L, + PREF_BUTTON_R, + PREF_BUTTON_ZL, + PREF_BUTTON_ZR, + PREF_BUTTON_PLUS, + PREF_BUTTON_MINUS, + PREF_BUTTON_DPAD, + PREF_STICK_L, + PREF_STICK_R, + PREF_BUTTON_HOME, + PREF_BUTTON_SCREENSHOT, + PREF_BUTTON_STICK_L, + PREF_BUTTON_STICK_R + ) + + const val LayoutOption_Unspecified = 0 + const val LayoutOption_MobilePortrait = 4 + const val LayoutOption_MobileLandscape = 5 } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt new file mode 100644 index 000000000..c9a0c664c --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt @@ -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) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt index 6621289fd..9bb3e66d4 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt @@ -3,36 +3,24 @@ package org.yuzu.yuzu_emu.features.settings.model +import org.yuzu.yuzu_emu.utils.NativeConfig + enum class StringSetting( override val key: String, - override val section: String, - override val defaultValue: String + override val category: Settings.Category ) : AbstractStringSetting { - AUDIO_OUTPUT_ENGINE("output_engine", Settings.SECTION_AUDIO, "auto"), - CUSTOM_RTC("custom_rtc", Settings.SECTION_SYSTEM, "0"); + // No string settings currently exist + 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 get() = string - override val isRuntimeEditable: Boolean - get() { - for (setting in NOT_RUNTIME_EDITABLE) { - if (setting == this) { - return false - } - } - return true - } - - companion object { - private val NOT_RUNTIME_EDITABLE = listOf( - CUSTOM_RTC - ) - - fun from(key: String): StringSetting? = StringSetting.values().firstOrNull { it.key == key } - - fun clear() = StringSetting.values().forEach { it.string = it.defaultValue } - } + override fun reset() = NativeConfig.setString(key, defaultValue) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt index bc0bf7788..8bc164197 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt @@ -3,29 +3,16 @@ 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.AbstractLongSetting class DateTimeSetting( - setting: AbstractSetting?, + private val longSetting: AbstractLongSetting, titleId: Int, - descriptionId: Int, - val key: String? = null, - private val defaultValue: String? = null -) : SettingsItem(setting, titleId, descriptionId) { + descriptionId: Int +) : SettingsItem(longSetting, titleId, descriptionId) { override val type = TYPE_DATETIME_SETTING - val value: String - get() = if (setting != null) { - val setting = setting as AbstractStringSetting - setting.string - } else { - defaultValue!! - } - - fun setSelectedValue(datetime: String): AbstractStringSetting { - val stringSetting = setting as AbstractStringSetting - stringSetting.string = datetime - return stringSetting - } + var value: Long + get() = longSetting.long + set(value) = (setting as AbstractLongSetting).setLong(value) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt index a67001311..d31ce1c31 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/HeaderSetting.kt @@ -5,6 +5,6 @@ package org.yuzu.yuzu_emu.features.settings.model.view class HeaderSetting( titleId: Int -) : SettingsItem(null, titleId, 0) { +) : SettingsItem(emptySetting, titleId, 0) { override val type = TYPE_HEADER } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt index caaab50d8..522cc49df 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/RunnableSetting.kt @@ -8,6 +8,6 @@ class RunnableSetting( descriptionId: Int, val isRuntimeRunnable: Boolean, val runnable: () -> Unit -) : SettingsItem(null, titleId, descriptionId) { +) : SettingsItem(emptySetting, titleId, descriptionId) { override val type = TYPE_RUNNABLE } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt index 07520849e..b3b3fc209 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt @@ -4,7 +4,15 @@ package org.yuzu.yuzu_emu.features.settings.model.view 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.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. @@ -14,7 +22,7 @@ import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting * file.) */ abstract class SettingsItem( - var setting: AbstractSetting?, + val setting: AbstractSetting, val nameId: Int, val descriptionId: Int ) { @@ -23,7 +31,7 @@ abstract class SettingsItem( val isEditable: Boolean get() { if (!NativeLibrary.isRunning()) return true - return setting?.isRuntimeEditable ?: false + return setting.isRuntimeModifiable } companion object { @@ -35,5 +43,240 @@ abstract class SettingsItem( const val TYPE_STRING_SINGLE_CHOICE = 5 const val TYPE_DATETIME_SETTING = 6 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.put(item: SettingsItem) { + put(item.setting.key, item) + } + + // List of all general + val settingsItems = HashMap().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)) + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt index 7306ec458..705527a73 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt @@ -4,36 +4,27 @@ 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.AbstractSetting class SingleChoiceSetting( - setting: AbstractIntSetting?, + setting: AbstractSetting, titleId: Int, descriptionId: Int, val choicesId: Int, - val valuesId: Int, - val key: String? = null, - val defaultValue: Int? = null + val valuesId: Int ) : SettingsItem(setting, titleId, descriptionId) { override val type = TYPE_SINGLE_CHOICE - val selectedValue: Int - get() = if (setting != null) { - val setting = setting as AbstractIntSetting - setting.int - } else { - defaultValue!! + var selectedValue: Int + get() { + return when (setting) { + is AbstractIntSetting -> setting.int + else -> -1 + } + } + set(value) { + when (setting) { + is AbstractIntSetting -> setting.setInt(value) + } } - - /** - * 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 - } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt index 92d0167ae..c3b5df02c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt @@ -3,60 +3,39 @@ 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.AbstractIntSetting 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( - setting: AbstractSetting?, + setting: AbstractSetting, titleId: Int, descriptionId: Int, val min: Int, val max: Int, - val units: String, - val key: String? = null, - val defaultValue: Int? = null + val units: String ) : SettingsItem(setting, titleId, descriptionId) { override val type = TYPE_SLIDER - val selectedValue: Int + var selectedValue: Int get() { - val setting = setting ?: return defaultValue!! return when (setting) { + is AbstractByteSetting -> setting.byte.toInt() + is AbstractShortSetting -> setting.short.toInt() is AbstractIntSetting -> setting.int is AbstractFloatSetting -> setting.float.roundToInt() - else -> { - Log.error("[SliderSetting] Error casting setting type.") - -1 - } + else -> -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 - } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt index 3b6731dcd..871dab4f3 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt @@ -3,57 +3,31 @@ 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 class StringSingleChoiceSetting( - setting: AbstractSetting?, + private val stringSetting: AbstractStringSetting, titleId: Int, descriptionId: Int, val choices: Array, - val values: Array?, - val key: String? = null, - private val defaultValue: String? = null -) : SettingsItem(setting, titleId, descriptionId) { + val values: Array +) : SettingsItem(stringSetting, titleId, descriptionId) { override val type = TYPE_STRING_SINGLE_CHOICE - fun getValueAt(index: Int): String? { - if (values == null) return null - return if (index >= 0 && index < values.size) { - values[index] - } else { - "" - } - } + fun getValueAt(index: Int): String = + if (index >= 0 && index < values.size) values[index] else "" + + var selectedValue: String + 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 get() { - val selectedValue = selectedValue - for (i in values!!.indices) { + for (i in values.indices) { if (values[i] == selectedValue) { return i } } return -1 } - - /** - * Write a value to the backing int. If that int was previously null, - * initializes a new one and returns it, so it can be added to the Hashmap. - * - * @param selection New value of the int. - * @return the existing setting with the new value applied. - */ - fun setSelectedValue(selection: String): AbstractStringSetting { - val stringSetting = setting as AbstractStringSetting - stringSetting.string = selection - return stringSetting - } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt index 8a9d13a92..91c273964 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt @@ -7,6 +7,6 @@ class SubmenuSetting( titleId: Int, descriptionId: Int, val menuKey: String -) : SettingsItem(null, titleId, descriptionId) { +) : SettingsItem(emptySetting, titleId, descriptionId) { override val type = TYPE_SUBMENU } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt index 90b198718..416967e64 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt @@ -10,53 +10,22 @@ import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting class SwitchSetting( setting: AbstractSetting, titleId: Int, - descriptionId: Int, - val key: String? = null, - val defaultValue: Any? = null + descriptionId: Int ) : SettingsItem(setting, titleId, descriptionId) { override val type = TYPE_SWITCH - val isChecked: Boolean + var checked: Boolean get() { - if (setting == null) { - return defaultValue as Boolean + return when (setting) { + is AbstractIntSetting -> setting.int == 1 + is AbstractBooleanSetting -> setting.boolean + else -> false } - - // Try integer setting - try { - val setting = setting as AbstractIntSetting - return setting.int == 1 - } catch (_: ClassCastException) { - } - - // Try boolean setting - try { - val setting = setting as AbstractBooleanSetting - return setting.boolean - } catch (_: ClassCastException) { - } - return defaultValue as Boolean } - - /** - * Write a value to the backing boolean. If that boolean was previously null, - * initializes a new one and returns it, so it can be added to the Hashmap. - * - * @param checked Pretty self explanatory. - * @return the existing setting with the new value applied. - */ - fun setChecked(checked: Boolean): AbstractSetting { - // Try integer setting - try { - val setting = setting as AbstractIntSetting - setting.int = if (checked) 1 else 0 - return setting - } catch (_: ClassCastException) { + set(value) { + when (setting) { + is AbstractIntSetting -> setting.setInt(if (value) 1 else 0) + is AbstractBooleanSetting -> setting.setBoolean(value) + } } - - // Try boolean setting - val setting = setting as AbstractBooleanSetting - setting.boolean = checked - return setting - } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt index e6fffc832..908c01265 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt @@ -3,10 +3,7 @@ package org.yuzu.yuzu_emu.features.settings.ui -import android.content.Context -import android.content.Intent import android.os.Bundle -import android.view.Menu import android.view.View import android.view.ViewGroup.MarginLayoutParams import android.widget.Toast @@ -16,28 +13,24 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updatePadding +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.navArgs import com.google.android.material.color.MaterialColors import java.io.IOException import org.yuzu.yuzu_emu.R 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.SettingsViewModel -import org.yuzu.yuzu_emu.features.settings.model.StringSetting 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.* -class SettingsActivity : AppCompatActivity(), SettingsActivityView { - private val presenter = SettingsActivityPresenter(this) - +class SettingsActivity : AppCompatActivity() { private lateinit var binding: ActivitySettingsBinding - private val settingsViewModel: SettingsViewModel by viewModels() + private val args by navArgs() - override val settings: Settings get() = settingsViewModel.settings + private val settingsViewModel: SettingsViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { ThemeHelper.setTheme(this) @@ -47,16 +40,17 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView { binding = ActivitySettingsBinding.inflate(layoutInflater) 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) - val launcher = intent - val gameID = launcher.getStringExtra(ARG_GAME_ID) - val menuTag = launcher.getStringExtra(ARG_MENU_TAG) - presenter.onCreate(savedInstanceState, menuTag!!, gameID!!) - - // Show "Back" button in the action bar for navigation - setSupportActionBar(binding.toolbarSettings) - supportActionBar!!.setDisplayHomeAsUpEnabled(true) + if (savedInstanceState != null) { + settingsViewModel.shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE) + } if (InsetsHelper.getSystemGestureType(applicationContext) != 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( this, object : OnBackPressedCallback(true) { @@ -82,34 +98,28 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView { setInsets() } - override fun onSupportNavigateUp(): Boolean { - navigateBack() - return true - } - - private fun navigateBack() { - if (supportFragmentManager.backStackEntryCount > 0) { - supportFragmentManager.popBackStack() + fun navigateBack() { + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment + if (navHostFragment.childFragmentManager.backStackEntryCount > 0) { + navHostFragment.navController.popBackStack() } else { finish() } } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - val inflater = menuInflater - inflater.inflate(R.menu.menu_settings, menu) - return true - } - override fun onSaveInstanceState(outState: Bundle) { // Critical: If super method is not called, rotations will be busted. super.onSaveInstanceState(outState) - presenter.saveState(outState) + outState.putBoolean(KEY_SHOULD_SAVE, settingsViewModel.shouldSave) } override fun 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() { super.onStop() - presenter.onStop(isFinishing) - } - - override fun showSettingsFragment(menuTag: String, addToStack: Boolean, gameId: String) { - if (!addToStack && settingsFragment != null) { - return + if (isFinishing && settingsViewModel.shouldSave) { + Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...") + Settings.saveSettings() } - - 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() + override fun onDestroy() { + settingsViewModel.clear() + super.onDestroy() } fun onSettingsReset() { // Prevents saving to a non-existent settings file - presenter.onSettingsReset() - - // Reset the static memory representation of each setting - BooleanSetting.clear() - FloatSetting.clear() - IntSetting.clear() - StringSetting.clear() + settingsViewModel.shouldSave = false // Delete settings file because the user may have changed values that do not exist in the UI val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG) if (!settingsFile.delete()) { throw IOException("Failed to delete $settingsFile") } + 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() } - fun setToolbarTitle(title: String) { - binding.toolbarSettingsLayout.title = title - } - - private val settingsFragment: SettingsFragment? - get() = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as SettingsFragment? - private fun setInsets() { ViewCompat.setOnApplyWindowInsetsListener( - binding.frameContent + binding.navigationBarShade ) { view: View, windowInsets: WindowInsetsCompat -> val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) - val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) - view.updatePadding( - left = barInsets.left + cutoutInsets.left, - right = barInsets.right + cutoutInsets.right - ) - val mlpAppBar = binding.appbarSettings.layoutParams as MarginLayoutParams - mlpAppBar.leftMargin = barInsets.left + cutoutInsets.left - mlpAppBar.rightMargin = barInsets.right + cutoutInsets.right - binding.appbarSettings.layoutParams = mlpAppBar - - val mlpShade = binding.navigationBarShade.layoutParams as MarginLayoutParams + val mlpShade = view.layoutParams as MarginLayoutParams mlpShade.height = barInsets.bottom - binding.navigationBarShade.layoutParams = mlpShade + view.layoutParams = mlpShade windowInsets } } companion object { - private const val ARG_MENU_TAG = "menu_tag" - private const val ARG_GAME_ID = "game_id" - private const val FRAGMENT_TAG = "settings" - - 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) - } + private const val KEY_SHOULD_SAVE = "should_save" } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.kt deleted file mode 100644 index 93e677b21..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.kt +++ /dev/null @@ -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" - } -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.kt deleted file mode 100644 index c186fc388..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.kt +++ /dev/null @@ -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() -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt index 9711e2c51..a7a029fc1 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt @@ -4,51 +4,54 @@ package org.yuzu.yuzu_emu.features.settings.ui import android.content.Context -import android.content.DialogInterface import android.icu.util.Calendar import android.icu.util.TimeZone import android.text.format.DateFormat import android.view.LayoutInflater import android.view.ViewGroup -import android.widget.TextView -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity -import androidx.recyclerview.widget.RecyclerView +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModelProvider +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.dialog.MaterialAlertDialogBuilder -import com.google.android.material.slider.Slider import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.TimeFormat +import kotlinx.coroutines.launch 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.ListItemSettingSwitchBinding 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.ui.viewholder.* +import org.yuzu.yuzu_emu.fragments.SettingsDialogFragment +import org.yuzu.yuzu_emu.model.SettingsViewModel class SettingsAdapter( - private val fragmentView: SettingsFragmentView, + private val fragment: Fragment, private val context: Context -) : RecyclerView.Adapter(), DialogInterface.OnClickListener { - private var settings: ArrayList? = null - private var clickedItem: SettingsItem? = null - private var clickedPosition: Int - private var dialog: AlertDialog? = null - private var sliderProgress = 0 - private var textSliderValue: TextView? = null - - private var defaultCancelListener = - DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> closeDialog() } +) : ListAdapter( + AsyncDifferConfig.Builder(DiffCallback()).build() +) { + private val settingsViewModel: SettingsViewModel + get() = ViewModelProvider(fragment.requireActivity())[SettingsViewModel::class.java] 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 { @@ -90,67 +93,41 @@ class SettingsAdapter( } override fun onBindViewHolder(holder: SettingViewHolder, position: Int) { - holder.bind(getItem(position)) + holder.bind(currentList[position]) } - private fun getItem(position: Int): SettingsItem { - return settings!![position] - } - - override fun getItemCount(): Int { - return if (settings != null) { - settings!!.size - } else { - 0 - } - } + override fun getItemCount(): Int = currentList.size override fun getItemViewType(position: Int): Int { - return getItem(position).type + return currentList[position].type } - fun setSettingsList(settings: ArrayList?) { - this.settings = settings - notifyDataSetChanged() - } - - fun onBooleanClick(item: SwitchSetting, position: Int, checked: Boolean) { - val setting = item.setChecked(checked) - fragmentView.putSetting(setting) - fragmentView.onSettingChanged() - } - - private fun onSingleChoiceClick(item: SingleChoiceSetting) { - clickedItem = item - val value = getSelectionForSingleChoiceValue(item) - dialog = MaterialAlertDialogBuilder(context) - .setTitle(item.nameId) - .setSingleChoiceItems(item.choicesId, value, this) - .show() + fun onBooleanClick(item: SwitchSetting, checked: Boolean) { + item.checked = checked + settingsViewModel.setShouldReloadSettingsList(true) + settingsViewModel.shouldSave = true } fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) { - clickedPosition = position - onSingleChoiceClick(item) - } - - private fun onStringSingleChoiceClick(item: StringSingleChoiceSetting) { - clickedItem = item - dialog = MaterialAlertDialogBuilder(context) - .setTitle(item.nameId) - .setSingleChoiceItems(item.choices, item.selectValueIndex, this) - .show() + SettingsDialogFragment.newInstance( + settingsViewModel, + item, + SettingsItem.TYPE_SINGLE_CHOICE, + position + ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) } fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) { - clickedPosition = position - onStringSingleChoiceClick(item) + SettingsDialogFragment.newInstance( + settingsViewModel, + item, + SettingsItem.TYPE_STRING_SINGLE_CHOICE, + position + ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) } fun onDateTimeClick(item: DateTimeSetting, position: Int) { - clickedItem = item - clickedPosition = position - val storedTime = java.lang.Long.decode(item.value) * 1000 + val storedTime = item.value * 1000 // Helper to extract hour and minute from epoch time val calendar: Calendar = Calendar.getInstance() @@ -158,7 +135,7 @@ class SettingsAdapter( calendar.timeZone = TimeZone.getTimeZone("UTC") var timeFormat: Int = TimeFormat.CLOCK_12H - if (DateFormat.is24HourFormat(fragmentView.activityView as AppCompatActivity)) { + if (DateFormat.is24HourFormat(context)) { timeFormat = TimeFormat.CLOCK_24H } @@ -175,7 +152,7 @@ class SettingsAdapter( datePicker.addOnPositiveButtonClickListener { timePicker.show( - (fragmentView.activityView as AppCompatActivity).supportFragmentManager, + fragment.childFragmentManager, "TimePicker" ) } @@ -183,160 +160,50 @@ class SettingsAdapter( var epochTime: Long = datePicker.selection!! / 1000 epochTime += timePicker.hour.toLong() * 60 * 60 epochTime += timePicker.minute.toLong() * 60 - val rtcString = epochTime.toString() - if (item.value != rtcString) { - fragmentView.onSettingChanged() + if (item.value != epochTime) { + settingsViewModel.shouldSave = true + notifyItemChanged(position) + item.value = epochTime } - notifyItemChanged(clickedPosition) - val setting = item.setSelectedValue(rtcString) - fragmentView.putSetting(setting) - clickedItem = null } datePicker.show( - (fragmentView.activityView as AppCompatActivity).supportFragmentManager, + fragment.childFragmentManager, "DatePicker" ) } fun onSliderClick(item: SliderSetting, position: Int) { - clickedItem = item - clickedPosition = position - sliderProgress = item.selectedValue - - val inflater = LayoutInflater.from(context) - val sliderBinding = DialogSliderBinding.inflate(inflater) - - textSliderValue = sliderBinding.textValue - textSliderValue!!.text = 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() + SettingsDialogFragment.newInstance( + settingsViewModel, + item, + SettingsItem.TYPE_SLIDER, + position + ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) } 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) { - when (clickedItem) { - is SingleChoiceSetting -> { - val scSetting = clickedItem as SingleChoiceSetting - val value = getValueForSingleChoiceSelection(scSetting, which) - if (scSetting.selectedValue != value) { - fragmentView.onSettingChanged() - } - - // 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() + fun onLongClick(item: SettingsItem, position: Int): Boolean { + SettingsDialogFragment.newInstance( + settingsViewModel, + item, + SettingsDialogFragment.TYPE_RESET_SETTING, + position + ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) return true } - fun closeDialog() { - if (dialog != null) { - if (clickedPosition != -1) { - notifyItemChanged(clickedPosition) - clickedPosition = -1 - } - dialog!!.dismiss() - dialog = null + private class DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean { + return oldItem.setting.key == newItem.setting.key } - } - private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int { - val valuesId = item.valuesId - return if (valuesId > 0) { - val valuesArray = context.resources.getIntArray(valuesId) - valuesArray[which] - } else { - which + override fun areContentsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean { + return oldItem.setting.key == newItem.setting.key } } - - 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 - } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt index 70a74c4dd..bc319714c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt @@ -3,40 +3,43 @@ package org.yuzu.yuzu_emu.features.settings.ui -import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.ViewGroup.MarginLayoutParams import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.findNavController +import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager 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.features.settings.model.AbstractSetting -import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem +import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile +import org.yuzu.yuzu_emu.model.SettingsViewModel -class SettingsFragment : Fragment(), SettingsFragmentView { - override var activityView: SettingsActivityView? = null - - private val fragmentPresenter = SettingsFragmentPresenter(this) +class SettingsFragment : Fragment() { + private lateinit var presenter: SettingsFragmentPresenter private var settingsAdapter: SettingsAdapter? = null private var _binding: FragmentSettingsBinding? = null private val binding get() = _binding!! - override fun onAttach(context: Context) { - super.onAttach(context) - activityView = requireActivity() as SettingsActivityView - } + private val args by navArgs() + + private val settingsViewModel: SettingsViewModel by activityViewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val menuTag = requireArguments().getString(ARGUMENT_MENU_TAG) - val gameId = requireArguments().getString(ARGUMENT_GAME_ID) - fragmentPresenter.onCreate(menuTag!!, gameId!!) + enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) + returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) } override fun onCreateView( @@ -49,7 +52,14 @@ class SettingsFragment : Fragment(), SettingsFragmentView { } 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( requireContext(), LinearLayoutManager.VERTICAL @@ -57,71 +67,86 @@ class SettingsFragment : Fragment(), SettingsFragmentView { dividerDecoration.isLastItemDecorated = false binding.listSettings.apply { adapter = settingsAdapter - layoutManager = LinearLayoutManager(activity) + layoutManager = LinearLayoutManager(requireContext()) 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() } - override fun onDetach() { - super.onDetach() - activityView = null - if (settingsAdapter != null) { - settingsAdapter!!.closeDialog() - } - } - - override fun showSettingsList(settingsList: ArrayList) { - 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() + override fun onResume() { + super.onResume() + settingsViewModel.setIsUsingSearch(false) } private fun setInsets() { ViewCompat.setOnApplyWindowInsetsListener( - binding.listSettings - ) { view: View, windowInsets: WindowInsetsCompat -> - val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) - view.updatePadding(bottom = insets.bottom) + binding.root + ) { _: View, windowInsets: WindowInsetsCompat -> + 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 + + 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 } } - - 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 - } - } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 59c1d9d54..22a529b1b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -3,63 +3,66 @@ package org.yuzu.yuzu_emu.features.settings.ui +import android.content.Context import android.content.SharedPreferences import android.os.Build import android.text.TextUtils +import android.widget.Toast import androidx.preference.PreferenceManager import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication 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.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.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.utils.SettingsFile -import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment -import org.yuzu.yuzu_emu.utils.ThemeHelper +import org.yuzu.yuzu_emu.model.SettingsViewModel +import org.yuzu.yuzu_emu.utils.NativeConfig -class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) { - private var menuTag: String? = null - private lateinit var gameId: String - private var settingsList: ArrayList? = null +class SettingsFragmentPresenter( + private val settingsViewModel: SettingsViewModel, + private val adapter: SettingsAdapter, + private var menuTag: String, + private var gameId: String +) { + private var settingsList = ArrayList() - private val settingsActivity get() = fragmentView.activityView as SettingsActivity - private val settings get() = fragmentView.activityView!!.settings + private val preferences: SharedPreferences + get() = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) - private lateinit var preferences: SharedPreferences + private val context: Context get() = YuzuApplication.appContext - fun onCreate(menuTag: String, gameId: String) { - this.gameId = gameId - this.menuTag = menuTag + // Extension for populating settings list based on paired settings + fun ArrayList.add(key: String) { + 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() { - preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) 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() { if (!TextUtils.isEmpty(gameId)) { - settingsActivity.setToolbarTitle("Game Settings: $gameId") + settingsViewModel.setToolbarTitle( + context.getString( + R.string.advanced_settings_game, + gameId + ) + ) } + val sl = ArrayList() - if (menuTag == null) { - return - } when (menuTag) { SettingsFile.FILE_NAME_CONFIG -> addConfigSettings(sl) Settings.SECTION_GENERAL -> addGeneralSettings(sl) @@ -69,335 +72,104 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) Settings.SECTION_THEME -> addThemeSettings(sl) Settings.SECTION_DEBUG -> addDebugSettings(sl) else -> { - fragmentView.showToastMessage("Unimplemented menu", false) + val context = YuzuApplication.appContext + Toast.makeText( + context, + context.getString(R.string.unimplemented_menu), + Toast.LENGTH_SHORT + ).show() return } } settingsList = sl - fragmentView.showSettingsList(settingsList!!) + adapter.submitList(settingsList) } private fun addConfigSettings(sl: ArrayList) { - settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.advanced_settings)) + settingsViewModel.setToolbarTitle(context.getString(R.string.advanced_settings)) 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( - 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( - RunnableSetting( - R.string.reset_to_default, - 0, - false - ) { - ResetSettingsDialogFragment().show( - settingsActivity.supportFragmentManager, - ResetSettingsDialogFragment.TAG - ) + RunnableSetting(R.string.reset_to_default, 0, false) { + settingsViewModel.setShouldShowResetSettingsDialog(true) } ) } } private fun addGeneralSettings(sl: ArrayList) { - settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_general)) + settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_general)) sl.apply { - add( - SwitchSetting( - IntSetting.RENDERER_USE_SPEED_LIMIT, - R.string.frame_limit_enable, - 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 - ) - ) + add(BooleanSetting.RENDERER_USE_SPEED_LIMIT.key) + add(ShortSetting.RENDERER_SPEED_LIMIT.key) + add(IntSetting.CPU_ACCURACY.key) + add(BooleanSetting.PICTURE_IN_PICTURE.key) } } private fun addSystemSettings(sl: ArrayList) { - settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_system)) + settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_system)) sl.apply { - add( - SwitchSetting( - IntSetting.USE_DOCKED_MODE, - R.string.use_docked_mode, - R.string.use_docked_mode_description, - 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 - ) - ) + add(BooleanSetting.USE_DOCKED_MODE.key) + add(IntSetting.REGION_INDEX.key) + add(IntSetting.LANGUAGE_INDEX.key) + add(BooleanSetting.USE_CUSTOM_RTC.key) + add(LongSetting.CUSTOM_RTC.key) } } private fun addGraphicsSettings(sl: ArrayList) { - settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_graphics)) + settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_graphics)) sl.apply { - add( - SingleChoiceSetting( - IntSetting.RENDERER_ACCURACY, - R.string.renderer_accuracy, - 0, - R.array.rendererAccuracyNames, - R.array.rendererAccuracyValues, - IntSetting.RENDERER_ACCURACY.key, - IntSetting.RENDERER_ACCURACY.defaultValue - ) - ) - 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 - ) - ) + add(IntSetting.RENDERER_ACCURACY.key) + add(IntSetting.RENDERER_RESOLUTION.key) + add(IntSetting.RENDERER_VSYNC.key) + add(IntSetting.RENDERER_SCALING_FILTER.key) + add(IntSetting.RENDERER_ANTI_ALIASING.key) + add(IntSetting.RENDERER_SCREEN_LAYOUT.key) + add(IntSetting.RENDERER_ASPECT_RATIO.key) + add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.key) + add(BooleanSetting.RENDERER_FORCE_MAX_CLOCK.key) + add(BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS.key) + add(BooleanSetting.RENDERER_REACTIVE_FLUSHING.key) } } private fun addAudioSettings(sl: ArrayList) { - settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_audio)) + settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_audio)) sl.apply { - add( - StringSingleChoiceSetting( - 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 - ) - ) + add(IntSetting.AUDIO_OUTPUT_ENGINE.key) + add(ByteSetting.AUDIO_VOLUME.key) } } private fun addThemeSettings(sl: ArrayList) { - settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_theme)) + settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_theme)) sl.apply { val theme: AbstractIntSetting = object : AbstractIntSetting { - override var int: Int + override val int: Int get() = preferences.getInt(Settings.PREF_THEME, 0) - set(value) { - preferences.edit() - .putInt(Settings.PREF_THEME, value) - .apply() - settingsActivity.recreate() - } - 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 + + override fun setInt(value: Int) { + preferences.edit() + .putInt(Settings.PREF_THEME, value) + .apply() + 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() + } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { @@ -423,20 +195,26 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) } val themeMode: AbstractIntSetting = object : AbstractIntSetting { - override var int: Int + override val int: Int get() = preferences.getInt(Settings.PREF_THEME_MODE, -1) - set(value) { - preferences.edit() - .putInt(Settings.PREF_THEME_MODE, value) - .apply() - ThemeHelper.setThemeMode(settingsActivity) - } - 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 + + override fun setInt(value: Int) { + preferences.edit() + .putInt(Settings.PREF_THEME_MODE, value) + .apply() + 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) + } } add( @@ -450,21 +228,26 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting { - override var boolean: Boolean + override val boolean: Boolean get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) - set(value) { - preferences.edit() - .putBoolean(Settings.PREF_BLACK_BACKGROUNDS, value) - .apply() - settingsActivity.recreate() - } - 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 + + override fun setBoolean(value: Boolean) { + preferences.edit() + .putBoolean(Settings.PREF_BLACK_BACKGROUNDS, value) + .apply() + 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) + } } add( @@ -478,62 +261,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) } private fun addDebugSettings(sl: ArrayList) { - settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_debug)) + settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_debug)) sl.apply { add(HeaderSetting(R.string.gpu)) - add( - SingleChoiceSetting( - 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(IntSetting.RENDERER_BACKEND.key) + add(BooleanSetting.RENDERER_DEBUG.key) add(HeaderSetting(R.string.cpu)) - add( - SwitchSetting( - 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 - ) - ) + add(BooleanSetting.CPU_DEBUG_MODE.key) + add(SettingsItem.FASTMEM_COMBINED) } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentView.kt deleted file mode 100644 index 1ebe35eaa..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentView.kt +++ /dev/null @@ -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) - - /** - * 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() -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt index 79572fc06..525f013f8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt @@ -29,7 +29,7 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA } binding.textSettingValue.visibility = View.VISIBLE - val epochTime = setting.value.toLong() + val epochTime = setting.value val instant = Instant.ofEpochMilli(epochTime * 1000) val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")) val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) @@ -46,7 +46,7 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA override fun onLongClick(clicked: View): Boolean { if (setting.isEditable) { - return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) + return adapter.onLongClick(setting, bindingAdapterPosition) } return false } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt index b42d955aa..80d1b22c1 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt @@ -35,7 +35,7 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti } } } else if (item is StringSingleChoiceSetting) { - for (i in item.values!!.indices) { + for (i in item.values.indices) { if (item.values[i] == item.selectedValue) { binding.textSettingValue.text = item.choices[i] break @@ -66,7 +66,7 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti override fun onLongClick(clicked: View): Boolean { if (setting.isEditable) { - return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) + return adapter.onLongClick(setting, bindingAdapterPosition) } return false } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt index a23b5d109..b83c90100 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt @@ -41,7 +41,7 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda override fun onLongClick(clicked: View): Boolean { if (setting.isEditable) { - return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) + return adapter.onLongClick(setting, bindingAdapterPosition) } return false } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt index ef34bf5f4..57fdeaa20 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt @@ -25,10 +25,12 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter binding.textSettingDescription.text = "" binding.textSettingDescription.visibility = View.GONE } + + binding.switchWidget.setOnCheckedChangeListener(null) + binding.switchWidget.isChecked = setting.checked 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) } @@ -41,7 +43,7 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter override fun onLongClick(clicked: View): Boolean { if (setting.isEditable) { - return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) + return adapter.onLongClick(setting, bindingAdapterPosition) } return false } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt index 70a52df5d..2b04d666a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt @@ -3,18 +3,15 @@ package org.yuzu.yuzu_emu.features.settings.utils +import android.widget.Toast import java.io.* -import java.util.* import org.ini4j.Wini -import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication 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.Log +import org.yuzu.yuzu_emu.utils.NativeConfig /** * 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 { const val FILE_NAME_CONFIG = "config" - private var sectionsMap = BiMap() - - /** - * 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 { - val sections: HashMap = 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 { - return readFile(getSettingsFile(fileName), false, view) - } - - fun readFile(fileName: String): HashMap = - 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 { - return readFile(getCustomGameSettingsFile(gameId), true, view) - } - /** * Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error * telling why it failed. * * @param fileName The target filename without a path or extension. - * @param sections The HashMap containing the Settings we want to serialize. - * @param view The current view. */ - fun saveFile( - fileName: String, - sections: TreeMap, - view: SettingsActivityView - ) { + fun saveFile(fileName: String) { val ini = getSettingsFile(fileName) try { - val writer = Wini(ini) - val keySet: Set = sections.keys - for (key in keySet) { - val section = sections[key] - writeSection(writer, section!!) + val wini = Wini(ini) + for (specificCategory in Settings.Category.values()) { + val categoryHeader = NativeConfig.getConfigHeader(specificCategory.ordinal) + for (setting in Settings.settingsList) { + 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) { Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.message) - view.showToastMessage( - YuzuApplication.appContext - .getString(R.string.error_saving, fileName, e.message), - false - ) + val context = YuzuApplication.appContext + Toast.makeText( + context, + context.getString(R.string.error_saving, fileName, e.message), + Toast.LENGTH_SHORT + ).show() } } - fun saveCustomGameSettings(gameId: String?, sections: HashMap) { - val sortedSections: Set = TreeSet(sections.keys) - for (sectionKey in sortedSections) { - val section = sections[sectionKey] - val settings = section!!.settings - val sortedKeySet: Set = 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 = 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) - } - } - } + fun getSettingsFile(fileName: String): File = + File(DirectoryInitialization.userDirectory + "/config/" + fileName + ".ini") } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index 956c35c0a..53f19c4f8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -29,6 +29,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.findNavController import androidx.navigation.fragment.navArgs import androidx.preference.PreferenceManager import androidx.window.layout.FoldingFeature @@ -38,6 +39,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.slider.Slider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R 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.features.settings.model.IntSetting 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.model.Game import org.yuzu.yuzu_emu.overlay.InputOverlay @@ -158,7 +159,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } 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 } @@ -230,7 +235,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { override fun onResume() { super.onResume() if (!DirectoryInitialization.areDirectoriesReady) { - DirectoryInitialization.start(requireContext()) + DirectoryInitialization.start() } updateScreenLayout() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index d5e793491..cbbe14d22 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt @@ -25,17 +25,18 @@ import androidx.core.view.updatePadding import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.transition.MaterialSharedAxis import org.yuzu.yuzu_emu.BuildConfig +import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding import org.yuzu.yuzu_emu.features.DocumentProvider 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.model.HomeSetting import org.yuzu.yuzu_emu.model.HomeViewModel @@ -74,7 +75,13 @@ class HomeSettingsFragment : Fragment() { R.string.advanced_settings, R.string.settings_description, 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( @@ -90,7 +97,13 @@ class HomeSettingsFragment : Fragment() { R.string.preferences_theme, R.string.theme_and_color_description, R.drawable.ic_palette, - { SettingsActivity.launch(requireContext(), Settings.SECTION_THEME, "") } + { + val action = HomeNavigationDirections.actionGlobalSettingsActivity( + null, + Settings.SECTION_THEME + ) + binding.root.findNavController().navigate(action) + } ) ) add( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt new file mode 100644 index 000000000..d18ec6974 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt @@ -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 + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt new file mode 100644 index 000000000..55b6a0367 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt @@ -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 = 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" + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt new file mode 100644 index 000000000..d16d15fa6 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt @@ -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 get() = _toolbarTitle + + private val _shouldRecreate = MutableLiveData(false) + val shouldRecreate: LiveData get() = _shouldRecreate + + private val _shouldNavigateBack = MutableLiveData(false) + val shouldNavigateBack: LiveData get() = _shouldNavigateBack + + private val _shouldShowResetSettingsDialog = MutableLiveData(false) + val shouldShowResetSettingsDialog: LiveData get() = _shouldShowResetSettingsDialog + + private val _shouldReloadSettingsList = MutableLiveData(false) + val shouldReloadSettingsList: LiveData get() = _shouldReloadSettingsList + + private val _isUsingSearch = MutableLiveData(false) + val isUsingSearch: LiveData 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" + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index aaf3a0ec1..7735452e5 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -33,14 +33,13 @@ import java.io.IOException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.activities.EmulationActivity import org.yuzu.yuzu_emu.databinding.ActivityMainBinding import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding 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.fragments.IndeterminateProgressDialogFragment import org.yuzu.yuzu_emu.fragments.LongMessageDialogFragment @@ -54,7 +53,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider { private val homeViewModel: HomeViewModel by viewModels() private val gamesViewModel: GamesViewModel by viewModels() - private val settingsViewModel: SettingsViewModel by viewModels() override var themeId: Int = 0 @@ -62,8 +60,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider { val splashScreen = installSplashScreen() splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } - settingsViewModel.settings.loadSettings() - ThemeHelper.setTheme(this) super.onCreate(savedInstanceState) @@ -109,11 +105,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider { when (it.itemId) { R.id.gamesFragment -> gamesViewModel.setShouldScrollToTop(true) R.id.searchFragment -> gamesViewModel.setSearchFocused(true) - R.id.homeSettingsFragment -> SettingsActivity.launch( - this, - SettingsFile.FILE_NAME_CONFIG, - "" - ) + R.id.homeSettingsFragment -> { + val action = HomeNavigationDirections.actionGlobalSettingsActivity( + null, + SettingsFile.FILE_NAME_CONFIG + ) + navHostFragment.navController.navigate(action) + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/BiMap.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/BiMap.kt deleted file mode 100644 index 9cfda74ee..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/BiMap.kt +++ /dev/null @@ -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 { - private val forward: MutableMap = HashMap() - private val backward: MutableMap = 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] - } -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt index 2ee63697e..3c9f6bad0 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt @@ -3,18 +3,18 @@ package org.yuzu.yuzu_emu.utils -import android.content.Context import java.io.IOException import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.YuzuApplication object DirectoryInitialization { private var userPath: String? = null var areDirectoriesReady: Boolean = false - fun start(context: Context) { + fun start() { if (!areDirectoriesReady) { - initializeInternalStorage(context) + initializeInternalStorage() NativeLibrary.initializeEmulation() areDirectoriesReady = true } @@ -26,9 +26,9 @@ object DirectoryInitialization { return userPath } - private fun initializeInternalStorage(context: Context) { + private fun initializeInternalStorage() { try { - userPath = context.getExternalFilesDir(null)!!.canonicalPath + userPath = YuzuApplication.appContext.getExternalFilesDir(null)!!.canonicalPath NativeLibrary.setAppDirectory(userPath!!) } catch (e: IOException) { e.printStackTrace() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt new file mode 100644 index 000000000..9425f8b99 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt @@ -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 +} diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt index e2ed08e9f..e15d1480b 100644 --- a/src/android/app/src/main/jni/CMakeLists.txt +++ b/src/android/app/src/main/jni/CMakeLists.txt @@ -14,6 +14,8 @@ add_library(yuzu-android SHARED id_cache.cpp id_cache.h native.cpp + native_config.cpp + uisettings.cpp ) set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR}) diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index 9de9bd93e..34b425cb4 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -16,18 +16,20 @@ #include "input_common/main.h" #include "jni/config.h" #include "jni/default_ini.h" +#include "uisettings.h" namespace FS = Common::FS; -Config::Config(std::optional config_path) - : config_loc{config_path.value_or(FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "config.ini")}, - config{std::make_unique(FS::PathToUTF8String(config_loc))} { - Reload(); +Config::Config(const std::string& config_name, ConfigType config_type) + : type(config_type), global{config_type == ConfigType::GlobalConfig} { + Initialize(config_name); } Config::~Config() = default; bool Config::LoadINI(const std::string& default_contents, bool retry) { + void(FS::CreateParentDir(config_loc)); + config = std::make_unique(FS::PathToUTF8String(config_loc)); const auto config_loc_str = FS::PathToUTF8String(config_loc); if (config->ParseError() < 0) { if (retry) { @@ -301,9 +303,28 @@ void Config::ReadValues() { // Network 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); ReadValues(); } diff --git a/src/android/app/src/main/jni/config.h b/src/android/app/src/main/jni/config.h index 0d7d6e94d..e1e8f47ed 100644 --- a/src/android/app/src/main/jni/config.h +++ b/src/android/app/src/main/jni/config.h @@ -13,25 +13,35 @@ class INIReader; class Config { - std::filesystem::path config_loc; - std::unique_ptr config; - bool LoadINI(const std::string& default_contents = "", bool retry = true); - void ReadValues(); public: - explicit Config(std::optional config_path = std::nullopt); + enum class ConfigType { + GlobalConfig, + PerGameConfig, + InputProfile, + }; + + explicit Config(const std::string& config_name = "config", + ConfigType config_type = ConfigType::GlobalConfig); ~Config(); - void Reload(); + void Initialize(const std::string& config_name); 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 setting The yuzu setting to modify */ template void ReadSetting(const std::string& group, Settings::Setting& setting); + + void ReadValues(); + + const ConfigType type; + std::unique_ptr config; + std::string config_loc; + const bool global; }; diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 7e17833a0..b2adfdeda 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -824,34 +824,6 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_reloadSettings(JNIEnv* env, jclass cl 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, jstring j_game_id) { std::string_view game_id = env->GetStringUTFChars(j_game_id, 0); diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp new file mode 100644 index 000000000..8a704960c --- /dev/null +++ b/src/android/app/src/main/jni/native_config.cpp @@ -0,0 +1,237 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include + +#include "common/logging/log.h" +#include "common/settings.h" +#include "jni/android_common/android_common.h" +#include "jni/config.h" +#include "uisettings.h" + +template +Settings::Setting* 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*>(basicSetting); + } + if (basicAndroidSetting != 0) { + return static_cast*>(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(env, jkey); + if (setting == nullptr) { + return false; + } + setting->SetGlobal(true); + + if (static_cast(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(env, jkey); + if (setting == nullptr) { + return; + } + setting->SetGlobal(true); + setting->SetValue(static_cast(value)); +} + +jbyte Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getByte(JNIEnv* env, jobject obj, jstring jkey, + jboolean getDefault) { + auto setting = getSetting(env, jkey); + if (setting == nullptr) { + return -1; + } + setting->SetGlobal(true); + + if (static_cast(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(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(env, jkey); + if (setting == nullptr) { + return -1; + } + setting->SetGlobal(true); + + if (static_cast(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(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(env, jkey); + if (setting == nullptr) { + return -1; + } + setting->SetGlobal(true); + + if (static_cast(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(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(env, jkey); + if (setting == nullptr) { + return -1; + } + setting->SetGlobal(true); + + if (static_cast(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(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(env, jkey); + if (setting == nullptr) { + return -1; + } + setting->SetGlobal(true); + + if (static_cast(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(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(env, jkey); + if (setting == nullptr) { + return ToJString(env, ""); + } + setting->SetGlobal(true); + + if (static_cast(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(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(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(env, jkey); + if (setting == nullptr) { + return ToJString(env, ""); + } + if (setting->PairedSetting() == nullptr) { + return ToJString(env, ""); + } + + return ToJString(env, setting->PairedSetting()->GetLabel()); +} + +} // extern "C" diff --git a/src/android/app/src/main/jni/uisettings.cpp b/src/android/app/src/main/jni/uisettings.cpp new file mode 100644 index 000000000..f2f0bad50 --- /dev/null +++ b/src/android/app/src/main/jni/uisettings.cpp @@ -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 diff --git a/src/android/app/src/main/jni/uisettings.h b/src/android/app/src/main/jni/uisettings.h new file mode 100644 index 000000000..494654af7 --- /dev/null +++ b/src/android/app/src/main/jni/uisettings.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include "common/common_types.h" +#include "common/settings_setting.h" + +namespace AndroidSettings { + +struct Values { + Settings::Linkage linkage; + + // Android + Settings::Setting picture_in_picture{linkage, true, "picture_in_picture", + Settings::Category::Android}; + Settings::Setting screen_layout{linkage, + 5, + "screen_layout", + Settings::Category::Android, + Settings::Specialization::Default, + true, + true}; +}; + +extern Values values; + +} // namespace AndroidSettings diff --git a/src/android/app/src/main/res/anim-ldrtl/anim_pop_settings_fragment_out.xml b/src/android/app/src/main/res/anim-ldrtl/anim_pop_settings_fragment_out.xml deleted file mode 100644 index 9f49c133a..000000000 --- a/src/android/app/src/main/res/anim-ldrtl/anim_pop_settings_fragment_out.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - diff --git a/src/android/app/src/main/res/anim-ldrtl/anim_settings_fragment_in.xml b/src/android/app/src/main/res/anim-ldrtl/anim_settings_fragment_in.xml deleted file mode 100644 index 82fd719db..000000000 --- a/src/android/app/src/main/res/anim-ldrtl/anim_settings_fragment_in.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - diff --git a/src/android/app/src/main/res/anim/anim_pop_settings_fragment_out.xml b/src/android/app/src/main/res/anim/anim_pop_settings_fragment_out.xml deleted file mode 100644 index 5892128f1..000000000 --- a/src/android/app/src/main/res/anim/anim_pop_settings_fragment_out.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - diff --git a/src/android/app/src/main/res/anim/anim_settings_fragment_in.xml b/src/android/app/src/main/res/anim/anim_settings_fragment_in.xml deleted file mode 100644 index 98e0cf8bd..000000000 --- a/src/android/app/src/main/res/anim/anim_settings_fragment_in.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - diff --git a/src/android/app/src/main/res/anim/anim_settings_fragment_out.xml b/src/android/app/src/main/res/anim/anim_settings_fragment_out.xml deleted file mode 100644 index 77a40a4d1..000000000 --- a/src/android/app/src/main/res/anim/anim_settings_fragment_out.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - diff --git a/src/android/app/src/main/res/animator/menu_slide_in_from_start.xml b/src/android/app/src/main/res/animator/menu_slide_in_from_start.xml deleted file mode 100644 index 4612aee13..000000000 --- a/src/android/app/src/main/res/animator/menu_slide_in_from_start.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/android/app/src/main/res/animator/menu_slide_out_to_start.xml b/src/android/app/src/main/res/animator/menu_slide_out_to_start.xml deleted file mode 100644 index c00478946..000000000 --- a/src/android/app/src/main/res/animator/menu_slide_out_to_start.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/activity_settings.xml b/src/android/app/src/main/res/layout/activity_settings.xml index 14ae83b04..8a026a30a 100644 --- a/src/android/app/src/main/res/layout/activity_settings.xml +++ b/src/android/app/src/main/res/layout/activity_settings.xml @@ -1,42 +1,24 @@ - - - - - - - - - - - - + + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> - + diff --git a/src/android/app/src/main/res/layout/fragment_settings.xml b/src/android/app/src/main/res/layout/fragment_settings.xml index 167720347..ebedbf1ec 100644 --- a/src/android/app/src/main/res/layout/fragment_settings.xml +++ b/src/android/app/src/main/res/layout/fragment_settings.xml @@ -1,14 +1,41 @@ - + android:layout_height="match_parent" + android:background="?attr/colorSurface"> + + + + + + + + + + + android:clipToPadding="false" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> - + diff --git a/src/android/app/src/main/res/layout/fragment_settings_search.xml b/src/android/app/src/main/res/layout/fragment_settings_search.xml new file mode 100644 index 000000000..c779ed2fc --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_settings_search.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + +