diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt index 105f49ab8..e63960fa8 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt @@ -12,6 +12,7 @@ import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.R import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.IntSetting +import org.citra.citra_emu.features.settings.model.IntListSetting import org.citra.citra_emu.features.settings.model.Settings import org.citra.citra_emu.features.settings.utils.SettingsFile import org.citra.citra_emu.utils.EmulationMenuSettings @@ -31,8 +32,16 @@ class ScreenAdjustmentUtil( BooleanSetting.SWAP_SCREEN.boolean = isEnabled settings.saveSetting(BooleanSetting.SWAP_SCREEN, SettingsFile.FILE_NAME_CONFIG) } + fun cycleLayouts() { - val landscapeValues = context.resources.getIntArray(R.array.landscapeValues) + + val landscapeLayoutsToCycle = IntListSetting.LAYOUTS_TO_CYCLE.list; + val landscapeValues = + if (landscapeLayoutsToCycle.isNotEmpty()) + landscapeLayoutsToCycle.toIntArray() + else context.resources.getIntArray( + R.array.landscapeValues + ) val portraitValues = context.resources.getIntArray(R.array.portraitValues) if (NativeLibrary.isPortraitMode) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractListSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractListSetting.kt new file mode 100644 index 000000000..33d0b85cf --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractListSetting.kt @@ -0,0 +1,9 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +interface AbstractListSetting : AbstractSetting { + var list: List +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntListSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntListSetting.kt new file mode 100644 index 000000000..9d78b5cdd --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntListSetting.kt @@ -0,0 +1,39 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +enum class IntListSetting( + override val key: String, + override val section: String, + override val defaultValue: List +) : AbstractListSetting { + LAYOUTS_TO_CYCLE("layouts_to_cycle", Settings.SECTION_LAYOUT, listOf(0,1,2,3,4,5)); + + override var list: List = defaultValue + + override val valueAsString: String + get() = list.joinToString() + + + 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:List = listOf(); + + + fun from(key: String): IntListSetting? = + values().firstOrNull { it.key == key } + + fun clear() = values().forEach { it.list = it.defaultValue } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/MultiChoiceSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/MultiChoiceSetting.kt new file mode 100644 index 000000000..20786af7f --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/MultiChoiceSetting.kt @@ -0,0 +1,46 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model.view +import org.citra.citra_emu.features.settings.model.AbstractSetting +import org.citra.citra_emu.features.settings.model.IntListSetting +class MultiChoiceSetting( + setting: AbstractSetting?, + titleId: Int, + descriptionId: Int, + val choicesId: Int, + val valuesId: Int, + val key: String? = null, + val defaultValue: List? = null, + override var isEnabled: Boolean = true +) : SettingsItem(setting, titleId, descriptionId) { + override val type = TYPE_MULTI_CHOICE + + val selectedValues: List + get() { + if (setting == null) { + return defaultValue!! + } + try { + val setting = setting as IntListSetting + return setting.list + }catch (_: ClassCastException) { + } + return defaultValue!! + } + + /** + * Write a value to the backing list. 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: List): IntListSetting { + val intSetting = setting as IntListSetting + intSetting.list = selection + return intSetting + } + +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt index c3f11def5..68aa2226c 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt @@ -47,5 +47,6 @@ abstract class SettingsItem( const val TYPE_INPUT_BINDING = 8 const val TYPE_STRING_INPUT = 9 const val TYPE_FLOAT_INPUT = 10 + const val TYPE_MULTI_CHOICE = 11 } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt index bc55bd5d6..233637e12 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt @@ -41,12 +41,14 @@ import org.citra.citra_emu.features.settings.model.AbstractIntSetting import org.citra.citra_emu.features.settings.model.AbstractSetting import org.citra.citra_emu.features.settings.model.AbstractStringSetting import org.citra.citra_emu.features.settings.model.FloatSetting +import org.citra.citra_emu.features.settings.model.IntListSetting import org.citra.citra_emu.features.settings.model.ScaledFloatSetting import org.citra.citra_emu.features.settings.model.AbstractShortSetting import org.citra.citra_emu.features.settings.model.view.DateTimeSetting import org.citra.citra_emu.features.settings.model.view.InputBindingSetting import org.citra.citra_emu.features.settings.model.view.SettingsItem import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting +import org.citra.citra_emu.features.settings.model.view.MultiChoiceSetting import org.citra.citra_emu.features.settings.model.view.SliderSetting import org.citra.citra_emu.features.settings.model.view.StringInputSetting import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting @@ -55,6 +57,7 @@ import org.citra.citra_emu.features.settings.model.view.SwitchSetting import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder +import org.citra.citra_emu.features.settings.ui.viewholder.MultiChoiceViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.RunnableViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder @@ -72,7 +75,8 @@ import kotlin.math.roundToInt class SettingsAdapter( private val fragmentView: SettingsFragmentView, public val context: Context -) : RecyclerView.Adapter(), DialogInterface.OnClickListener { +) : RecyclerView.Adapter(), DialogInterface.OnClickListener, + DialogInterface.OnMultiChoiceClickListener { private var settings: ArrayList? = null private var clickedItem: SettingsItem? = null private var clickedPosition: Int @@ -104,6 +108,10 @@ class SettingsAdapter( SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this) } + SettingsItem.TYPE_MULTI_CHOICE -> { + MultiChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this) + } + SettingsItem.TYPE_SLIDER -> { SliderViewHolder(ListItemSettingBinding.inflate(inflater), this) } @@ -181,21 +189,30 @@ class SettingsAdapter( 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_MULTI_CHOICE -> { + (oldItem as MultiChoiceSetting).isEnabled == (newItem as MultiChoiceSetting).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 } @@ -214,7 +231,7 @@ class SettingsAdapter( // 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 + // Reload the settings list to update the UI fragmentView.loadSettingsList() } @@ -232,6 +249,21 @@ class SettingsAdapter( onSingleChoiceClick(item) } + private fun onMultiChoiceClick(item: MultiChoiceSetting) { + clickedItem = item + + val value: BooleanArray = getSelectionForMultiChoiceValue(item); + dialog = MaterialAlertDialogBuilder(context) + .setTitle(item.nameId) + .setMultiChoiceItems(item.choicesId, value, this) + .show() + } + + fun onMultiChoiceClick(item: MultiChoiceSetting, position: Int) { + clickedPosition = position + onMultiChoiceClick(item) + } + private fun onStringSingleChoiceClick(item: StringSingleChoiceSetting) { clickedItem = item dialog = context?.let { @@ -360,14 +392,14 @@ class SettingsAdapter( sliderString = sliderProgress.roundToInt().toString() if (textSliderValue?.text.toString() != sliderString) { textSliderValue?.setText(sliderString) - textSliderValue?.setSelection(textSliderValue?.length() ?: 0 ) + textSliderValue?.setSelection(textSliderValue?.length() ?: 0) } } else { val currentText = textSliderValue?.text.toString() val currentTextValue = currentText.toFloat() if (currentTextValue != sliderProgress) { textSliderValue?.setText(sliderString) - textSliderValue?.setSelection(textSliderValue?.length() ?: 0 ) + textSliderValue?.setSelection(textSliderValue?.length() ?: 0) } } } @@ -447,6 +479,7 @@ class SettingsAdapter( } it.setSelectedValue(value) } + is AbstractShortSetting -> { val value = getValueForSingleChoiceSelection(it, which).toShort() if (it.selectedValue.toShort() != value) { @@ -454,6 +487,7 @@ class SettingsAdapter( } it.setSelectedValue(value) } + else -> throw IllegalStateException("Unrecognized type used for SingleChoiceSetting!") } fragmentView?.putSetting(setting) @@ -499,11 +533,12 @@ class SettingsAdapter( val setting = it.setSelectedValue(value) fragmentView?.putSetting(setting) } + else -> { val setting = it.setSelectedValue(sliderProgress) fragmentView?.putSetting(setting) } - } + } fragmentView.loadSettingsList() closeDialog() } @@ -519,7 +554,7 @@ class SettingsAdapter( fragmentView?.putSetting(setting) fragmentView.loadSettingsList() closeDialog() - } + } } } clickedItem = null @@ -527,6 +562,21 @@ class SettingsAdapter( textInputValue = "" } + //onclick for multichoice + override fun onClick(dialog: DialogInterface?, which: Int, isChecked: Boolean) { + val mcsetting = clickedItem as? MultiChoiceSetting + mcsetting?.let { + val value = getValueForMultiChoiceSelection(it, which) + if (it.selectedValues.contains(value) != isChecked) { + val setting = it.setSelectedValue((if (isChecked) it.selectedValues + value else it.selectedValues - value).sorted()) + fragmentView?.putSetting(setting) + fragmentView?.onSettingChanged() + } + fragmentView.loadSettingsList() + } + } + + fun onLongClick(setting: AbstractSetting, position: Int): Boolean { MaterialAlertDialogBuilder(context) .setMessage(R.string.reset_setting_confirmation) @@ -616,6 +666,16 @@ class SettingsAdapter( } } + private fun getValueForMultiChoiceSelection(item: MultiChoiceSetting, which: Int): Int { + val valuesId = item.valuesId + return if (valuesId > 0) { + val valuesArray = context.resources.getIntArray(valuesId) + valuesArray[which] + } else { + which + } + } + private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int { val value = item.selectedValue val valuesId = item.valuesId @@ -632,4 +692,20 @@ class SettingsAdapter( } return -1 } + + private fun getSelectionForMultiChoiceValue(item: MultiChoiceSetting): BooleanArray { + val value = item.selectedValues; + val valuesId = item.valuesId; + if (valuesId > 0) { + val valuesArray = context.resources.getIntArray(valuesId); + val res = BooleanArray(valuesArray.size){false} + for (index in valuesArray.indices) { + if (value.contains(valuesArray[index])) { + res[index] = true; + } + } + return res; + } + return BooleanArray(1){false}; + } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index d4baf6166..b65622b03 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -14,6 +14,7 @@ import android.os.Build import android.text.TextUtils import androidx.preference.PreferenceManager import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.serialization.builtins.IntArraySerializer import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.R import org.citra.citra_emu.features.settings.model.AbstractBooleanSetting @@ -24,12 +25,14 @@ import org.citra.citra_emu.features.settings.model.AbstractStringSetting import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.FloatSetting import org.citra.citra_emu.features.settings.model.IntSetting +import org.citra.citra_emu.features.settings.model.IntListSetting import org.citra.citra_emu.features.settings.model.ScaledFloatSetting import org.citra.citra_emu.features.settings.model.Settings import org.citra.citra_emu.features.settings.model.StringSetting import org.citra.citra_emu.features.settings.model.view.DateTimeSetting import org.citra.citra_emu.features.settings.model.view.HeaderSetting import org.citra.citra_emu.features.settings.model.view.InputBindingSetting +import org.citra.citra_emu.features.settings.model.view.MultiChoiceSetting import org.citra.citra_emu.features.settings.model.view.RunnableSetting import org.citra.citra_emu.features.settings.model.view.SettingsItem import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting @@ -1106,6 +1109,17 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) BooleanSetting.UPRIGHT_SCREEN.defaultValue ) ) + add( + MultiChoiceSetting( + IntListSetting.LAYOUTS_TO_CYCLE, + R.string.layouts_to_cycle, + R.string.layouts_to_cycle_description, + R.array.landscapeLayouts, + R.array.landscapeLayoutValues, + IntListSetting.LAYOUTS_TO_CYCLE.key, + IntListSetting.LAYOUTS_TO_CYCLE.defaultValue + ) + ) add( SingleChoiceSetting( IntSetting.PORTRAIT_SCREEN_LAYOUT, diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt new file mode 100644 index 000000000..8493115a4 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt @@ -0,0 +1,80 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui.viewholder + +import android.view.View +import org.citra.citra_emu.databinding.ListItemSettingBinding +import org.citra.citra_emu.features.settings.model.view.SettingsItem +import org.citra.citra_emu.features.settings.model.view.MultiChoiceSetting +import org.citra.citra_emu.features.settings.ui.SettingsAdapter + +class MultiChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : + SettingViewHolder(binding.root, adapter) { + private lateinit var setting: SettingsItem + + override fun bind(item: SettingsItem) { + setting = item + binding.textSettingName.setText(item.nameId) + if (item.descriptionId != 0) { + binding.textSettingDescription.visibility = View.VISIBLE + binding.textSettingDescription.setText(item.descriptionId) + } else { + binding.textSettingDescription.visibility = View.GONE + } + binding.textSettingValue.visibility = View.VISIBLE + binding.textSettingValue.text = getTextSetting() + + 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 + } + } + + private fun getTextSetting(): String { + when (val item = setting) { + is MultiChoiceSetting -> { + val resMgr = binding.textSettingDescription.context.resources + val values = resMgr.getIntArray(item.valuesId) + var resList:List = emptyList(); + values.forEachIndexed { i: Int, value: Int -> + if ((setting as MultiChoiceSetting).selectedValues.contains(value)) { + resList = resList + resMgr.getStringArray(item.choicesId)[i]; + } + } + return resList.joinToString(); + } + + else -> return "" + } + } + + override fun onClick(clicked: View) { + if (!setting.isEditable || !setting.isEnabled) { + adapter.onClickDisabledSetting(!setting.isEditable) + return + } + + if (setting is MultiChoiceSetting) { + adapter.onMultiChoiceClick( + (setting as MultiChoiceSetting), + bindingAdapterPosition + ) + } + } + + override fun onLongClick(clicked: View): Boolean { + if (setting.isActive) { + return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) + } else { + adapter.onClickDisabledSetting(!setting.isEditable) + } + return false + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt index dec3e4e0a..6c3cd8265 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt @@ -12,6 +12,7 @@ import org.citra.citra_emu.R import org.citra.citra_emu.features.settings.model.AbstractSetting import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.FloatSetting +import org.citra.citra_emu.features.settings.model.IntListSetting import org.citra.citra_emu.features.settings.model.IntSetting import org.citra.citra_emu.features.settings.model.ScaledFloatSetting import org.citra.citra_emu.features.settings.model.SettingSection @@ -255,6 +256,11 @@ object SettingsFile { return stringSetting } + val intListSetting = IntListSetting.from(key) + if (intListSetting != null) { + intListSetting.list = value.split(", ").map { it.toInt() } + } + return null } diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index f60fdd0e3..b1b3b5856 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -341,6 +341,8 @@ Reverse Landscape Portrait Reverse Portrait + Layouts to Cycle + Which layouts are cycled by the Cycle Layout hotkey Default 16:9 4:3