android: Add isEnabled setting item conditional check (#814)

* android: Add `isEnabled` setting item conditional check

Co-authored-by: Charles Lombardo <clombardo169@gmail.com>
(Thanks to him for the idea of using DiffUtil)

Now it is possible to have a conditional check for each setting type which once met will disable itself and re-enable once the condition is unmet again in real-time

* Refactor setting checks to deduplicate repeated `isEditable && isEnabled` conditionals

This is done by adding a new value, `setting.isActive` which is equivalent to `setting.isEditable && setting.isEnabled`

* Removed seemingly redundant `isEnabled` overrides

* Updated license headers

---------

Co-authored-by: Kleidis <167202775+kleidis@users.noreply.github.com>
This commit is contained in:
OpenSauce 2025-04-11 14:35:21 +01:00 committed by GitHub
parent c69b642f54
commit 12bc825b8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 133 additions and 45 deletions

View file

@ -1,4 +1,4 @@
// Copyright Citra Emulator Project / Lime3DS Emulator Project // Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
@ -28,6 +28,13 @@ abstract class SettingsItem(
return setting?.isRuntimeEditable ?: false return setting?.isRuntimeEditable ?: false
} }
open var isEnabled: Boolean = true
val isActive: Boolean
get() {
return this.isEditable && this.isEnabled
}
companion object { companion object {
const val TYPE_HEADER = 0 const val TYPE_HEADER = 0
const val TYPE_SWITCH = 1 const val TYPE_SWITCH = 1

View file

@ -7,7 +7,6 @@ package org.citra.citra_emu.features.settings.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.graphics.Color
import android.icu.util.Calendar import android.icu.util.Calendar
import android.icu.util.TimeZone import android.icu.util.TimeZone
import android.text.Editable import android.text.Editable
@ -17,11 +16,11 @@ import android.text.TextWatcher
import android.text.format.DateFormat import android.text.format.DateFormat
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.EditText
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -66,7 +65,6 @@ import org.citra.citra_emu.features.settings.ui.viewholder.SwitchSettingViewHold
import org.citra.citra_emu.fragments.MessageDialogFragment import org.citra.citra_emu.fragments.MessageDialogFragment
import org.citra.citra_emu.fragments.MotionBottomSheetDialogFragment import org.citra.citra_emu.fragments.MotionBottomSheetDialogFragment
import org.citra.citra_emu.utils.SystemSaveGame import org.citra.citra_emu.utils.SystemSaveGame
import java.lang.IllegalStateException
import java.lang.NumberFormatException import java.lang.NumberFormatException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -153,15 +151,71 @@ class SettingsAdapter(
return getItem(position)?.type ?: -1 return getItem(position)?.type ?: -1
} }
fun setSettingsList(settings: ArrayList<SettingsItem>?) { fun setSettingsList(newSettings: ArrayList<SettingsItem>?) {
this.settings = settings ?: arrayListOf() if (settings == null) {
notifyDataSetChanged() settings = newSettings ?: arrayListOf()
notifyDataSetChanged()
return
}
val oldSettings = settings
val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize() = oldSettings?.size ?: 0
override fun getNewListSize() = newSettings?.size ?: 0
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldSettings?.get(oldItemPosition)?.setting
val newItem = newSettings?.get(newItemPosition)?.setting
return oldItem?.key == newItem?.key
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldSettings?.get(oldItemPosition)
val newItem = newSettings?.get(newItemPosition)
if (oldItem == null || newItem == null || oldItem.type != newItem.type) {
return false
}
return when (oldItem.type) {
SettingsItem.TYPE_SLIDER -> {
(oldItem as SliderSetting).isEnabled == (newItem as SliderSetting).isEnabled
}
SettingsItem.TYPE_SWITCH -> {
(oldItem as SwitchSetting).isEnabled == (newItem as SwitchSetting).isEnabled
}
SettingsItem.TYPE_SINGLE_CHOICE -> {
(oldItem as SingleChoiceSetting).isEnabled == (newItem as SingleChoiceSetting).isEnabled
}
SettingsItem.TYPE_DATETIME_SETTING -> {
(oldItem as DateTimeSetting).isEnabled == (newItem as DateTimeSetting).isEnabled
}
SettingsItem.TYPE_STRING_SINGLE_CHOICE -> {
(oldItem as StringSingleChoiceSetting).isEnabled == (newItem as StringSingleChoiceSetting).isEnabled
}
SettingsItem.TYPE_STRING_INPUT -> {
(oldItem as StringInputSetting).isEnabled == (newItem as StringInputSetting).isEnabled
}
else -> {
oldItem == newItem
}
}
}
})
settings = newSettings ?: arrayListOf()
diffResult.dispatchUpdatesTo(this)
} }
fun onBooleanClick(item: SwitchSetting, position: Int, checked: Boolean) { fun onBooleanClick(item: SwitchSetting, position: Int, checked: Boolean) {
val setting = item.setChecked(checked) val setting = item.setChecked(checked)
fragmentView.putSetting(setting) fragmentView.putSetting(setting)
fragmentView.onSettingChanged() fragmentView.onSettingChanged()
// If statement is required otherwise the app will crash on activity recreate ex. theme settings
if (fragmentView.activityView != null)
// Reload the settings list to update the UI
fragmentView.loadSettingsList()
} }
private fun onSingleChoiceClick(item: SingleChoiceSetting) { private fun onSingleChoiceClick(item: SingleChoiceSetting) {
@ -247,6 +301,7 @@ class SettingsAdapter(
notifyItemChanged(clickedPosition) notifyItemChanged(clickedPosition)
val setting = item.setSelectedValue(rtcString) val setting = item.setSelectedValue(rtcString)
fragmentView.putSetting(setting) fragmentView.putSetting(setting)
fragmentView.loadSettingsList()
clickedItem = null clickedItem = null
} }
datePicker.show( datePicker.show(
@ -402,6 +457,7 @@ class SettingsAdapter(
else -> throw IllegalStateException("Unrecognized type used for SingleChoiceSetting!") else -> throw IllegalStateException("Unrecognized type used for SingleChoiceSetting!")
} }
fragmentView?.putSetting(setting) fragmentView?.putSetting(setting)
fragmentView.loadSettingsList()
closeDialog() closeDialog()
} }
} }
@ -425,6 +481,7 @@ class SettingsAdapter(
} }
fragmentView?.putSetting(setting) fragmentView?.putSetting(setting)
fragmentView.loadSettingsList()
closeDialog() closeDialog()
} }
} }
@ -447,6 +504,7 @@ class SettingsAdapter(
fragmentView?.putSetting(setting) fragmentView?.putSetting(setting)
} }
} }
fragmentView.loadSettingsList()
closeDialog() closeDialog()
} }
} }
@ -459,6 +517,7 @@ class SettingsAdapter(
} }
val setting = it.setSelectedValue(textInputValue ?: "") val setting = it.setSelectedValue(textInputValue ?: "")
fragmentView?.putSetting(setting) fragmentView?.putSetting(setting)
fragmentView.loadSettingsList()
closeDialog() closeDialog()
} }
} }
@ -488,6 +547,7 @@ class SettingsAdapter(
} }
notifyItemChanged(position) notifyItemChanged(position)
fragmentView.onSettingChanged() fragmentView.onSettingChanged()
fragmentView.loadSettingsList()
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show() .show()
@ -495,10 +555,19 @@ class SettingsAdapter(
return true return true
} }
fun onClickDisabledSetting() { fun onClickDisabledSetting(isRuntimeDisabled: Boolean) {
MessageDialogFragment.newInstance( val titleId = if (isRuntimeDisabled)
R.string.setting_not_editable, R.string.setting_not_editable
else
R.string.setting_disabled
val messageId = if (isRuntimeDisabled)
R.string.setting_not_editable_description R.string.setting_not_editable_description
else
R.string.setting_disabled_description
MessageDialogFragment.newInstance(
titleId,
messageId
).show((fragmentView as SettingsFragment).childFragmentManager, MessageDialogFragment.TAG) ).show((fragmentView as SettingsFragment).childFragmentManager, MessageDialogFragment.TAG)
} }

