diff options
| author | 2023-08-25 21:27:13 -0400 | |
|---|---|---|
| committer | 2023-08-29 19:42:42 -0400 | |
| commit | 45280a03420d4cdc2fb491b21d409f4101997bab (patch) | |
| tree | ca7b5c3b54f6accaed77a1514e3a6448b3cc7de2 | |
| parent | android: Add search for settings (diff) | |
| download | yuzu-45280a03420d4cdc2fb491b21d409f4101997bab.tar.gz yuzu-45280a03420d4cdc2fb491b21d409f4101997bab.tar.xz yuzu-45280a03420d4cdc2fb491b21d409f4101997bab.zip | |
android: Proper state restoration on settings dialogs
All dialog code (except for the Date/Time ones) has been extracted out into a generic settings dialog fragment that handles everything through a viewmodel. State for each dialog will now be retained and dialogs will stay shown through configuration changes.
I won't be changing the current state of the date and time dialog fragments until Google decides to make their classes non-final or if/when we migrate to Jetpack Compose.
Diffstat (limited to '')
9 files changed, 319 insertions, 198 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt index f5eba1222..a7a029fc1 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt | |||
| @@ -4,58 +4,54 @@ | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.ui | 4 | package org.yuzu.yuzu_emu.features.settings.ui |
| 5 | 5 | ||
| 6 | import android.content.Context | 6 | import android.content.Context |
| 7 | import android.content.DialogInterface | ||
| 8 | import android.icu.util.Calendar | 7 | import android.icu.util.Calendar |
| 9 | import android.icu.util.TimeZone | 8 | import android.icu.util.TimeZone |
| 10 | import android.text.format.DateFormat | 9 | import android.text.format.DateFormat |
| 11 | import android.view.LayoutInflater | 10 | import android.view.LayoutInflater |
| 12 | import android.view.ViewGroup | 11 | import android.view.ViewGroup |
| 13 | import android.widget.TextView | ||
| 14 | import androidx.appcompat.app.AlertDialog | ||
| 15 | import androidx.fragment.app.Fragment | 12 | import androidx.fragment.app.Fragment |
| 13 | import androidx.lifecycle.Lifecycle | ||
| 16 | import androidx.lifecycle.ViewModelProvider | 14 | import androidx.lifecycle.ViewModelProvider |
| 15 | import androidx.lifecycle.lifecycleScope | ||
| 16 | import androidx.lifecycle.repeatOnLifecycle | ||
| 17 | import androidx.navigation.findNavController | 17 | import androidx.navigation.findNavController |
| 18 | import androidx.recyclerview.widget.AsyncDifferConfig | 18 | import androidx.recyclerview.widget.AsyncDifferConfig |
| 19 | import androidx.recyclerview.widget.DiffUtil | 19 | import androidx.recyclerview.widget.DiffUtil |
| 20 | import androidx.recyclerview.widget.ListAdapter | 20 | import androidx.recyclerview.widget.ListAdapter |
| 21 | import com.google.android.material.datepicker.MaterialDatePicker | 21 | import com.google.android.material.datepicker.MaterialDatePicker |
| 22 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||
| 23 | import com.google.android.material.slider.Slider | ||
| 24 | import com.google.android.material.timepicker.MaterialTimePicker | 22 | import com.google.android.material.timepicker.MaterialTimePicker |
| 25 | import com.google.android.material.timepicker.TimeFormat | 23 | import com.google.android.material.timepicker.TimeFormat |
| 24 | import kotlinx.coroutines.launch | ||
| 26 | import org.yuzu.yuzu_emu.R | 25 | import org.yuzu.yuzu_emu.R |
| 27 | import org.yuzu.yuzu_emu.SettingsNavigationDirections | 26 | import org.yuzu.yuzu_emu.SettingsNavigationDirections |
| 28 | import org.yuzu.yuzu_emu.databinding.DialogSliderBinding | ||
| 29 | import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding | 27 | import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding |
| 30 | import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding | 28 | import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding |
| 31 | import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding | 29 | import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding |
| 32 | import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting | ||
| 33 | import org.yuzu.yuzu_emu.features.settings.model.ByteSetting | ||
| 34 | import org.yuzu.yuzu_emu.features.settings.model.FloatSetting | ||
| 35 | import org.yuzu.yuzu_emu.features.settings.model.ShortSetting | ||
| 36 | import org.yuzu.yuzu_emu.features.settings.model.view.* | 30 | import org.yuzu.yuzu_emu.features.settings.model.view.* |
| 37 | import org.yuzu.yuzu_emu.features.settings.ui.viewholder.* | 31 | import org.yuzu.yuzu_emu.features.settings.ui.viewholder.* |
| 32 | import org.yuzu.yuzu_emu.fragments.SettingsDialogFragment | ||
| 38 | import org.yuzu.yuzu_emu.model.SettingsViewModel | 33 | import org.yuzu.yuzu_emu.model.SettingsViewModel |
| 39 | 34 | ||
| 40 | class SettingsAdapter( | 35 | class SettingsAdapter( |
| 41 | private val fragment: Fragment, | 36 | private val fragment: Fragment, |
| 42 | private val context: Context | 37 | private val context: Context |
| 43 | ) : ListAdapter<SettingsItem, SettingViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()), | 38 | ) : ListAdapter<SettingsItem, SettingViewHolder>( |
| 44 | DialogInterface.OnClickListener { | 39 | AsyncDifferConfig.Builder(DiffCallback()).build() |
| 45 | private var clickedItem: SettingsItem? = null | 40 | ) { |
| 46 | private var clickedPosition: Int | ||
| 47 | private var dialog: AlertDialog? = null | ||
| 48 | private var sliderProgress = 0 | ||
| 49 | private var textSliderValue: TextView? = null | ||
| 50 | |||
| 51 | private val settingsViewModel: SettingsViewModel | 41 | private val settingsViewModel: SettingsViewModel |
| 52 | get() = ViewModelProvider(fragment.requireActivity())[SettingsViewModel::class.java] | 42 | get() = ViewModelProvider(fragment.requireActivity())[SettingsViewModel::class.java] |
| 53 | 43 | ||
| 54 | private var defaultCancelListener = | ||
| 55 | DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> closeDialog() } | ||
| 56 | |||
| 57 | init { | 44 | init { |
| 58 | clickedPosition = -1 | 45 | fragment.viewLifecycleOwner.lifecycleScope.launch { |
| 46 | fragment.repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
| 47 | settingsViewModel.adapterItemChanged.collect { | ||
| 48 | if (it != -1) { | ||
| 49 | notifyItemChanged(it) | ||
| 50 | settingsViewModel.setAdapterItemChanged(-1) | ||
| 51 | } | ||
| 52 | } | ||
| 53 | } | ||
| 54 | } | ||
| 59 | } | 55 | } |
| 60 | 56 | ||
| 61 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder { | 57 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder { |
| @@ -112,36 +108,25 @@ class SettingsAdapter( | |||
| 112 | settingsViewModel.shouldSave = true | 108 | settingsViewModel.shouldSave = true |
| 113 | } | 109 | } |
| 114 | 110 | ||
| 115 | private fun onSingleChoiceClick(item: SingleChoiceSetting) { | ||
| 116 | clickedItem = item | ||
| 117 | val value = getSelectionForSingleChoiceValue(item) | ||
| 118 | dialog = MaterialAlertDialogBuilder(context) | ||
| 119 | .setTitle(item.nameId) | ||
| 120 | .setSingleChoiceItems(item.choicesId, value, this) | ||
| 121 | .show() | ||
| 122 | } | ||
| 123 | |||
| 124 | fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) { | 111 | fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) { |
| 125 | clickedPosition = position | 112 | SettingsDialogFragment.newInstance( |
| 126 | onSingleChoiceClick(item) | 113 | settingsViewModel, |
| 127 | } | 114 | item, |
| 128 | 115 | SettingsItem.TYPE_SINGLE_CHOICE, | |
| 129 | private fun onStringSingleChoiceClick(item: StringSingleChoiceSetting) { | 116 | position |
| 130 | clickedItem = item | 117 | ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) |
| 131 | dialog = MaterialAlertDialogBuilder(context) | ||
| 132 | .setTitle(item.nameId) | ||
| 133 | .setSingleChoiceItems(item.choices, item.selectValueIndex, this) | ||
| 134 | .show() | ||
| 135 | } | 118 | } |
| 136 | 119 | ||
| 137 | fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) { | 120 | fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) { |
| 138 | clickedPosition = position | 121 | SettingsDialogFragment.newInstance( |
| 139 | onStringSingleChoiceClick(item) | 122 | settingsViewModel, |
| 123 | item, | ||
| 124 | SettingsItem.TYPE_STRING_SINGLE_CHOICE, | ||
| 125 | position | ||
| 126 | ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) | ||
| 140 | } | 127 | } |
| 141 | 128 | ||
| 142 | fun onDateTimeClick(item: DateTimeSetting, position: Int) { | 129 | fun onDateTimeClick(item: DateTimeSetting, position: Int) { |
| 143 | clickedItem = item | ||
| 144 | clickedPosition = position | ||
| 145 | val storedTime = item.value * 1000 | 130 | val storedTime = item.value * 1000 |
| 146 | 131 | ||
| 147 | // Helper to extract hour and minute from epoch time | 132 | // Helper to extract hour and minute from epoch time |
| @@ -177,10 +162,9 @@ class SettingsAdapter( | |||
| 177 | epochTime += timePicker.minute.toLong() * 60 | 162 | epochTime += timePicker.minute.toLong() * 60 |
| 178 | if (item.value != epochTime) { | 163 | if (item.value != epochTime) { |
| 179 | settingsViewModel.shouldSave = true | 164 | settingsViewModel.shouldSave = true |
| 180 | notifyItemChanged(clickedPosition) | 165 | notifyItemChanged(position) |
| 181 | item.value = epochTime | 166 | item.value = epochTime |
| 182 | } | 167 | } |
| 183 | clickedItem = null | ||
| 184 | } | 168 | } |
| 185 | datePicker.show( | 169 | datePicker.show( |
| 186 | fragment.childFragmentManager, | 170 | fragment.childFragmentManager, |
| @@ -189,40 +173,12 @@ class SettingsAdapter( | |||
| 189 | } | 173 | } |
| 190 | 174 | ||
| 191 | fun onSliderClick(item: SliderSetting, position: Int) { | 175 | fun onSliderClick(item: SliderSetting, position: Int) { |
| 192 | clickedItem = item | 176 | SettingsDialogFragment.newInstance( |
| 193 | clickedPosition = position | 177 | settingsViewModel, |
| 194 | sliderProgress = item.selectedValue as Int | 178 | item, |
| 195 | 179 | SettingsItem.TYPE_SLIDER, | |
| 196 | val inflater = LayoutInflater.from(context) | 180 | position |
| 197 | val sliderBinding = DialogSliderBinding.inflate(inflater) | 181 | ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) |
| 198 | |||
| 199 | textSliderValue = sliderBinding.textValue | ||
| 200 | textSliderValue!!.text = String.format( | ||
| 201 | context.getString(R.string.value_with_units), | ||
| 202 | sliderProgress.toString(), | ||
| 203 | item.units | ||
| 204 | ) | ||
| 205 | |||
| 206 | sliderBinding.slider.apply { | ||
| 207 | valueFrom = item.min.toFloat() | ||
| 208 | valueTo = item.max.toFloat() | ||
| 209 | value = sliderProgress.toFloat() | ||
| 210 | addOnChangeListener { _: Slider, value: Float, _: Boolean -> | ||
| 211 | sliderProgress = value.toInt() | ||
| 212 | textSliderValue!!.text = String.format( | ||
| 213 | context.getString(R.string.value_with_units), | ||
| 214 | sliderProgress.toString(), | ||
| 215 | item.units | ||
| 216 | ) | ||
| 217 | } | ||
| 218 | } | ||
| 219 | |||
| 220 | dialog = MaterialAlertDialogBuilder(context) | ||
| 221 | .setTitle(item.nameId) | ||
| 222 | .setView(sliderBinding.root) | ||
| 223 | .setPositiveButton(android.R.string.ok, this) | ||
| 224 | .setNegativeButton(android.R.string.cancel, defaultCancelListener) | ||
| 225 | .show() | ||
| 226 | } | 182 | } |
| 227 | 183 | ||
| 228 | fun onSubmenuClick(item: SubmenuSetting) { | 184 | fun onSubmenuClick(item: SubmenuSetting) { |
| @@ -230,112 +186,17 @@ class SettingsAdapter( | |||
| 230 | fragment.view?.findNavController()?.navigate(action) | 186 | fragment.view?.findNavController()?.navigate(action) |
| 231 | } | 187 | } |
| 232 | 188 | ||
| 233 | override fun onClick(dialog: DialogInterface, which: Int) { | 189 | fun onLongClick(item: SettingsItem, position: Int): Boolean { |
| 234 | when (clickedItem) { | 190 | SettingsDialogFragment.newInstance( |
| 235 | is SingleChoiceSetting -> { | 191 | settingsViewModel, |
| 236 | val scSetting = clickedItem as SingleChoiceSetting | 192 | item, |
| 237 | val value = getValueForSingleChoiceSelection(scSetting, which) | 193 | SettingsDialogFragment.TYPE_RESET_SETTING, |
| 238 | if (scSetting.selectedValue != value) { | 194 | position |
| 239 | settingsViewModel.shouldSave = true | 195 | ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) |
| 240 | } | ||
| 241 | |||
| 242 | // Get the backing Setting, which may be null (if for example it was missing from the file) | ||
| 243 | scSetting.selectedValue = value | ||
| 244 | closeDialog() | ||
| 245 | } | ||
| 246 | |||
| 247 | is StringSingleChoiceSetting -> { | ||
| 248 | val scSetting = clickedItem as StringSingleChoiceSetting | ||
| 249 | val value = scSetting.getValueAt(which) | ||
| 250 | if (scSetting.selectedValue != value) settingsViewModel.shouldSave = true | ||
| 251 | scSetting.selectedValue = value!! | ||
| 252 | closeDialog() | ||
| 253 | } | ||
| 254 | |||
| 255 | is SliderSetting -> { | ||
| 256 | val sliderSetting = clickedItem as SliderSetting | ||
| 257 | if (sliderSetting.selectedValue != sliderProgress) { | ||
| 258 | settingsViewModel.shouldSave = true | ||
| 259 | } | ||
| 260 | when (sliderSetting.setting) { | ||
| 261 | is ByteSetting -> { | ||
| 262 | val value = sliderProgress.toByte() | ||
| 263 | sliderSetting.selectedValue = value.toInt() | ||
| 264 | } | ||
| 265 | |||
| 266 | is ShortSetting -> { | ||
| 267 | val value = sliderProgress.toShort() | ||
| 268 | sliderSetting.selectedValue = value.toInt() | ||
| 269 | } | ||
| 270 | |||
| 271 | is FloatSetting -> { | ||
| 272 | val value = sliderProgress.toFloat() | ||
| 273 | sliderSetting.selectedValue = value.toInt() | ||
| 274 | } | ||
| 275 | |||
| 276 | else -> { | ||
| 277 | sliderSetting.selectedValue = sliderProgress | ||
| 278 | } | ||
| 279 | } | ||
| 280 | closeDialog() | ||
| 281 | } | ||
| 282 | } | ||
| 283 | clickedItem = null | ||
| 284 | sliderProgress = -1 | ||
| 285 | } | ||
| 286 | |||
| 287 | fun onLongClick(setting: AbstractSetting, position: Int): Boolean { | ||
| 288 | MaterialAlertDialogBuilder(context) | ||
| 289 | .setMessage(R.string.reset_setting_confirmation) | ||
| 290 | .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> | ||
| 291 | setting.reset() | ||
| 292 | notifyItemChanged(position) | ||
| 293 | settingsViewModel.shouldSave = true | ||
| 294 | } | ||
| 295 | .setNegativeButton(android.R.string.cancel, null) | ||
| 296 | .show() | ||
| 297 | 196 | ||
| 298 | return true | 197 | return true |
| 299 | } | 198 | } |
| 300 | 199 | ||
| 301 | fun closeDialog() { | ||
| 302 | if (dialog != null) { | ||
| 303 | if (clickedPosition != -1) { | ||
| 304 | notifyItemChanged(clickedPosition) | ||
| 305 | clickedPosition = -1 | ||
| 306 | } | ||
| 307 | dialog!!.dismiss() | ||
| 308 | dialog = null | ||
| 309 | } | ||
| 310 | } | ||
| 311 | |||
| 312 | private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int { | ||
| 313 | val valuesId = item.valuesId | ||
| 314 | return if (valuesId > 0) { | ||
| 315 | val valuesArray = context.resources.getIntArray(valuesId) | ||
| 316 | valuesArray[which] | ||
| 317 | } else { | ||
| 318 | which | ||
| 319 | } | ||
| 320 | } | ||
| 321 | |||
| 322 | private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int { | ||
| 323 | val value = item.selectedValue | ||
| 324 | val valuesId = item.valuesId | ||
| 325 | if (valuesId > 0) { | ||
| 326 | val valuesArray = context.resources.getIntArray(valuesId) | ||
| 327 | for (index in valuesArray.indices) { | ||
| 328 | val current = valuesArray[index] | ||
| 329 | if (current == value) { | ||
| 330 | return index | ||
| 331 | } | ||
| 332 | } | ||
| 333 | } else { | ||
| 334 | return value | ||
| 335 | } | ||
| 336 | return -1 | ||
| 337 | } | ||
| 338 | |||
| 339 | private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() { | 200 | private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() { |
| 340 | override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean { | 201 | override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean { |
| 341 | return oldItem.setting.key == newItem.setting.key | 202 | return oldItem.setting.key == newItem.setting.key |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt index 0ea587a88..bc319714c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt | |||
| @@ -123,11 +123,6 @@ class SettingsFragment : Fragment() { | |||
| 123 | settingsViewModel.setIsUsingSearch(false) | 123 | settingsViewModel.setIsUsingSearch(false) |
| 124 | } | 124 | } |
| 125 | 125 | ||
| 126 | override fun onDetach() { | ||
| 127 | super.onDetach() | ||
| 128 | settingsAdapter?.closeDialog() | ||
| 129 | } | ||
| 130 | |||
| 131 | private fun setInsets() { | 126 | private fun setInsets() { |
| 132 | ViewCompat.setOnApplyWindowInsetsListener( | 127 | ViewCompat.setOnApplyWindowInsetsListener( |
| 133 | binding.root | 128 | binding.root |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt index 68c0b24d6..525f013f8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt | |||
| @@ -46,7 +46,7 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA | |||
| 46 | 46 | ||
| 47 | override fun onLongClick(clicked: View): Boolean { | 47 | override fun onLongClick(clicked: View): Boolean { |
| 48 | if (setting.isEditable) { | 48 | if (setting.isEditable) { |
| 49 | return adapter.onLongClick(setting.setting, bindingAdapterPosition) | 49 | return adapter.onLongClick(setting, bindingAdapterPosition) |
| 50 | } | 50 | } |
| 51 | return false | 51 | return false |
| 52 | } | 52 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt index a582c425b..80d1b22c1 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt | |||
| @@ -66,7 +66,7 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti | |||
| 66 | 66 | ||
| 67 | override fun onLongClick(clicked: View): Boolean { | 67 | override fun onLongClick(clicked: View): Boolean { |
| 68 | if (setting.isEditable) { | 68 | if (setting.isEditable) { |
| 69 | return adapter.onLongClick(setting.setting, bindingAdapterPosition) | 69 | return adapter.onLongClick(setting, bindingAdapterPosition) |
| 70 | } | 70 | } |
| 71 | return false | 71 | return false |
| 72 | } | 72 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt index d94a46262..b83c90100 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt | |||
| @@ -41,7 +41,7 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda | |||
| 41 | 41 | ||
| 42 | override fun onLongClick(clicked: View): Boolean { | 42 | override fun onLongClick(clicked: View): Boolean { |
| 43 | if (setting.isEditable) { | 43 | if (setting.isEditable) { |
| 44 | return adapter.onLongClick(setting.setting, bindingAdapterPosition) | 44 | return adapter.onLongClick(setting, bindingAdapterPosition) |
| 45 | } | 45 | } |
| 46 | return false | 46 | return false |
| 47 | } | 47 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt index 0a37d3624..57fdeaa20 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt | |||
| @@ -43,7 +43,7 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter | |||
| 43 | 43 | ||
| 44 | override fun onLongClick(clicked: View): Boolean { | 44 | override fun onLongClick(clicked: View): Boolean { |
| 45 | if (setting.isEditable) { | 45 | if (setting.isEditable) { |
| 46 | return adapter.onLongClick(setting.setting, bindingAdapterPosition) | 46 | return adapter.onLongClick(setting, bindingAdapterPosition) |
| 47 | } | 47 | } |
| 48 | return false | 48 | return false |
| 49 | } | 49 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt new file mode 100644 index 000000000..d18ec6974 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt | |||
| @@ -0,0 +1,235 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.fragments | ||
| 5 | |||
| 6 | import android.app.Dialog | ||
| 7 | import android.content.DialogInterface | ||
| 8 | import android.os.Bundle | ||
| 9 | import android.view.LayoutInflater | ||
| 10 | import android.view.View | ||
| 11 | import android.view.ViewGroup | ||
| 12 | import androidx.fragment.app.DialogFragment | ||
| 13 | import androidx.fragment.app.activityViewModels | ||
| 14 | import androidx.lifecycle.Lifecycle | ||
| 15 | import androidx.lifecycle.lifecycleScope | ||
| 16 | import androidx.lifecycle.repeatOnLifecycle | ||
| 17 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||
| 18 | import com.google.android.material.slider.Slider | ||
| 19 | import kotlinx.coroutines.launch | ||
| 20 | import org.yuzu.yuzu_emu.R | ||
| 21 | import org.yuzu.yuzu_emu.databinding.DialogSliderBinding | ||
| 22 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | ||
| 23 | import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting | ||
| 24 | import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting | ||
| 25 | import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting | ||
| 26 | import org.yuzu.yuzu_emu.model.SettingsViewModel | ||
| 27 | |||
| 28 | class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener { | ||
| 29 | private var type = 0 | ||
| 30 | private var position = 0 | ||
| 31 | |||
| 32 | private var defaultCancelListener = | ||
| 33 | DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> closeDialog() } | ||
| 34 | |||
| 35 | private val settingsViewModel: SettingsViewModel by activityViewModels() | ||
| 36 | |||
| 37 | private lateinit var sliderBinding: DialogSliderBinding | ||
| 38 | |||
| 39 | override fun onCreate(savedInstanceState: Bundle?) { | ||
| 40 | super.onCreate(savedInstanceState) | ||
| 41 | type = requireArguments().getInt(TYPE) | ||
| 42 | position = requireArguments().getInt(POSITION) | ||
| 43 | |||
| 44 | if (settingsViewModel.clickedItem == null) dismiss() | ||
| 45 | } | ||
| 46 | |||
| 47 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { | ||
| 48 | return when (type) { | ||
| 49 | TYPE_RESET_SETTING -> { | ||
| 50 | MaterialAlertDialogBuilder(requireContext()) | ||
| 51 | .setMessage(R.string.reset_setting_confirmation) | ||
| 52 | .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> | ||
| 53 | settingsViewModel.clickedItem!!.setting.reset() | ||
| 54 | settingsViewModel.setAdapterItemChanged(position) | ||
| 55 | settingsViewModel.shouldSave = true | ||
| 56 | } | ||
| 57 | .setNegativeButton(android.R.string.cancel, null) | ||
| 58 | .create() | ||
| 59 | } | ||
| 60 | |||
| 61 | SettingsItem.TYPE_SINGLE_CHOICE -> { | ||
| 62 | val item = settingsViewModel.clickedItem as SingleChoiceSetting | ||
| 63 | val value = getSelectionForSingleChoiceValue(item) | ||
| 64 | MaterialAlertDialogBuilder(requireContext()) | ||
| 65 | .setTitle(item.nameId) | ||
| 66 | .setSingleChoiceItems(item.choicesId, value, this) | ||
| 67 | .create() | ||
| 68 | } | ||
| 69 | |||
| 70 | SettingsItem.TYPE_SLIDER -> { | ||
| 71 | sliderBinding = DialogSliderBinding.inflate(layoutInflater) | ||
| 72 | val item = settingsViewModel.clickedItem as SliderSetting | ||
| 73 | |||
| 74 | settingsViewModel.setSliderTextValue(item.selectedValue.toFloat(), item.units) | ||
| 75 | sliderBinding.slider.apply { | ||
| 76 | valueFrom = item.min.toFloat() | ||
| 77 | valueTo = item.max.toFloat() | ||
| 78 | value = settingsViewModel.sliderProgress.value.toFloat() | ||
| 79 | addOnChangeListener { _: Slider, value: Float, _: Boolean -> | ||
| 80 | settingsViewModel.setSliderTextValue(value, item.units) | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | MaterialAlertDialogBuilder(requireContext()) | ||
| 85 | .setTitle(item.nameId) | ||
| 86 | .setView(sliderBinding.root) | ||
| 87 | .setPositiveButton(android.R.string.ok, this) | ||
| 88 | .setNegativeButton(android.R.string.cancel, defaultCancelListener) | ||
| 89 | .create() | ||
| 90 | } | ||
| 91 | |||
| 92 | SettingsItem.TYPE_STRING_SINGLE_CHOICE -> { | ||
| 93 | val item = settingsViewModel.clickedItem as StringSingleChoiceSetting | ||
| 94 | MaterialAlertDialogBuilder(requireContext()) | ||
| 95 | .setTitle(item.nameId) | ||
| 96 | .setSingleChoiceItems(item.choices, item.selectValueIndex, this) | ||
| 97 | .create() | ||
| 98 | } | ||
| 99 | |||
| 100 | else -> super.onCreateDialog(savedInstanceState) | ||
| 101 | } | ||
| 102 | } | ||
| 103 | |||
| 104 | override fun onCreateView( | ||
| 105 | inflater: LayoutInflater, | ||
| 106 | container: ViewGroup?, | ||
| 107 | savedInstanceState: Bundle? | ||
| 108 | ): View? { | ||
| 109 | return when (type) { | ||
| 110 | SettingsItem.TYPE_SLIDER -> sliderBinding.root | ||
| 111 | else -> super.onCreateView(inflater, container, savedInstanceState) | ||
| 112 | } | ||
| 113 | } | ||
| 114 | |||
| 115 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| 116 | super.onViewCreated(view, savedInstanceState) | ||
| 117 | when (type) { | ||
| 118 | SettingsItem.TYPE_SLIDER -> { | ||
| 119 | viewLifecycleOwner.lifecycleScope.launch { | ||
| 120 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 121 | settingsViewModel.sliderTextValue.collect { | ||
| 122 | sliderBinding.textValue.text = it | ||
| 123 | } | ||
| 124 | } | ||
| 125 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 126 | settingsViewModel.sliderProgress.collect { | ||
| 127 | sliderBinding.slider.value = it.toFloat() | ||
| 128 | } | ||
| 129 | } | ||
| 130 | } | ||
| 131 | } | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | override fun onClick(dialog: DialogInterface, which: Int) { | ||
| 136 | when (settingsViewModel.clickedItem) { | ||
| 137 | is SingleChoiceSetting -> { | ||
| 138 | val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting | ||
| 139 | val value = getValueForSingleChoiceSelection(scSetting, which) | ||
| 140 | if (scSetting.selectedValue != value) { | ||
| 141 | settingsViewModel.shouldSave = true | ||
| 142 | } | ||
| 143 | scSetting.selectedValue = value | ||
| 144 | } | ||
| 145 | |||
| 146 | is StringSingleChoiceSetting -> { | ||
| 147 | val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting | ||
| 148 | val value = scSetting.getValueAt(which) | ||
| 149 | if (scSetting.selectedValue != value) settingsViewModel.shouldSave = true | ||
| 150 | scSetting.selectedValue = value | ||
| 151 | } | ||
| 152 | |||
| 153 | is SliderSetting -> { | ||
| 154 | val sliderSetting = settingsViewModel.clickedItem as SliderSetting | ||
| 155 | if (sliderSetting.selectedValue != settingsViewModel.sliderProgress.value) { | ||
| 156 | settingsViewModel.shouldSave = true | ||
| 157 | } | ||
| 158 | sliderSetting.selectedValue = settingsViewModel.sliderProgress.value | ||
| 159 | } | ||
| 160 | } | ||
| 161 | closeDialog() | ||
| 162 | } | ||
| 163 | |||
| 164 | private fun closeDialog() { | ||
| 165 | settingsViewModel.setAdapterItemChanged(position) | ||
| 166 | settingsViewModel.clickedItem = null | ||
| 167 | settingsViewModel.setSliderProgress(-1f) | ||
| 168 | dismiss() | ||
| 169 | } | ||
| 170 | |||
| 171 | private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int { | ||
| 172 | val valuesId = item.valuesId | ||
| 173 | return if (valuesId > 0) { | ||
| 174 | val valuesArray = requireContext().resources.getIntArray(valuesId) | ||
| 175 | valuesArray[which] | ||
| 176 | } else { | ||
| 177 | which | ||
| 178 | } | ||
| 179 | } | ||
| 180 | |||
| 181 | private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int { | ||
| 182 | val value = item.selectedValue | ||
| 183 | val valuesId = item.valuesId | ||
| 184 | if (valuesId > 0) { | ||
| 185 | val valuesArray = requireContext().resources.getIntArray(valuesId) | ||
| 186 | for (index in valuesArray.indices) { | ||
| 187 | val current = valuesArray[index] | ||
| 188 | if (current == value) { | ||
| 189 | return index | ||
| 190 | } | ||
| 191 | } | ||
| 192 | } else { | ||
| 193 | return value | ||
| 194 | } | ||
| 195 | return -1 | ||
| 196 | } | ||
| 197 | |||
| 198 | companion object { | ||
| 199 | const val TAG = "SettingsDialogFragment" | ||
| 200 | |||
| 201 | const val TYPE_RESET_SETTING = -1 | ||
| 202 | |||
| 203 | const val TITLE = "Title" | ||
| 204 | const val TYPE = "Type" | ||
| 205 | const val POSITION = "Position" | ||
| 206 | |||
| 207 | fun newInstance( | ||
| 208 | settingsViewModel: SettingsViewModel, | ||
| 209 | clickedItem: SettingsItem, | ||
| 210 | type: Int, | ||
| 211 | position: Int | ||
| 212 | ): SettingsDialogFragment { | ||
| 213 | when (type) { | ||
| 214 | SettingsItem.TYPE_HEADER, | ||
| 215 | SettingsItem.TYPE_SWITCH, | ||
| 216 | SettingsItem.TYPE_SUBMENU, | ||
| 217 | SettingsItem.TYPE_DATETIME_SETTING, | ||
| 218 | SettingsItem.TYPE_RUNNABLE -> | ||
| 219 | throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!") | ||
| 220 | |||
| 221 | SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress( | ||
| 222 | (clickedItem as SliderSetting).selectedValue.toFloat() | ||
| 223 | ) | ||
| 224 | } | ||
| 225 | settingsViewModel.clickedItem = clickedItem | ||
| 226 | |||
| 227 | val args = Bundle() | ||
| 228 | args.putInt(TYPE, type) | ||
| 229 | args.putInt(POSITION, position) | ||
| 230 | val fragment = SettingsDialogFragment() | ||
| 231 | fragment.arguments = args | ||
| 232 | return fragment | ||
| 233 | } | ||
| 234 | } | ||
| 235 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt index 4f93db4ad..55b6a0367 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt | |||
| @@ -91,11 +91,6 @@ class SettingsSearchFragment : Fragment() { | |||
| 91 | setInsets() | 91 | setInsets() |
| 92 | } | 92 | } |
| 93 | 93 | ||
| 94 | override fun onDetach() { | ||
| 95 | super.onDetach() | ||
| 96 | settingsAdapter?.closeDialog() | ||
| 97 | } | ||
| 98 | |||
| 99 | override fun onSaveInstanceState(outState: Bundle) { | 94 | override fun onSaveInstanceState(outState: Bundle) { |
| 100 | super.onSaveInstanceState(outState) | 95 | super.onSaveInstanceState(outState) |
| 101 | outState.putString(SEARCH_TEXT, binding.searchText.text.toString()) | 96 | outState.putString(SEARCH_TEXT, binding.searchText.text.toString()) |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt index a0cb7225f..d16d15fa6 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt | |||
| @@ -5,13 +5,19 @@ package org.yuzu.yuzu_emu.model | |||
| 5 | 5 | ||
| 6 | import androidx.lifecycle.LiveData | 6 | import androidx.lifecycle.LiveData |
| 7 | import androidx.lifecycle.MutableLiveData | 7 | import androidx.lifecycle.MutableLiveData |
| 8 | import androidx.lifecycle.SavedStateHandle | ||
| 8 | import androidx.lifecycle.ViewModel | 9 | import androidx.lifecycle.ViewModel |
| 10 | import org.yuzu.yuzu_emu.R | ||
| 11 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 12 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | ||
| 9 | 13 | ||
| 10 | class SettingsViewModel : ViewModel() { | 14 | class SettingsViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { |
| 11 | var game: Game? = null | 15 | var game: Game? = null |
| 12 | 16 | ||
| 13 | var shouldSave = false | 17 | var shouldSave = false |
| 14 | 18 | ||
| 19 | var clickedItem: SettingsItem? = null | ||
| 20 | |||
| 15 | private val _toolbarTitle = MutableLiveData("") | 21 | private val _toolbarTitle = MutableLiveData("") |
| 16 | val toolbarTitle: LiveData<String> get() = _toolbarTitle | 22 | val toolbarTitle: LiveData<String> get() = _toolbarTitle |
| 17 | 23 | ||
| @@ -30,6 +36,12 @@ class SettingsViewModel : ViewModel() { | |||
| 30 | private val _isUsingSearch = MutableLiveData(false) | 36 | private val _isUsingSearch = MutableLiveData(false) |
| 31 | val isUsingSearch: LiveData<Boolean> get() = _isUsingSearch | 37 | val isUsingSearch: LiveData<Boolean> get() = _isUsingSearch |
| 32 | 38 | ||
| 39 | val sliderProgress = savedStateHandle.getStateFlow(KEY_SLIDER_PROGRESS, -1) | ||
| 40 | |||
| 41 | val sliderTextValue = savedStateHandle.getStateFlow(KEY_SLIDER_TEXT_VALUE, "") | ||
| 42 | |||
| 43 | val adapterItemChanged = savedStateHandle.getStateFlow(KEY_ADAPTER_ITEM_CHANGED, -1) | ||
| 44 | |||
| 33 | fun setToolbarTitle(value: String) { | 45 | fun setToolbarTitle(value: String) { |
| 34 | _toolbarTitle.value = value | 46 | _toolbarTitle.value = value |
| 35 | } | 47 | } |
| @@ -54,8 +66,31 @@ class SettingsViewModel : ViewModel() { | |||
| 54 | _isUsingSearch.value = value | 66 | _isUsingSearch.value = value |
| 55 | } | 67 | } |
| 56 | 68 | ||
| 69 | fun setSliderTextValue(value: Float, units: String) { | ||
| 70 | savedStateHandle[KEY_SLIDER_PROGRESS] = value | ||
| 71 | savedStateHandle[KEY_SLIDER_TEXT_VALUE] = String.format( | ||
| 72 | YuzuApplication.appContext.getString(R.string.value_with_units), | ||
| 73 | value.toInt().toString(), | ||
| 74 | units | ||
| 75 | ) | ||
| 76 | } | ||
| 77 | |||
| 78 | fun setSliderProgress(value: Float) { | ||
| 79 | savedStateHandle[KEY_SLIDER_PROGRESS] = value | ||
| 80 | } | ||
| 81 | |||
| 82 | fun setAdapterItemChanged(value: Int) { | ||
| 83 | savedStateHandle[KEY_ADAPTER_ITEM_CHANGED] = value | ||
| 84 | } | ||
| 85 | |||
| 57 | fun clear() { | 86 | fun clear() { |
| 58 | game = null | 87 | game = null |
| 59 | shouldSave = false | 88 | shouldSave = false |
| 60 | } | 89 | } |
| 90 | |||
| 91 | companion object { | ||
| 92 | const val KEY_SLIDER_TEXT_VALUE = "SliderTextValue" | ||
| 93 | const val KEY_SLIDER_PROGRESS = "SliderProgress" | ||
| 94 | const val KEY_ADAPTER_ITEM_CHANGED = "AdapterItemChanged" | ||
| 95 | } | ||
| 61 | } | 96 | } |