android: Add per-game settings
This commit is contained in:
		| @@ -12,6 +12,7 @@ import org.yuzu.yuzu_emu.features.settings.model.ByteSetting | |||||||
| import org.yuzu.yuzu_emu.features.settings.model.IntSetting | import org.yuzu.yuzu_emu.features.settings.model.IntSetting | ||||||
| import org.yuzu.yuzu_emu.features.settings.model.LongSetting | import org.yuzu.yuzu_emu.features.settings.model.LongSetting | ||||||
| import org.yuzu.yuzu_emu.features.settings.model.ShortSetting | import org.yuzu.yuzu_emu.features.settings.model.ShortSetting | ||||||
|  | import org.yuzu.yuzu_emu.utils.NativeConfig | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments. |  * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments. | ||||||
| @@ -30,9 +31,19 @@ abstract class SettingsItem( | |||||||
|     val isEditable: Boolean |     val isEditable: Boolean | ||||||
|         get() { |         get() { | ||||||
|             if (!NativeLibrary.isRunning()) return true |             if (!NativeLibrary.isRunning()) return true | ||||||
|  |  | ||||||
|  |             // Prevent editing settings that were modified in per-game config while editing global | ||||||
|  |             // config | ||||||
|  |             if (!NativeConfig.isPerGameConfigLoaded() && !setting.global) { | ||||||
|  |                 return false | ||||||
|  |             } | ||||||
|             return setting.isRuntimeModifiable |             return setting.isRuntimeModifiable | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |     val needsRuntimeGlobal: Boolean | ||||||
|  |         get() = NativeLibrary.isRunning() && !setting.global && | ||||||
|  |             !NativeConfig.isPerGameConfigLoaded() | ||||||
|  |  | ||||||
|     companion object { |     companion object { | ||||||
|         const val TYPE_HEADER = 0 |         const val TYPE_HEADER = 0 | ||||||
|         const val TYPE_SWITCH = 1 |         const val TYPE_SWITCH = 1 | ||||||
|   | |||||||
| @@ -19,10 +19,9 @@ import androidx.lifecycle.repeatOnLifecycle | |||||||
| import androidx.navigation.fragment.NavHostFragment | import androidx.navigation.fragment.NavHostFragment | ||||||
| import androidx.navigation.navArgs | import androidx.navigation.navArgs | ||||||
| import com.google.android.material.color.MaterialColors | import com.google.android.material.color.MaterialColors | ||||||
| import kotlinx.coroutines.CoroutineScope |  | ||||||
| import kotlinx.coroutines.Dispatchers |  | ||||||
| import kotlinx.coroutines.flow.collectLatest | import kotlinx.coroutines.flow.collectLatest | ||||||
| import kotlinx.coroutines.launch | import kotlinx.coroutines.launch | ||||||
|  | import org.yuzu.yuzu_emu.NativeLibrary | ||||||
| import java.io.IOException | import java.io.IOException | ||||||
| import org.yuzu.yuzu_emu.R | import org.yuzu.yuzu_emu.R | ||||||
| import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding | import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding | ||||||
| @@ -46,6 +45,9 @@ class SettingsActivity : AppCompatActivity() { | |||||||
|         binding = ActivitySettingsBinding.inflate(layoutInflater) |         binding = ActivitySettingsBinding.inflate(layoutInflater) | ||||||
|         setContentView(binding.root) |         setContentView(binding.root) | ||||||
|  |  | ||||||
|  |         if (!NativeConfig.isPerGameConfigLoaded() && args.game != null) { | ||||||
|  |             SettingsFile.loadCustomConfig(args.game!!) | ||||||
|  |         } | ||||||
|         settingsViewModel.game = args.game |         settingsViewModel.game = args.game | ||||||
|  |  | ||||||
|         val navHostFragment = |         val navHostFragment = | ||||||
| @@ -126,7 +128,6 @@ class SettingsActivity : AppCompatActivity() { | |||||||
|  |  | ||||||
|     override fun onStart() { |     override fun onStart() { | ||||||
|         super.onStart() |         super.onStart() | ||||||
|         // TODO: Load custom settings contextually |  | ||||||
|         if (!DirectoryInitialization.areDirectoriesReady) { |         if (!DirectoryInitialization.areDirectoriesReady) { | ||||||
|             DirectoryInitialization.start() |             DirectoryInitialization.start() | ||||||
|         } |         } | ||||||
| @@ -134,24 +135,35 @@ class SettingsActivity : AppCompatActivity() { | |||||||
|  |  | ||||||
|     override fun onStop() { |     override fun onStop() { | ||||||
|         super.onStop() |         super.onStop() | ||||||
|         CoroutineScope(Dispatchers.IO).launch { |         Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...") | ||||||
|             NativeConfig.saveSettings() |         if (isFinishing) { | ||||||
|  |             NativeLibrary.applySettings() | ||||||
|  |             if (args.game == null) { | ||||||
|  |                 NativeConfig.saveGlobalConfig() | ||||||
|  |             } else if (NativeConfig.isPerGameConfigLoaded()) { | ||||||
|  |                 NativeLibrary.logSettings() | ||||||
|  |                 NativeConfig.savePerGameConfig() | ||||||
|  |                 NativeConfig.unloadPerGameConfig() | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override fun onDestroy() { |  | ||||||
|         settingsViewModel.clear() |  | ||||||
|         super.onDestroy() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun onSettingsReset() { |     fun onSettingsReset() { | ||||||
|         // Delete settings file because the user may have changed values that do not exist in the UI |         // Delete settings file because the user may have changed values that do not exist in the UI | ||||||
|         NativeConfig.unloadConfig() |         if (args.game == null) { | ||||||
|         val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG) |             NativeConfig.unloadGlobalConfig() | ||||||
|         if (!settingsFile.delete()) { |             val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG) | ||||||
|             throw IOException("Failed to delete $settingsFile") |             if (!settingsFile.delete()) { | ||||||
|  |                 throw IOException("Failed to delete $settingsFile") | ||||||
|  |             } | ||||||
|  |             NativeConfig.initializeGlobalConfig() | ||||||
|  |         } else { | ||||||
|  |             NativeConfig.unloadPerGameConfig() | ||||||
|  |             val settingsFile = SettingsFile.getCustomSettingsFile(args.game!!) | ||||||
|  |             if (!settingsFile.delete()) { | ||||||
|  |                 throw IOException("Failed to delete $settingsFile") | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|         NativeConfig.initializeConfig() |  | ||||||
|  |  | ||||||
|         Toast.makeText( |         Toast.makeText( | ||||||
|             applicationContext, |             applicationContext, | ||||||
|   | |||||||
| @@ -196,6 +196,12 @@ class SettingsAdapter( | |||||||
|         return true |         return true | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     fun onClearClick(item: SettingsItem, position: Int) { | ||||||
|  |         item.setting.global = true | ||||||
|  |         notifyItemChanged(position) | ||||||
|  |         settingsViewModel.setShouldReloadSettingsList(true) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() { |     private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() { | ||||||
|         override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean { |         override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean { | ||||||
|             return oldItem.setting.key == newItem.setting.key |             return oldItem.setting.key == newItem.setting.key | ||||||
|   | |||||||
| @@ -66,7 +66,13 @@ class SettingsFragment : Fragment() { | |||||||
|             args.menuTag |             args.menuTag | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         binding.toolbarSettingsLayout.title = getString(args.menuTag.titleId) |         binding.toolbarSettingsLayout.title = if (args.menuTag == Settings.MenuTag.SECTION_ROOT && | ||||||
|  |             args.game != null | ||||||
|  |         ) { | ||||||
|  |             args.game!!.title | ||||||
|  |         } else { | ||||||
|  |             getString(args.menuTag.titleId) | ||||||
|  |         } | ||||||
|         binding.listSettings.apply { |         binding.listSettings.apply { | ||||||
|             adapter = settingsAdapter |             adapter = settingsAdapter | ||||||
|             layoutManager = LinearLayoutManager(requireContext()) |             layoutManager = LinearLayoutManager(requireContext()) | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import android.content.SharedPreferences | |||||||
| import android.os.Build | import android.os.Build | ||||||
| import android.widget.Toast | import android.widget.Toast | ||||||
| import androidx.preference.PreferenceManager | import androidx.preference.PreferenceManager | ||||||
|  | import org.yuzu.yuzu_emu.NativeLibrary | ||||||
| import org.yuzu.yuzu_emu.R | import org.yuzu.yuzu_emu.R | ||||||
| import org.yuzu.yuzu_emu.YuzuApplication | import org.yuzu.yuzu_emu.YuzuApplication | ||||||
| import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting | import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting | ||||||
| @@ -31,9 +32,17 @@ class SettingsFragmentPresenter( | |||||||
|     private val preferences: SharedPreferences |     private val preferences: SharedPreferences | ||||||
|         get() = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) |         get() = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) | ||||||
|  |  | ||||||
|     // Extension for populating settings list based on paired settings |     // Extension for altering settings list based on each setting's properties | ||||||
|     fun ArrayList<SettingsItem>.add(key: String) { |     fun ArrayList<SettingsItem>.add(key: String) { | ||||||
|         val item = SettingsItem.settingsItems[key]!! |         val item = SettingsItem.settingsItems[key]!! | ||||||
|  |         if (settingsViewModel.game != null && !item.setting.isSwitchable) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!NativeConfig.isPerGameConfigLoaded() && !NativeLibrary.isRunning()) { | ||||||
|  |             item.setting.global = true | ||||||
|  |         } | ||||||
|  |  | ||||||
|         val pairedSettingKey = item.setting.pairedSettingKey |         val pairedSettingKey = item.setting.pairedSettingKey | ||||||
|         if (pairedSettingKey.isNotEmpty()) { |         if (pairedSettingKey.isNotEmpty()) { | ||||||
|             val pairedSettingValue = NativeConfig.getBoolean( |             val pairedSettingValue = NativeConfig.getBoolean( | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding | |||||||
| import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting | import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting | ||||||
| import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | ||||||
| import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | ||||||
|  | import org.yuzu.yuzu_emu.utils.NativeConfig | ||||||
|  |  | ||||||
| class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : | class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : | ||||||
|     SettingViewHolder(binding.root, adapter) { |     SettingViewHolder(binding.root, adapter) { | ||||||
| @@ -35,6 +36,17 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA | |||||||
|         val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) |         val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) | ||||||
|         binding.textSettingValue.text = dateFormatter.format(zonedTime) |         binding.textSettingValue.text = dateFormatter.format(zonedTime) | ||||||
|  |  | ||||||
|  |         binding.buttonClear.visibility = if (setting.setting.global || | ||||||
|  |             !NativeConfig.isPerGameConfigLoaded() | ||||||
|  |         ) { | ||||||
|  |             View.GONE | ||||||
|  |         } else { | ||||||
|  |             View.VISIBLE | ||||||
|  |         } | ||||||
|  |         binding.buttonClear.setOnClickListener { | ||||||
|  |             adapter.onClearClick(setting, bindingAdapterPosition) | ||||||
|  |         } | ||||||
|  |  | ||||||
|         setStyle(setting.isEditable, binding) |         setStyle(setting.isEditable, binding) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -38,6 +38,7 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA | |||||||
|             binding.textSettingDescription.visibility = View.GONE |             binding.textSettingDescription.visibility = View.GONE | ||||||
|         } |         } | ||||||
|         binding.textSettingValue.visibility = View.GONE |         binding.textSettingValue.visibility = View.GONE | ||||||
|  |         binding.buttonClear.visibility = View.GONE | ||||||
|  |  | ||||||
|         setStyle(setting.isEditable, binding) |         setStyle(setting.isEditable, binding) | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -41,6 +41,7 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings | |||||||
|         binding.textSettingName.alpha = opacity |         binding.textSettingName.alpha = opacity | ||||||
|         binding.textSettingDescription.alpha = opacity |         binding.textSettingDescription.alpha = opacity | ||||||
|         binding.textSettingValue.alpha = opacity |         binding.textSettingValue.alpha = opacity | ||||||
|  |         binding.buttonClear.isEnabled = isEditable | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun setStyle(isEditable: Boolean, binding: ListItemSettingSwitchBinding) { |     fun setStyle(isEditable: Boolean, binding: ListItemSettingSwitchBinding) { | ||||||
| @@ -48,5 +49,6 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings | |||||||
|         val opacity = if (isEditable) 1.0f else 0.5f |         val opacity = if (isEditable) 1.0f else 0.5f | ||||||
|         binding.textSettingName.alpha = opacity |         binding.textSettingName.alpha = opacity | ||||||
|         binding.textSettingDescription.alpha = opacity |         binding.textSettingDescription.alpha = opacity | ||||||
|  |         binding.buttonClear.isEnabled = isEditable | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ 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.SingleChoiceSetting | ||||||
| import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting | import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting | ||||||
| import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | ||||||
|  | import org.yuzu.yuzu_emu.utils.NativeConfig | ||||||
|  |  | ||||||
| class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : | class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : | ||||||
|     SettingViewHolder(binding.root, adapter) { |     SettingViewHolder(binding.root, adapter) { | ||||||
| @@ -43,6 +44,17 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         binding.buttonClear.visibility = if (setting.setting.global || | ||||||
|  |             !NativeConfig.isPerGameConfigLoaded() | ||||||
|  |         ) { | ||||||
|  |             View.GONE | ||||||
|  |         } else { | ||||||
|  |             View.VISIBLE | ||||||
|  |         } | ||||||
|  |         binding.buttonClear.setOnClickListener { | ||||||
|  |             adapter.onClearClick(setting, bindingAdapterPosition) | ||||||
|  |         } | ||||||
|  |  | ||||||
|         setStyle(setting.isEditable, binding) |         setStyle(setting.isEditable, binding) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding | |||||||
| import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | ||||||
| import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting | import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting | ||||||
| import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | ||||||
|  | import org.yuzu.yuzu_emu.utils.NativeConfig | ||||||
|  |  | ||||||
| class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : | class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : | ||||||
|     SettingViewHolder(binding.root, adapter) { |     SettingViewHolder(binding.root, adapter) { | ||||||
| @@ -30,6 +31,17 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda | |||||||
|             setting.units |             setting.units | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |         binding.buttonClear.visibility = if (setting.setting.global || | ||||||
|  |             !NativeConfig.isPerGameConfigLoaded() | ||||||
|  |         ) { | ||||||
|  |             View.GONE | ||||||
|  |         } else { | ||||||
|  |             View.VISIBLE | ||||||
|  |         } | ||||||
|  |         binding.buttonClear.setOnClickListener { | ||||||
|  |             adapter.onClearClick(setting, bindingAdapterPosition) | ||||||
|  |         } | ||||||
|  |  | ||||||
|         setStyle(setting.isEditable, binding) |         setStyle(setting.isEditable, binding) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -37,6 +37,7 @@ class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAd | |||||||
|             binding.textSettingDescription.visibility = View.GONE |             binding.textSettingDescription.visibility = View.GONE | ||||||
|         } |         } | ||||||
|         binding.textSettingValue.visibility = View.GONE |         binding.textSettingValue.visibility = View.GONE | ||||||
|  |         binding.buttonClear.visibility = View.GONE | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override fun onClick(clicked: View) { |     override fun onClick(clicked: View) { | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding | |||||||
| import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | ||||||
| import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting | import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting | ||||||
| import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | ||||||
|  | import org.yuzu.yuzu_emu.utils.NativeConfig | ||||||
|  |  | ||||||
| class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) : | class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) : | ||||||
|     SettingViewHolder(binding.root, adapter) { |     SettingViewHolder(binding.root, adapter) { | ||||||
| @@ -29,7 +30,18 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter | |||||||
|         binding.switchWidget.setOnCheckedChangeListener(null) |         binding.switchWidget.setOnCheckedChangeListener(null) | ||||||
|         binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal) |         binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal) | ||||||
|         binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> |         binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> | ||||||
|             adapter.onBooleanClick(item, binding.switchWidget.isChecked) |             adapter.onBooleanClick(item, binding.switchWidget.isChecked, bindingAdapterPosition) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.buttonClear.visibility = if (setting.setting.global || | ||||||
|  |             !NativeConfig.isPerGameConfigLoaded() | ||||||
|  |         ) { | ||||||
|  |             View.GONE | ||||||
|  |         } else { | ||||||
|  |             View.VISIBLE | ||||||
|  |         } | ||||||
|  |         binding.buttonClear.setOnClickListener { | ||||||
|  |             adapter.onClearClick(setting, bindingAdapterPosition) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         setStyle(setting.isEditable, binding) |         setStyle(setting.isEditable, binding) | ||||||
|   | |||||||
| @@ -3,15 +3,27 @@ | |||||||
|  |  | ||||||
| package org.yuzu.yuzu_emu.features.settings.utils | package org.yuzu.yuzu_emu.features.settings.utils | ||||||
|  |  | ||||||
|  | import android.net.Uri | ||||||
|  | import org.yuzu.yuzu_emu.model.Game | ||||||
| import java.io.* | import java.io.* | ||||||
| import org.yuzu.yuzu_emu.utils.DirectoryInitialization | import org.yuzu.yuzu_emu.utils.DirectoryInitialization | ||||||
|  | import org.yuzu.yuzu_emu.utils.FileUtil | ||||||
|  | import org.yuzu.yuzu_emu.utils.NativeConfig | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Contains static methods for interacting with .ini files in which settings are stored. |  * Contains static methods for interacting with .ini files in which settings are stored. | ||||||
|  */ |  */ | ||||||
| object SettingsFile { | object SettingsFile { | ||||||
|     const val FILE_NAME_CONFIG = "config" |     const val FILE_NAME_CONFIG = "config.ini" | ||||||
|  |  | ||||||
|     fun getSettingsFile(fileName: String): File = |     fun getSettingsFile(fileName: String): File = | ||||||
|         File(DirectoryInitialization.userDirectory + "/config/" + fileName + ".ini") |         File(DirectoryInitialization.userDirectory + "/config/" + fileName) | ||||||
|  |  | ||||||
|  |     fun getCustomSettingsFile(game: Game): File = | ||||||
|  |         File(DirectoryInitialization.userDirectory + "/config/custom/" + game.settingsName + ".ini") | ||||||
|  |  | ||||||
|  |     fun loadCustomConfig(game: Game) { | ||||||
|  |         val fileName = FileUtil.getFilename(Uri.parse(game.path)) | ||||||
|  |         NativeConfig.initializePerGameConfig(game.programId, fileName) | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -52,6 +52,7 @@ import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding | |||||||
| import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding | import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding | ||||||
| import org.yuzu.yuzu_emu.features.settings.model.IntSetting | import org.yuzu.yuzu_emu.features.settings.model.IntSetting | ||||||
| import org.yuzu.yuzu_emu.features.settings.model.Settings | import org.yuzu.yuzu_emu.features.settings.model.Settings | ||||||
|  | import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile | ||||||
| import org.yuzu.yuzu_emu.model.DriverViewModel | import org.yuzu.yuzu_emu.model.DriverViewModel | ||||||
| import org.yuzu.yuzu_emu.model.Game | import org.yuzu.yuzu_emu.model.Game | ||||||
| import org.yuzu.yuzu_emu.model.EmulationViewModel | import org.yuzu.yuzu_emu.model.EmulationViewModel | ||||||
| @@ -127,6 +128,16 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||||||
|             return |             return | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         if (args.custom) { | ||||||
|  |             SettingsFile.loadCustomConfig(args.game!!) | ||||||
|  |             NativeConfig.unloadPerGameConfig() | ||||||
|  |         } else { | ||||||
|  |             NativeConfig.reloadGlobalConfig() | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Install the selected driver asynchronously as the game starts | ||||||
|  |         driverViewModel.onLaunchGame() | ||||||
|  |  | ||||||
|         // So this fragment doesn't restart on configuration changes; i.e. rotation. |         // So this fragment doesn't restart on configuration changes; i.e. rotation. | ||||||
|         retainInstance = true |         retainInstance = true | ||||||
|         preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) |         preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) | ||||||
| @@ -217,6 +228,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||||||
|                     true |                     true | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  |                 R.id.menu_settings_per_game -> { | ||||||
|  |                     val action = HomeNavigationDirections.actionGlobalSettingsActivity( | ||||||
|  |                         args.game, | ||||||
|  |                         Settings.MenuTag.SECTION_ROOT | ||||||
|  |                     ) | ||||||
|  |                     binding.root.findNavController().navigate(action) | ||||||
|  |                     true | ||||||
|  |                 } | ||||||
|  |  | ||||||
|                 R.id.menu_overlay_controls -> { |                 R.id.menu_overlay_controls -> { | ||||||
|                     showOverlayOptions() |                     showOverlayOptions() | ||||||
|                     true |                     true | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ import org.yuzu.yuzu_emu.R | |||||||
| import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding | import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding | ||||||
| import org.yuzu.yuzu_emu.model.GameDir | import org.yuzu.yuzu_emu.model.GameDir | ||||||
| import org.yuzu.yuzu_emu.model.GamesViewModel | import org.yuzu.yuzu_emu.model.GamesViewModel | ||||||
|  | import org.yuzu.yuzu_emu.utils.NativeConfig | ||||||
| import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable | import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable | ||||||
|  |  | ||||||
| class GameFolderPropertiesDialogFragment : DialogFragment() { | class GameFolderPropertiesDialogFragment : DialogFragment() { | ||||||
| @@ -49,6 +50,11 @@ class GameFolderPropertiesDialogFragment : DialogFragment() { | |||||||
|             .show() |             .show() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     override fun onStop() { | ||||||
|  |         super.onStop() | ||||||
|  |         NativeConfig.saveGlobalConfig() | ||||||
|  |     } | ||||||
|  |  | ||||||
|     override fun onSaveInstanceState(outState: Bundle) { |     override fun onSaveInstanceState(outState: Bundle) { | ||||||
|         super.onSaveInstanceState(outState) |         super.onSaveInstanceState(outState) | ||||||
|         outState.putBoolean(DEEP_SCAN, deepScan) |         outState.putBoolean(DEEP_SCAN, deepScan) | ||||||
|   | |||||||
| @@ -304,6 +304,11 @@ class SetupFragment : Fragment() { | |||||||
|         setInsets() |         setInsets() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     override fun onStop() { | ||||||
|  |         super.onStop() | ||||||
|  |         NativeConfig.saveGlobalConfig() | ||||||
|  |     } | ||||||
|  |  | ||||||
|     override fun onSaveInstanceState(outState: Bundle) { |     override fun onSaveInstanceState(outState: Bundle) { | ||||||
|         super.onSaveInstanceState(outState) |         super.onSaveInstanceState(outState) | ||||||
|         if (_binding != null) { |         if (_binding != null) { | ||||||
|   | |||||||
| @@ -168,6 +168,7 @@ class GamesViewModel : ViewModel() { | |||||||
|     fun onCloseGameFoldersFragment() = |     fun onCloseGameFoldersFragment() = | ||||||
|         viewModelScope.launch { |         viewModelScope.launch { | ||||||
|             withContext(Dispatchers.IO) { |             withContext(Dispatchers.IO) { | ||||||
|  |                 NativeConfig.saveGlobalConfig() | ||||||
|                 getGameDirs(true) |                 getGameDirs(true) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -68,8 +68,4 @@ class SettingsViewModel : ViewModel() { | |||||||
|     fun setAdapterItemChanged(value: Int) { |     fun setAdapterItemChanged(value: Int) { | ||||||
|         _adapterItemChanged.value = value |         _adapterItemChanged.value = value | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun clear() { |  | ||||||
|         game = null |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -28,12 +28,9 @@ import androidx.navigation.ui.setupWithNavController | |||||||
| import androidx.preference.PreferenceManager | import androidx.preference.PreferenceManager | ||||||
| import com.google.android.material.color.MaterialColors | import com.google.android.material.color.MaterialColors | ||||||
| import com.google.android.material.navigation.NavigationBarView | import com.google.android.material.navigation.NavigationBarView | ||||||
| import kotlinx.coroutines.CoroutineScope |  | ||||||
| import java.io.File | import java.io.File | ||||||
| import java.io.FilenameFilter | import java.io.FilenameFilter | ||||||
| import kotlinx.coroutines.Dispatchers |  | ||||||
| import kotlinx.coroutines.launch | import kotlinx.coroutines.launch | ||||||
| import kotlinx.coroutines.withContext |  | ||||||
| import org.yuzu.yuzu_emu.HomeNavigationDirections | import org.yuzu.yuzu_emu.HomeNavigationDirections | ||||||
| import org.yuzu.yuzu_emu.NativeLibrary | import org.yuzu.yuzu_emu.NativeLibrary | ||||||
| import org.yuzu.yuzu_emu.R | import org.yuzu.yuzu_emu.R | ||||||
| @@ -258,13 +255,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||||||
|         super.onResume() |         super.onResume() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override fun onStop() { |  | ||||||
|         super.onStop() |  | ||||||
|         CoroutineScope(Dispatchers.IO).launch { |  | ||||||
|             NativeConfig.saveSettings() |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun onDestroy() { |     override fun onDestroy() { | ||||||
|         EmulationActivity.stopForegroundService(this) |         EmulationActivity.stopForegroundService(this) | ||||||
|         super.onDestroy() |         super.onDestroy() | ||||||
| @@ -677,7 +667,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 // Clear existing user data |                 // Clear existing user data | ||||||
|                 NativeConfig.unloadConfig() |                 NativeConfig.unloadGlobalConfig() | ||||||
|                 File(DirectoryInitialization.userDirectory!!).deleteRecursively() |                 File(DirectoryInitialization.userDirectory!!).deleteRecursively() | ||||||
|  |  | ||||||
|                 // Copy archive to internal storage |                 // Copy archive to internal storage | ||||||
| @@ -696,7 +686,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||||||
|  |  | ||||||
|                 // Reinitialize relevant data |                 // Reinitialize relevant data | ||||||
|                 NativeLibrary.initializeSystem(true) |                 NativeLibrary.initializeSystem(true) | ||||||
|                 NativeConfig.initializeConfig() |                 NativeConfig.initializeGlobalConfig() | ||||||
|                 gamesViewModel.reloadGames(false) |                 gamesViewModel.reloadGames(false) | ||||||
|  |  | ||||||
|                 return@newInstance getString(R.string.user_data_import_success) |                 return@newInstance getString(R.string.user_data_import_success) | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ object DirectoryInitialization { | |||||||
|         if (!areDirectoriesReady) { |         if (!areDirectoriesReady) { | ||||||
|             initializeInternalStorage() |             initializeInternalStorage() | ||||||
|             NativeLibrary.initializeSystem(false) |             NativeLibrary.initializeSystem(false) | ||||||
|             NativeConfig.initializeConfig() |             NativeConfig.initializeGlobalConfig() | ||||||
|             areDirectoriesReady = true |             areDirectoriesReady = true | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -7,30 +7,54 @@ import org.yuzu.yuzu_emu.model.GameDir | |||||||
|  |  | ||||||
| object NativeConfig { | object NativeConfig { | ||||||
|     /** |     /** | ||||||
|      * Creates a Config object and opens the emulation config. |      * Loads global config. | ||||||
|      */ |      */ | ||||||
|     @Synchronized |     @Synchronized | ||||||
|     external fun initializeConfig() |     external fun initializeGlobalConfig() | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Destroys the stored config object. This automatically saves the existing config. |      * Destroys the stored global config object. This does not save the existing config. | ||||||
|      */ |      */ | ||||||
|     @Synchronized |     @Synchronized | ||||||
|     external fun unloadConfig() |     external fun unloadGlobalConfig() | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Reads values saved to the config file and saves them. |      * Reads values in the global config file and saves them. | ||||||
|      */ |      */ | ||||||
|     @Synchronized |     @Synchronized | ||||||
|     external fun reloadSettings() |     external fun reloadGlobalConfig() | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Saves settings values in memory to disk. |      * Saves global settings values in memory to disk. | ||||||
|      */ |      */ | ||||||
|     @Synchronized |     @Synchronized | ||||||
|     external fun saveSettings() |     external fun saveGlobalConfig() | ||||||
|  |  | ||||||
|     external fun getBoolean(key: String, getDefault: Boolean): Boolean |     /** | ||||||
|  |      * Creates per-game config for the specified parameters. Must be unloaded once per-game config | ||||||
|  |      * is closed with [unloadPerGameConfig]. All switchable values that [NativeConfig] gets/sets | ||||||
|  |      * will follow the per-game config until the global config is reloaded. | ||||||
|  |      * | ||||||
|  |      * @param programId String representation of the u64 programId | ||||||
|  |      * @param fileName Filename of the game, including its extension | ||||||
|  |      */ | ||||||
|  |     @Synchronized | ||||||
|  |     external fun initializePerGameConfig(programId: String, fileName: String) | ||||||
|  |  | ||||||
|  |     @Synchronized | ||||||
|  |     external fun isPerGameConfigLoaded(): Boolean | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Saves per-game settings values in memory to disk. | ||||||
|  |      */ | ||||||
|  |     @Synchronized | ||||||
|  |     external fun savePerGameConfig() | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Destroys the stored per-game config object. This does not save the config. | ||||||
|  |      */ | ||||||
|  |     @Synchronized | ||||||
|  |     external fun unloadPerGameConfig() | ||||||
|  |  | ||||||
|     @Synchronized |     @Synchronized | ||||||
|     external fun getBoolean(key: String, needsGlobal: Boolean): Boolean |     external fun getBoolean(key: String, needsGlobal: Boolean): Boolean | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ | |||||||
|  |  | ||||||
| #include <string> | #include <string> | ||||||
|  |  | ||||||
|  | #include <common/fs/fs_util.h> | ||||||
| #include <jni.h> | #include <jni.h> | ||||||
|  |  | ||||||
| #include "android_config.h" | #include "android_config.h" | ||||||
| @@ -12,17 +13,19 @@ | |||||||
| #include "frontend_common/config.h" | #include "frontend_common/config.h" | ||||||
| #include "jni/android_common/android_common.h" | #include "jni/android_common/android_common.h" | ||||||
| #include "jni/id_cache.h" | #include "jni/id_cache.h" | ||||||
|  | #include "native.h" | ||||||
|  |  | ||||||
| std::unique_ptr<AndroidConfig> config; | std::unique_ptr<AndroidConfig> global_config; | ||||||
|  | std::unique_ptr<AndroidConfig> per_game_config; | ||||||
|  |  | ||||||
| template <typename T> | template <typename T> | ||||||
| Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) { | Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) { | ||||||
|     auto key = GetJString(env, jkey); |     auto key = GetJString(env, jkey); | ||||||
|     auto basicSetting = Settings::values.linkage.by_key[key]; |     auto basicSetting = Settings::values.linkage.by_key[key]; | ||||||
|     auto basicAndroidSetting = AndroidSettings::values.linkage.by_key[key]; |  | ||||||
|     if (basicSetting != 0) { |     if (basicSetting != 0) { | ||||||
|         return static_cast<Settings::Setting<T>*>(basicSetting); |         return static_cast<Settings::Setting<T>*>(basicSetting); | ||||||
|     } |     } | ||||||
|  |     auto basicAndroidSetting = AndroidSettings::values.linkage.by_key[key]; | ||||||
|     if (basicAndroidSetting != 0) { |     if (basicAndroidSetting != 0) { | ||||||
|         return static_cast<Settings::Setting<T>*>(basicAndroidSetting); |         return static_cast<Settings::Setting<T>*>(basicAndroidSetting); | ||||||
|     } |     } | ||||||
| @@ -32,20 +35,43 @@ Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) { | |||||||
|  |  | ||||||
| extern "C" { | extern "C" { | ||||||
|  |  | ||||||
| void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeConfig(JNIEnv* env, jobject obj) { | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeGlobalConfig(JNIEnv* env, jobject obj) { | ||||||
|     config = std::make_unique<AndroidConfig>(); |     global_config = std::make_unique<AndroidConfig>(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadConfig(JNIEnv* env, jobject obj) { | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadGlobalConfig(JNIEnv* env, jobject obj) { | ||||||
|     config.reset(); |     global_config.reset(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_reloadSettings(JNIEnv* env, jobject obj) { | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_reloadGlobalConfig(JNIEnv* env, jobject obj) { | ||||||
|     config->AndroidConfig::ReloadAllValues(); |     global_config->AndroidConfig::ReloadAllValues(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveSettings(JNIEnv* env, jobject obj) { | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveGlobalConfig(JNIEnv* env, jobject obj) { | ||||||
|     config->AndroidConfig::SaveAllValues(); |     global_config->AndroidConfig::SaveAllValues(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializePerGameConfig(JNIEnv* env, jobject obj, | ||||||
|  |                                                                         jstring jprogramId, | ||||||
|  |                                                                         jstring jfileName) { | ||||||
|  |     auto program_id = EmulationSession::GetProgramId(env, jprogramId); | ||||||
|  |     auto file_name = GetJString(env, jfileName); | ||||||
|  |     const auto config_file_name = program_id == 0 ? file_name : fmt::format("{:016X}", program_id); | ||||||
|  |     per_game_config = | ||||||
|  |         std::make_unique<AndroidConfig>(config_file_name, Config::ConfigType::PerGameConfig); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_isPerGameConfigLoaded(JNIEnv* env, | ||||||
|  |                                                                           jobject obj) { | ||||||
|  |     return per_game_config != nullptr; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_savePerGameConfig(JNIEnv* env, jobject obj) { | ||||||
|  |     per_game_config->AndroidConfig::SaveAllValues(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadPerGameConfig(JNIEnv* env, jobject obj) { | ||||||
|  |     per_game_config.reset(); | ||||||
| } | } | ||||||
|  |  | ||||||
| jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getBoolean(JNIEnv* env, jobject obj, | jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getBoolean(JNIEnv* env, jobject obj, | ||||||
|   | |||||||
| @@ -62,6 +62,16 @@ | |||||||
|                 android:textSize="13sp" |                 android:textSize="13sp" | ||||||
|                 tools:text="1x" /> |                 tools:text="1x" /> | ||||||
|  |  | ||||||
|  |             <com.google.android.material.button.MaterialButton | ||||||
|  |                 android:id="@+id/button_clear" | ||||||
|  |                 style="@style/Widget.Material3.Button.TonalButton" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_marginTop="16dp" | ||||||
|  |                 android:visibility="gone" | ||||||
|  |                 android:text="@string/clear" | ||||||
|  |                 tools:visibility="visible" /> | ||||||
|  |  | ||||||
|         </LinearLayout> |         </LinearLayout> | ||||||
|  |  | ||||||
|     </LinearLayout> |     </LinearLayout> | ||||||
|   | |||||||
| @@ -10,41 +10,62 @@ | |||||||
|     android:minHeight="72dp" |     android:minHeight="72dp" | ||||||
|     android:padding="16dp"> |     android:padding="16dp"> | ||||||
|  |  | ||||||
|     <com.google.android.material.materialswitch.MaterialSwitch |  | ||||||
|         android:id="@+id/switch_widget" |  | ||||||
|         android:layout_width="wrap_content" |  | ||||||
|         android:layout_height="wrap_content" |  | ||||||
|         android:layout_alignParentEnd="true" |  | ||||||
|         android:layout_centerVertical="true" /> |  | ||||||
|  |  | ||||||
|     <LinearLayout |     <LinearLayout | ||||||
|         android:layout_width="match_parent" |         android:layout_width="match_parent" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:layout_alignParentTop="true" |  | ||||||
|         android:layout_centerVertical="true" |  | ||||||
|         android:layout_marginEnd="24dp" |  | ||||||
|         android:layout_toStartOf="@+id/switch_widget" |  | ||||||
|         android:gravity="center_vertical" |  | ||||||
|         android:orientation="vertical"> |         android:orientation="vertical"> | ||||||
|  |  | ||||||
|         <com.google.android.material.textview.MaterialTextView |         <LinearLayout | ||||||
|             android:id="@+id/text_setting_name" |             android:layout_width="match_parent" | ||||||
|             style="@style/TextAppearance.Material3.HeadlineMedium" |  | ||||||
|             android:layout_width="wrap_content" |  | ||||||
|             android:layout_height="wrap_content" |             android:layout_height="wrap_content" | ||||||
|             android:textAlignment="viewStart" |             android:orientation="horizontal"> | ||||||
|             android:textSize="17sp" |  | ||||||
|             app:lineHeight="28dp" |  | ||||||
|             tools:text="@string/frame_limit_enable" /> |  | ||||||
|  |  | ||||||
|         <com.google.android.material.textview.MaterialTextView |             <LinearLayout | ||||||
|             android:id="@+id/text_setting_description" |                 android:layout_width="0dp" | ||||||
|             style="@style/TextAppearance.Material3.BodySmall" |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_marginEnd="24dp" | ||||||
|  |                 android:gravity="center_vertical" | ||||||
|  |                 android:orientation="vertical" | ||||||
|  |                 android:layout_weight="1"> | ||||||
|  |  | ||||||
|  |                 <com.google.android.material.textview.MaterialTextView | ||||||
|  |                     android:id="@+id/text_setting_name" | ||||||
|  |                     style="@style/TextAppearance.Material3.HeadlineMedium" | ||||||
|  |                     android:layout_width="wrap_content" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:textAlignment="viewStart" | ||||||
|  |                     android:textSize="17sp" | ||||||
|  |                     app:lineHeight="28dp" | ||||||
|  |                     tools:text="@string/frame_limit_enable" /> | ||||||
|  |  | ||||||
|  |                 <com.google.android.material.textview.MaterialTextView | ||||||
|  |                     android:id="@+id/text_setting_description" | ||||||
|  |                     style="@style/TextAppearance.Material3.BodySmall" | ||||||
|  |                     android:layout_width="wrap_content" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:layout_marginTop="@dimen/spacing_small" | ||||||
|  |                     android:textAlignment="viewStart" | ||||||
|  |                     tools:text="@string/frame_limit_enable_description" /> | ||||||
|  |  | ||||||
|  |             </LinearLayout> | ||||||
|  |  | ||||||
|  |             <com.google.android.material.materialswitch.MaterialSwitch | ||||||
|  |                 android:id="@+id/switch_widget" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_gravity="center_vertical"/> | ||||||
|  |  | ||||||
|  |         </LinearLayout> | ||||||
|  |  | ||||||
|  |         <com.google.android.material.button.MaterialButton | ||||||
|  |             android:id="@+id/button_clear" | ||||||
|  |             style="@style/Widget.Material3.Button.TonalButton" | ||||||
|             android:layout_width="wrap_content" |             android:layout_width="wrap_content" | ||||||
|             android:layout_height="wrap_content" |             android:layout_height="wrap_content" | ||||||
|             android:layout_marginTop="@dimen/spacing_small" |             android:layout_marginTop="16dp" | ||||||
|             android:textAlignment="viewStart" |             android:text="@string/clear" | ||||||
|             tools:text="@string/frame_limit_enable_description" /> |             android:visibility="gone" | ||||||
|  |             tools:visibility="visible" /> | ||||||
|  |  | ||||||
|     </LinearLayout> |     </LinearLayout> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,6 +11,11 @@ | |||||||
|         android:icon="@drawable/ic_settings" |         android:icon="@drawable/ic_settings" | ||||||
|         android:title="@string/preferences_settings" /> |         android:title="@string/preferences_settings" /> | ||||||
|  |  | ||||||
|  |     <item | ||||||
|  |         android:id="@+id/menu_settings_per_game" | ||||||
|  |         android:icon="@drawable/ic_settings_outline" | ||||||
|  |         android:title="@string/per_game_settings" /> | ||||||
|  |  | ||||||
|     <item |     <item | ||||||
|         android:id="@+id/menu_overlay_controls" |         android:id="@+id/menu_overlay_controls" | ||||||
|         android:icon="@drawable/ic_controller" |         android:icon="@drawable/ic_controller" | ||||||
|   | |||||||
| @@ -15,6 +15,10 @@ | |||||||
|             app:argType="org.yuzu.yuzu_emu.model.Game" |             app:argType="org.yuzu.yuzu_emu.model.Game" | ||||||
|             app:nullable="true" |             app:nullable="true" | ||||||
|             android:defaultValue="@null" /> |             android:defaultValue="@null" /> | ||||||
|  |         <argument | ||||||
|  |             android:name="custom" | ||||||
|  |             app:argType="boolean" | ||||||
|  |             android:defaultValue="false" /> | ||||||
|     </fragment> |     </fragment> | ||||||
|  |  | ||||||
|     <activity |     <activity | ||||||
|   | |||||||
| @@ -77,6 +77,10 @@ | |||||||
|             app:argType="org.yuzu.yuzu_emu.model.Game" |             app:argType="org.yuzu.yuzu_emu.model.Game" | ||||||
|             app:nullable="true" |             app:nullable="true" | ||||||
|             android:defaultValue="@null" /> |             android:defaultValue="@null" /> | ||||||
|  |         <argument | ||||||
|  |             android:name="custom" | ||||||
|  |             app:argType="boolean" | ||||||
|  |             android:defaultValue="false" /> | ||||||
|     </activity> |     </activity> | ||||||
|  |  | ||||||
|     <action |     <action | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user