View file

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project // Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
@ -47,7 +47,7 @@ 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)
if (setting.isEditable) { if (setting.isActive) {
binding.textSettingName.alpha = 1f binding.textSettingName.alpha = 1f
binding.textSettingDescription.alpha = 1f binding.textSettingDescription.alpha = 1f
binding.textSettingValue.alpha = 1f binding.textSettingValue.alpha = 1f
@ -59,18 +59,18 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
} }
override fun onClick(clicked: View) { override fun onClick(clicked: View) {
if (setting.isEditable) { if (setting.isActive) {
adapter.onDateTimeClick(setting, bindingAdapterPosition) adapter.onDateTimeClick(setting, bindingAdapterPosition)
} else { } else {
adapter.onClickDisabledSetting() adapter.onClickDisabledSetting(!setting.isEditable)
} }
} }
override fun onLongClick(clicked: View): Boolean { override fun onLongClick(clicked: View): Boolean {
if (setting.isEditable) { if (setting.isActive) {
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
} else { } else {
adapter.onClickDisabledSetting() adapter.onClickDisabledSetting(!setting.isEditable)
} }
return false return false
} }

View file

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project // Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
@ -45,7 +45,7 @@ class InputBindingSettingViewHolder(val binding: ListItemSettingBinding, adapter
if (setting.isEditable) { if (setting.isEditable) {
adapter.onInputBindingClick(setting, bindingAdapterPosition) adapter.onInputBindingClick(setting, bindingAdapterPosition)
} else { } else {
adapter.onClickDisabledSetting() adapter.onClickDisabledSetting(!setting.isEditable)
} }
} }
@ -53,7 +53,7 @@ class InputBindingSettingViewHolder(val binding: ListItemSettingBinding, adapter
if (setting.isEditable) { if (setting.isEditable) {
adapter.onLongClick(setting.setting!!, bindingAdapterPosition) adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
} else { } else {
adapter.onClickDisabledSetting() adapter.onClickDisabledSetting(!setting.isEditable)
} }
return false return false
} }

