skyline/app/src/main/java/emu/skyline/preference/IntegerListPreference.kt

276 lines
10 KiB
Kotlin

/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2021 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.preference
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.content.res.Resources
import android.content.res.TypedArray
import android.os.Bundle
import android.os.Parcel
import android.os.Parcelable
import android.util.AttributeSet
import androidx.annotation.ArrayRes
import androidx.appcompat.app.AlertDialog
import androidx.core.content.res.TypedArrayUtils
import androidx.preference.DialogPreference
import androidx.preference.PreferenceDialogFragmentCompat
import androidx.preference.R
import emu.skyline.di.getSettings
import emu.skyline.R as sR
/**
* A Preference that displays a list of strings in a dialog and saves an integer that corresponds to the selected entry (as specified by entryValues or the index of the selected entry)
* @see androidx.preference.ListPreference
*/
@SuppressLint("RestrictedApi")
class IntegerListPreference @JvmOverloads constructor(
context : Context,
attrs : AttributeSet? = null,
defStyleAttr : Int = TypedArrayUtils.getAttr(
context, R.attr.dialogPreferenceStyle,
R.attr.dialogPreferenceStyle
),
defStyleRes : Int = 0
) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {
/**
* The list of entries to be shown in the list in subsequent dialogs
*/
var entries : Array<CharSequence>?
/**
* The array to find the value to save for a preference when an entry from entries is
* selected. If a user clicks on the second item in entries, the second item in this array
* will be saved to the preference
*/
var entryValues : IntArray?
private var value : Int? = null
set(value) {
// Always persist/notify the first time
val changed = field != value
if (changed || !isValueSet) {
field = value
isValueSet = true
value?.let { persistInt(it) }
if (changed)
notifyChanged()
}
}
private var isValueSet = false
val refreshRequired : Boolean
init {
val res : Resources = context.resources
val styledAttrs = context.obtainStyledAttributes(attrs, sR.styleable.IntegerListPreference, defStyleAttr, defStyleRes)
entries = TypedArrayUtils.getTextArray(styledAttrs, sR.styleable.IntegerListPreference_entries, sR.styleable.IntegerListPreference_android_entries)
val entryValuesId = TypedArrayUtils.getResourceId(styledAttrs, sR.styleable.IntegerListPreference_android_entryValues, sR.styleable.IntegerListPreference_android_entryValues, 0)
entryValues = if (entryValuesId != 0) res.getIntArray(entryValuesId) else null
if (TypedArrayUtils.getBoolean(styledAttrs, sR.styleable.IntegerListPreference_useSimpleSummaryProvider, sR.styleable.IntegerListPreference_useSimpleSummaryProvider, false))
summaryProvider = SimpleSummaryProvider.instance
refreshRequired = TypedArrayUtils.getBoolean(styledAttrs, sR.styleable.IntegerListPreference_refreshRequired, sR.styleable.IntegerListPreference_refreshRequired, false)
styledAttrs.recycle()
}
/**
* @param entriesResId The entries array as a resource
*/
fun setEntries(@ArrayRes entriesResId : Int) {
entries = context.resources.getTextArray(entriesResId)
}
/**
* @param entryValuesResId The entry values array as a resource
*/
fun setEntryValues(@ArrayRes entryValuesResId : Int) {
entryValues = context.resources.getIntArray(entryValuesResId)
}
/**
* @return The entry corresponding to the current value, or `null`
*/
fun getEntry() : CharSequence? {
return entries?.getOrNull(getValueIndex())
}
/**
* @return The index of the value, or -1 if not found
*/
private fun findIndexOfValue(value : Int?) : Int {
entryValues?.let {
if (value != null) {
for (i in it.indices.reversed()) {
if (it[i] == value)
return i
}
}
}
return value ?: -1
}
/**
* Sets the value to the given index from the entry values
*/
fun setValueIndex(index : Int) {
value = entryValues?.get(index) ?: index
}
private fun getValueIndex() : Int {
return findIndexOfValue(value)
}
override fun onGetDefaultValue(a : TypedArray, index : Int) : Any? {
return a.getInt(index, 0)
}
override fun onSetInitialValue(defaultValue : Any?) {
// `Preference` superclass passes a null defaultValue if it is sure there is already a persisted value
// We have to account for that here by passing a random number as default value
value = if (defaultValue != null)
getPersistedInt(defaultValue as Int)
else
getPersistedInt(0)
}
override fun onSaveInstanceState() : Parcelable? {
val superState = super.onSaveInstanceState()
if (isPersistent)
// No need to save instance state since it's persistent
return superState
val myState = SavedState(superState)
myState.value = value
return myState
}
override fun onRestoreInstanceState(state : Parcelable?) {
if (state == null || state.javaClass != SavedState::class.java) {
// Didn't save state for us in onSaveInstanceState
super.onRestoreInstanceState(state)
return
}
val myState = state as SavedState
super.onRestoreInstanceState(myState.superState)
value = myState.value
}
private class SavedState : BaseSavedState {
var value : Int? = null
constructor(source : Parcel) : super(source) {
value = source.readSerializable() as Int?
}
constructor(superState : Parcelable?) : super(superState)
override fun writeToParcel(dest : Parcel, flags : Int) {
super.writeToParcel(dest, flags)
dest.writeSerializable(value)
}
}
/**
* A simple [androidx.preference.Preference.SummaryProvider] implementation
* If no value has been set, the summary displayed will be 'Not set', otherwise the summary displayed will be the entry set for this preference
*/
class SimpleSummaryProvider private constructor() : SummaryProvider<IntegerListPreference> {
override fun provideSummary(preference : IntegerListPreference) : CharSequence {
return preference.getEntry() ?: preference.context.getString(R.string.not_set)
}
companion object {
private var simpleSummaryProvider : SimpleSummaryProvider? = null
val instance : SimpleSummaryProvider?
get() {
if (simpleSummaryProvider == null)
simpleSummaryProvider = SimpleSummaryProvider()
return simpleSummaryProvider
}
}
}
class IntegerListPreferenceDialogFragmentCompat : PreferenceDialogFragmentCompat() {
var clickedDialogEntryIndex = 0
private var entries : Array<CharSequence>? = null
private var entryValues : IntArray? = null
override fun onCreate(savedInstanceState : Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
val preference = listPreference
check(preference.entries != null) { "IntegerListPreference requires at least the entries array." }
clickedDialogEntryIndex = preference.findIndexOfValue(preference.value)
entries = preference.entries
entryValues = preference.entryValues
} else {
clickedDialogEntryIndex = savedInstanceState.getInt(SAVE_STATE_INDEX, 0)
entries = savedInstanceState.getCharSequenceArray(SAVE_STATE_ENTRIES)
entryValues = savedInstanceState.getIntArray(SAVE_STATE_ENTRY_VALUES)
}
}
override fun onSaveInstanceState(outState : Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(SAVE_STATE_INDEX, clickedDialogEntryIndex)
outState.putCharSequenceArray(SAVE_STATE_ENTRIES, entries)
outState.putIntArray(SAVE_STATE_ENTRY_VALUES, entryValues)
}
private val listPreference : IntegerListPreference
get() = preference as IntegerListPreference
override fun onPrepareDialogBuilder(builder : AlertDialog.Builder) {
super.onPrepareDialogBuilder(builder)
builder.setSingleChoiceItems(
entries, clickedDialogEntryIndex
) { dialog, which ->
if (clickedDialogEntryIndex != which) {
clickedDialogEntryIndex = which
if (listPreference.refreshRequired)
context?.getSettings()?.refreshRequired = true
}
// Clicking on an item simulates the positive button click, and dismisses the dialog
onClick(dialog, DialogInterface.BUTTON_POSITIVE)
dialog.dismiss()
}
// The typical interaction for list-based dialogs is to have click-on-an-item dismiss the dialog instead of the user having to press 'Ok'
builder.setPositiveButton(null, null)
}
override fun onDialogClosed(positiveResult : Boolean) {
if (positiveResult && clickedDialogEntryIndex >= 0) {
val value = entryValues?.get(clickedDialogEntryIndex) ?: clickedDialogEntryIndex
val preference = listPreference
if (preference.callChangeListener(value))
preference.value = value
}
}
companion object {
private const val SAVE_STATE_INDEX = "ListPreferenceDialogFragment.index"
private const val SAVE_STATE_ENTRIES = "ListPreferenceDialogFragment.entries"
private const val SAVE_STATE_ENTRY_VALUES = "ListPreferenceDialogFragment.entryValues"
fun newInstance(key : String?) : IntegerListPreferenceDialogFragmentCompat {
val fragment = IntegerListPreferenceDialogFragmentCompat()
val bundle = Bundle(1)
bundle.putString(ARG_KEY, key)
fragment.arguments = bundle
return fragment
}
}
}
}