View file

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project // Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
@ -60,7 +60,7 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
override fun onClick(clicked: View) { override fun onClick(clicked: View) {
if (!setting.isRuntimeRunnable && EmulationActivity.isRunning()) { if (!setting.isRuntimeRunnable && EmulationActivity.isRunning()) {
adapter.onClickDisabledSetting() adapter.onClickDisabledSetting(true)
} else { } else {
setting.runnable.invoke() setting.runnable.invoke()
} }

View file

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project // Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
@ -27,7 +27,7 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
binding.textSettingValue.visibility = View.VISIBLE binding.textSettingValue.visibility = View.VISIBLE
binding.textSettingValue.text = getTextSetting() binding.textSettingValue.text = getTextSetting()
if (setting.isEditable) { if (setting.isActive) {
binding.textSettingName.alpha = 1f binding.textSettingName.alpha = 1f
binding.textSettingDescription.alpha = 1f binding.textSettingDescription.alpha = 1f
binding.textSettingValue.alpha = 1f binding.textSettingValue.alpha = 1f
@ -65,8 +65,8 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
} }
override fun onClick(clicked: View) { override fun onClick(clicked: View) {
if (!setting.isEditable) { if (!setting.isEditable || !setting.isEnabled) {
adapter.onClickDisabledSetting() adapter.onClickDisabledSetting(!setting.isEditable)
return return
} }
@ -84,10 +84,10 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
} }
override fun onLongClick(clicked: View): Boolean { override fun onLongClick(clicked: View): Boolean {
if (setting.isEditable) { if (setting.isActive) {
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
} else { } else {
adapter.onClickDisabledSetting() adapter.onClickDisabledSetting(!setting.isEditable)
} }
return false return false
} }

View file

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project // Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
@ -35,7 +35,7 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
else -> "${(setting.setting as AbstractIntSetting).int}${setting.units}" else -> "${(setting.setting as AbstractIntSetting).int}${setting.units}"
} }
if (setting.isEditable) { if (setting.isActive) {
binding.textSettingName.alpha = 1f binding.textSettingName.alpha = 1f
binding.textSettingDescription.alpha = 1f binding.textSettingDescription.alpha = 1f
binding.textSettingValue.alpha = 1f binding.textSettingValue.alpha = 1f
@ -47,18 +47,18 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
} }
override fun onClick(clicked: View) { override fun onClick(clicked: View) {
if (setting.isEditable) { if (setting.isActive) {
adapter.onSliderClick(setting, bindingAdapterPosition) adapter.onSliderClick(setting, bindingAdapterPosition)
} else { } else {
adapter.onClickDisabledSetting() adapter.onClickDisabledSetting(!setting.isEditable)
} }
} }
override fun onLongClick(clicked: View): Boolean { override fun onLongClick(clicked: View): Boolean {
if (setting.isEditable) { if (setting.isActive) {
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
} else { } else {
adapter.onClickDisabledSetting() adapter.onClickDisabledSetting(!setting.isEditable)
} }
return false return false
} }

View file

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project // Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
@ -25,21 +25,31 @@ class StringInputViewHolder(val binding: ListItemSettingBinding, adapter: Settin
} }
binding.textSettingValue.visibility = View.VISIBLE binding.textSettingValue.visibility = View.VISIBLE
binding.textSettingValue.text = setting.setting?.valueAsString binding.textSettingValue.text = setting.setting?.valueAsString
if (setting.isActive) {
binding.textSettingName.alpha = 1f
binding.textSettingDescription.alpha = 1f
binding.textSettingValue.alpha = 1f
} else {
binding.textSettingName.alpha = 0.5f
binding.textSettingDescription.alpha = 0.5f
binding.textSettingValue.alpha = 0.5f
}
} }
override fun onClick(clicked: View) { override fun onClick(clicked: View) {
if (!setting.isEditable) { if (!setting.isEditable || !setting.isEnabled) {
adapter.onClickDisabledSetting() adapter.onClickDisabledSetting(!setting.isEditable)
return return
} }
adapter.onStringInputClick((setting as StringInputSetting), bindingAdapterPosition) adapter.onStringInputClick((setting as StringInputSetting), bindingAdapterPosition)
} }
override fun onLongClick(clicked: View): Boolean { override fun onLongClick(clicked: View): Boolean {
if (setting.isEditable) { if (setting.isActive) {
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
} else { } else {
adapter.onClickDisabledSetting() adapter.onClickDisabledSetting(!setting.isEditable)
} }
return false return false
} }

View file

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project // Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
@ -33,26 +33,26 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
adapter.onBooleanClick(item, bindingAdapterPosition, binding.switchWidget.isChecked) adapter.onBooleanClick(item, bindingAdapterPosition, binding.switchWidget.isChecked)
} }
binding.switchWidget.isEnabled = setting.isEditable binding.switchWidget.isEnabled = setting.isActive
val textAlpha = if (setting.isEditable) 1f else 0.5f val textAlpha = if (setting.isActive) 1f else 0.5f
binding.textSettingName.alpha = textAlpha binding.textSettingName.alpha = textAlpha
binding.textSettingDescription.alpha = textAlpha binding.textSettingDescription.alpha = textAlpha
} }
override fun onClick(clicked: View) { override fun onClick(clicked: View) {
if (setting.isEditable) { if (setting.isActive) {
binding.switchWidget.toggle() binding.switchWidget.toggle()
} else { } else {
adapter.onClickDisabledSetting() adapter.onClickDisabledSetting(!setting.isEditable)
} }
} }
override fun onLongClick(clicked: View): Boolean { override fun onLongClick(clicked: View): Boolean {
if (setting.isEditable) { if (setting.isActive) {
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
} else { } else {
adapter.onClickDisabledSetting() adapter.onClickDisabledSetting(!setting.isEditable)
} }
return false return false
} }

View file

@ -332,6 +332,8 @@
<string name="select_rtc_time">Select RTC time</string> <string name="select_rtc_time">Select RTC time</string>
<string name="reset_setting_confirmation">Do you want to reset this setting back to its default value?</string> <string name="reset_setting_confirmation">Do you want to reset this setting back to its default value?</string>
<string name="setting_not_editable">You can\'t edit this now</string> <string name="setting_not_editable">You can\'t edit this now</string>
<string name="setting_disabled">Setting disabled</string>
<string name="setting_disabled_description">This setting is currently disabled due to another setting not being the appropriate value.</string>
<string name="setting_not_editable_description">This option can\'t be changed while a game is running.</string> <string name="setting_not_editable_description">This option can\'t be changed while a game is running.</string>
<string name="auto_select">Auto-Select</string> <string name="auto_select">Auto-Select</string>