Merge pull request #14742 from Simonx22/android/settings-search-next

Android: Add global settings search
This commit is contained in:
JosJuice
2026-08-23 12:19:09 +02:00
committed by GitHub
20 changed files with 1302 additions and 663 deletions

View File

@@ -86,5 +86,6 @@ abstract class SettingsItem {
const val TYPE_STRING = 12
const val TYPE_HYPERLINK_HEADER = 13
const val TYPE_DATETIME_CHOICE = 14
const val TYPE_SEARCH_RESULT = 15
}
}

View File

@@ -0,0 +1,19 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.settings.model.view
import android.os.Bundle
import org.dolphinemu.dolphinemu.features.settings.model.AbstractSetting
import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag
class SettingsSearchResult(
name: CharSequence,
description: CharSequence,
val menuKey: MenuTag,
val settingPosition: Int,
val navigationExtras: Bundle?
) : SettingsItem(name, description) {
override val type: Int = TYPE_SEARCH_RESULT
override val setting: AbstractSetting? = null
}

View File

@@ -7,14 +7,17 @@ import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.view.KeyEvent
import android.view.Menu
import android.view.MotionEvent
import android.view.View
import android.view.animation.PathInterpolator
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
@@ -39,6 +42,17 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
private var dialog: AlertDialog? = null
private var toolbarLayout: CollapsingToolbarLayout? = null
private var binding: ActivitySettingsBinding? = null
private lateinit var searchView: SearchView
private var expandedToolbarHeight = 0
private var toolbarStateGeneration = 0
private var currentToolbarTitle: String? = null
private var currentToolbarShowsHeadline = false
private var currentToolbarShowsSearch = false
private var currentToolbarShowsSearchMode = false
override val settingsSearchQuery: String
get() = presenter!!.settingsSearchQuery
override val isSettingsSearchActive: Boolean
get() = presenter!!.isSettingsSearchActive
override var themeId: Int = 0
override var isMappingAllDevices = false
@@ -76,8 +90,11 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
presenter = SettingsActivityPresenter(this, settings)
presenter!!.onCreate(savedInstanceState, menuTag, gameID, revision, isWii, this)
toolbarLayout = binding!!.toolbarSettingsLayout
expandedToolbarHeight = toolbarLayout!!.layoutParams.height
setSupportActionBar(binding!!.toolbarSettings)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
setUpSettingsSearch()
setUpBackNavigation()
// TODO: Remove this when CollapsingToolbarLayouts are fixed by Google
// https://github.com/material-components/material-components-android/issues/1310
@@ -86,16 +103,78 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
enableScrollTint(this, binding!!.toolbarSettings, binding!!.appbarSettings)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.menu_settings, menu)
return true
private fun setUpSettingsSearch() {
searchView = binding!!.settingsSearch
searchView.setQuery(settingsSearchQuery, false)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
searchView.clearFocus()
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
presenter!!.onSettingsSearchQueryChanged(newText.orEmpty())
return true
}
})
binding!!.settingsSearchPreview.setOnClickListener { enterSettingsSearch() }
binding!!.settingsSearchToolbar.setNavigationOnClickListener { exitSettingsSearch() }
}
private fun enterSettingsSearch() {
if (!presenter!!.enterSettingsSearch()) {
return
}
refreshToolbarState()
val focusDelay = if (areSystemAnimationsEnabled()) SEARCH_FOCUS_DELAY_MS else 0L
searchView.postDelayed({
if (!isSettingsSearchActive) {
return@postDelayed
}
searchView.requestFocus()
WindowCompat.getInsetsController(window, searchView).show(WindowInsetsCompat.Type.ime())
}, focusDelay)
}
private fun exitSettingsSearch() {
if (!presenter!!.exitSettingsSearch()) {
return
}
searchView.setQuery("", false)
searchView.clearFocus()
WindowCompat.getInsetsController(window, searchView).hide(WindowInsetsCompat.Type.ime())
refreshToolbarState()
}
private fun refreshToolbarState() {
val title = currentToolbarTitle ?: getString(R.string.settings)
setToolbarState(
title, currentToolbarShowsHeadline, currentToolbarShowsSearch
)
}
private fun setUpBackNavigation() {
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (supportFragmentManager.backStackEntryCount == 0 && isSettingsSearchActive) {
exitSettingsSearch()
return
}
isEnabled = false
onBackPressedDispatcher.onBackPressed()
isEnabled = true
}
})
}
override fun onSaveInstanceState(outState: Bundle) {
// Critical: If super method is not called, rotations will be busted.
super.onSaveInstanceState(outState)
outState.putBoolean(KEY_MAPPING_ALL_DEVICES, isMappingAllDevices)
presenter!!.onSaveInstanceState(outState)
}
override fun onStart() {
@@ -128,10 +207,17 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
}
override fun showSettingsFragment(
menuTag: MenuTag, extras: Bundle?, addToStack: Boolean, gameId: String
) {
replaceSettingsFragment(menuTag, extras, addToStack, gameId, false)
}
private fun replaceSettingsFragment(
menuTag: MenuTag,
extras: Bundle?,
addToStack: Boolean,
gameId: String
gameId: String,
isSearchResult: Boolean
) {
if (!addToStack && fragment != null) return
val transaction = supportFragmentManager.beginTransaction()
@@ -140,15 +226,18 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
transaction.setCustomAnimations(
R.anim.anim_settings_fragment_in,
R.anim.anim_settings_fragment_out,
0,
R.anim.anim_pop_settings_fragment_out
if (isSearchResult) R.anim.anim_settings_search_pop_in else 0,
if (isSearchResult) {
R.anim.anim_settings_search_pop_out
} else {
R.anim.anim_pop_settings_fragment_out
}
)
}
transaction.addToBackStack(null)
}
transaction.replace(
R.id.frame_content_settings,
newInstance(menuTag, gameId, extras), FRAGMENT_TAG
R.id.frame_content_settings, newInstance(menuTag, gameId, extras), FRAGMENT_TAG
)
transaction.commit()
}
@@ -157,16 +246,22 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
fragment.show(supportFragmentManager, FRAGMENT_DIALOG_TAG)
}
override fun showSearchResult(
menuTag: MenuTag, settingPosition: Int, gameId: String, extras: Bundle?
) {
val navigationExtras = extras?.let(::Bundle) ?: Bundle()
navigationExtras.putInt(
SettingsFragment.ARGUMENT_SCROLL_TO_SETTING_POSITION, settingPosition
)
replaceSettingsFragment(menuTag, navigationExtras, true, gameId, true)
}
private fun areSystemAnimationsEnabled(): Boolean {
val duration = android.provider.Settings.Global.getFloat(
contentResolver,
android.provider.Settings.Global.ANIMATOR_DURATION_SCALE,
1f
contentResolver, android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, 1f
)
val transition = android.provider.Settings.Global.getFloat(
contentResolver,
android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE,
1f
contentResolver, android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE, 1f
)
return duration != 0f && transition != 0f
}
@@ -183,10 +278,8 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
override fun showLoading() {
if (dialog == null) {
dialog = MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.load_settings))
.setView(R.layout.dialog_indeterminate_progress)
.create()
dialog = MaterialAlertDialogBuilder(this).setTitle(getString(R.string.load_settings))
.setView(R.layout.dialog_indeterminate_progress).create()
}
dialog!!.show()
}
@@ -196,12 +289,10 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
}
override fun showGameIniJunkDeletionQuestion() {
MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.game_ini_junk_title))
MaterialAlertDialogBuilder(this).setTitle(getString(R.string.game_ini_junk_title))
.setMessage(getString(R.string.game_ini_junk_question))
.setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> presenter!!.clearGameSettings() }
.setNegativeButton(R.string.no, null)
.show()
.setNegativeButton(R.string.no, null).show()
}
override fun onSettingsFileLoaded(settings: Settings) {
@@ -229,13 +320,78 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
return presenter!!.hasMenuTagActionForValue(menuTag, value)
}
override fun getMenuTagActionExtras(menuTag: MenuTag, value: Int): Bundle? {
return presenter!!.getMenuTagActionExtras(menuTag, value)
}
override fun filterSettings(query: String) {
fragment?.filterSettings(query)
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
onBackPressedDispatcher.onBackPressed()
return true
}
override fun setToolbarTitle(title: String) {
binding!!.toolbarSettingsLayout.title = title
override fun setToolbarState(title: String, showHeadline: Boolean, showSearch: Boolean) {
val appBar = binding!!.appbarSettings
val generation = ++toolbarStateGeneration
val showSearchMode = showSearch && isSettingsSearchActive
val stateChanged =
currentToolbarTitle != title || currentToolbarShowsHeadline != showHeadline || currentToolbarShowsSearch != showSearch || currentToolbarShowsSearchMode != showSearchMode
appBar.animate().cancel()
if (!appBar.isLaidOut || !stateChanged) {
applyToolbarState(title, showHeadline, showSearch)
appBar.alpha = 1f
return
}
if (!showSearch) {
searchView.clearFocus()
}
appBar.animate().alpha(0f).setDuration(APP_BAR_FADE_OUT_DURATION_MS)
.setInterpolator(APP_BAR_FADE_OUT_INTERPOLATOR).withEndAction {
if (generation != toolbarStateGeneration) {
return@withEndAction
}
applyToolbarState(title, showHeadline, showSearch)
appBar.post {
if (generation != toolbarStateGeneration) {
return@post
}
appBar.animate().alpha(1f).setDuration(APP_BAR_FADE_IN_DURATION_MS)
.setInterpolator(APP_BAR_FADE_IN_INTERPOLATOR).start()
}
}.start()
}
private fun applyToolbarState(title: String, showHeadline: Boolean, showSearch: Boolean) {
val showSearchMode = showSearch && isSettingsSearchActive
toolbarLayout!!.isTitleEnabled = showHeadline
supportActionBar!!.title = title
if (showHeadline) {
toolbarLayout!!.title = title
}
toolbarLayout!!.layoutParams = toolbarLayout!!.layoutParams.apply {
height = if (showHeadline) {
expandedToolbarHeight
} else {
binding!!.toolbarSettings.layoutParams.height
}
}
toolbarLayout!!.visibility = if (showSearchMode) View.GONE else View.VISIBLE
binding!!.settingsSearchContainer.visibility =
if (showSearch && !showSearchMode) View.VISIBLE else View.GONE
binding!!.settingsSearchModeContainer.visibility =
if (showSearchMode) View.VISIBLE else View.GONE
currentToolbarTitle = title
currentToolbarShowsHeadline = showHeadline
currentToolbarShowsSearch = showSearch
currentToolbarShowsSearchMode = showSearchMode
}
override fun setOldControllerSettingsWarningVisibility(visible: Boolean): Int {
@@ -274,14 +430,16 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
private const val KEY_MAPPING_ALL_DEVICES = "all_devices"
private const val FRAGMENT_TAG = "settings"
private const val FRAGMENT_DIALOG_TAG = "settings_dialog"
private const val APP_BAR_FADE_OUT_DURATION_MS = 90L
private const val APP_BAR_FADE_IN_DURATION_MS = 180L
private const val SEARCH_FOCUS_DELAY_MS =
APP_BAR_FADE_OUT_DURATION_MS + APP_BAR_FADE_IN_DURATION_MS
private val APP_BAR_FADE_OUT_INTERPOLATOR = PathInterpolator(0.4f, 0f, 1f, 1f)
private val APP_BAR_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0.2f, 1f)
@JvmStatic
fun launch(
context: Context,
menuTag: MenuTag?,
gameId: String?,
revision: Int,
isWii: Boolean
context: Context, menuTag: MenuTag?, gameId: String?, revision: Int, isWii: Boolean
) {
val settings = Intent(context, SettingsActivity::class.java)
settings.putExtra(ARG_MENU_TAG, menuTag)
@@ -296,8 +454,7 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
val settings = Intent(context, SettingsActivity::class.java)
settings.putExtra(ARG_MENU_TAG, menuTag)
settings.putExtra(
ARG_IS_WII,
!NativeLibrary.IsRunning() || NativeLibrary.IsEmulatingWii()
ARG_IS_WII, !NativeLibrary.IsRunning() || NativeLibrary.IsEmulatingWii()
)
context.startActivity(settings)
}

View File

@@ -10,14 +10,17 @@ import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner
import org.dolphinemu.dolphinemu.utils.Log
class SettingsActivityPresenter(
private val activityView: SettingsActivityView,
var settings: Settings?
private val activityView: SettingsActivityView, var settings: Settings?
) {
private var menuTag: MenuTag? = null
private var gameId: String? = null
private var revision = 0
private var isWii = false
private lateinit var activity: AppCompatActivity
var settingsSearchQuery = ""
private set
var isSettingsSearchActive = false
private set
fun onCreate(
savedInstanceState: Bundle?,
@@ -32,6 +35,43 @@ class SettingsActivityPresenter(
this.revision = revision
this.isWii = isWii
this.activity = activity
if (savedInstanceState != null) {
isSettingsSearchActive =
savedInstanceState.getBoolean(KEY_SETTINGS_SEARCH_ACTIVE)
settingsSearchQuery =
savedInstanceState.getString(KEY_SETTINGS_SEARCH_QUERY).orEmpty()
}
}
fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(KEY_SETTINGS_SEARCH_ACTIVE, isSettingsSearchActive)
outState.putString(KEY_SETTINGS_SEARCH_QUERY, settingsSearchQuery)
}
fun onSettingsSearchQueryChanged(query: String) {
settingsSearchQuery = query
activityView.filterSettings(query)
}
fun enterSettingsSearch(): Boolean {
if (isSettingsSearchActive) {
return false
}
isSettingsSearchActive = true
activityView.filterSettings(settingsSearchQuery)
return true
}
fun exitSettingsSearch(): Boolean {
if (!isSettingsSearchActive) {
return false
}
isSettingsSearchActive = false
settingsSearchQuery = ""
activityView.filterSettings("")
return true
}
fun onDestroy() {
@@ -85,55 +125,49 @@ class SettingsActivityPresenter(
}
fun onMenuTagAction(menuTag: MenuTag, value: Int) {
if (menuTag.isSerialPort1Menu) {
// Not disabled or dummy
if (value != 0 && value != 255) {
val bundle = Bundle()
bundle.putInt(SettingsFragmentPresenter.ARG_SERIALPORT1_TYPE, value)
activityView.showSettingsFragment(menuTag, bundle, true, gameId!!)
}
}
if (menuTag.isGCPadMenu) {
// Not disabled
if (value != 0)
{
val bundle = Bundle()
bundle.putInt(SettingsFragmentPresenter.ARG_CONTROLLER_TYPE, value)
activityView.showSettingsFragment(menuTag, bundle, true, gameId!!)
}
}
if (menuTag.isWiimoteMenu) {
// Emulated Wii Remote
if (value == 1) {
activityView.showSettingsFragment(menuTag, null, true, gameId!!)
}
}
if (menuTag.isWiimoteExtensionMenu) {
// Not disabled
if (value != 0) {
val bundle = Bundle()
bundle.putInt(SettingsFragmentPresenter.ARG_CONTROLLER_TYPE, value)
activityView.showSettingsFragment(menuTag, bundle, true, gameId!!)
}
}
val action = getMenuTagAction(menuTag, value) ?: return
activityView.showSettingsFragment(action.menuTag, action.extras, true, gameId!!)
}
fun hasMenuTagActionForValue(menuTag: MenuTag, value: Int): Boolean {
if (menuTag.isSerialPort1Menu) {
return getMenuTagAction(menuTag, value) != null
}
fun getMenuTagActionExtras(menuTag: MenuTag, value: Int): Bundle? {
return getMenuTagAction(menuTag, value)?.extras
}
private fun getMenuTagAction(menuTag: MenuTag, value: Int): MenuTagAction? {
return when {
// Not disabled or dummy
return value != 0 && value != 255
}
if (menuTag.isGCPadMenu) {
menuTag.isSerialPort1Menu && value != 0 && value != 255 -> MenuTagAction(
menuTag, Bundle().apply {
putInt(SettingsFragmentPresenter.ARG_SERIALPORT1_TYPE, value)
})
// Not disabled
return value != 0
}
if (menuTag.isWiimoteMenu) {
menuTag.isGCPadMenu && value != 0 -> MenuTagAction(
menuTag, Bundle().apply {
putInt(SettingsFragmentPresenter.ARG_CONTROLLER_TYPE, value)
})
// Emulated Wii Remote
return value == 1
}
return if (menuTag.isWiimoteExtensionMenu) {
menuTag.isWiimoteMenu && value == 1 -> MenuTagAction(menuTag, null)
// Not disabled
value != 0
} else false
menuTag.isWiimoteExtensionMenu && value != 0 -> MenuTagAction(
menuTag, Bundle().apply {
putInt(SettingsFragmentPresenter.ARG_CONTROLLER_TYPE, value)
})
else -> null
}
}
private data class MenuTagAction(val menuTag: MenuTag, val extras: Bundle?)
companion object {
private const val KEY_SETTINGS_SEARCH_ACTIVE = "settings_search_active"
private const val KEY_SETTINGS_SEARCH_QUERY = "settings_search_query"
}
}

View File

@@ -10,6 +10,16 @@ import org.dolphinemu.dolphinemu.features.settings.model.Settings
* Abstraction for the Activity that manages SettingsFragments.
*/
interface SettingsActivityView {
/**
* The query currently displayed in the settings search view.
*/
val settingsSearchQuery: String
/**
* Whether the dedicated settings search screen is active.
*/
val isSettingsSearchActive: Boolean
/**
* Show a new SettingsFragment.
*
@@ -17,12 +27,19 @@ interface SettingsActivityView {
* @param addToStack Whether or not this fragment should replace a previous one.
*/
fun showSettingsFragment(
menuTag: MenuTag,
extras: Bundle?,
addToStack: Boolean,
gameId: String
menuTag: MenuTag, extras: Bundle?, addToStack: Boolean, gameId: String
)
/**
* Opens the settings screen containing a search result and scrolls to the result.
*/
fun showSearchResult(menuTag: MenuTag, settingPosition: Int, gameId: String, extras: Bundle?)
/**
* Filters the root settings screen using the current search query.
*/
fun filterSettings(query: String)
/**
* Shows a DialogFragment.
*
@@ -86,6 +103,11 @@ interface SettingsActivityView {
*/
fun hasMenuTagActionForValue(menuTag: MenuTag, value: Int): Boolean
/**
* Returns the arguments used when opening a navigable setting's associated screen.
*/
fun getMenuTagActionExtras(menuTag: MenuTag, value: Int): Bundle?
/**
* Show loading dialog while loading the settings
*/
@@ -102,9 +124,9 @@ interface SettingsActivityView {
fun showGameIniJunkDeletionQuestion()
/**
* Accesses the material toolbar layout and changes the title
* Updates the settings app bar as a single state change.
*/
fun setToolbarTitle(title: String)
fun setToolbarState(title: String, showHeadline: Boolean, showSearch: Boolean)
/**
* Returns whether the input mapping dialog should detect inputs from all devices,
* not just the device configured for the controller.

View File

@@ -27,14 +27,46 @@ import com.google.android.material.slider.Slider
import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.*
import org.dolphinemu.dolphinemu.databinding.DialogAdvancedMappingBinding
import org.dolphinemu.dolphinemu.databinding.DialogInputStringBinding
import org.dolphinemu.dolphinemu.databinding.DialogSliderBinding
import org.dolphinemu.dolphinemu.databinding.ListItemHeaderBinding
import org.dolphinemu.dolphinemu.databinding.ListItemMappingBinding
import org.dolphinemu.dolphinemu.databinding.ListItemSearchResultBinding
import org.dolphinemu.dolphinemu.databinding.ListItemSettingBinding
import org.dolphinemu.dolphinemu.databinding.ListItemSettingSwitchBinding
import org.dolphinemu.dolphinemu.databinding.ListItemSubmenuBinding
import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting
import org.dolphinemu.dolphinemu.features.input.ui.AdvancedMappingDialog
import org.dolphinemu.dolphinemu.features.input.ui.MotionAlertDialog
import org.dolphinemu.dolphinemu.features.input.ui.viewholder.InputMappingControlSettingViewHolder
import org.dolphinemu.dolphinemu.features.settings.model.Settings
import org.dolphinemu.dolphinemu.features.settings.model.view.*
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.*
import org.dolphinemu.dolphinemu.features.settings.model.view.DateTimeChoiceSetting
import org.dolphinemu.dolphinemu.features.settings.model.view.DirectoryPicker
import org.dolphinemu.dolphinemu.features.settings.model.view.FilePicker
import org.dolphinemu.dolphinemu.features.settings.model.view.FloatSliderSetting
import org.dolphinemu.dolphinemu.features.settings.model.view.InputStringSetting
import org.dolphinemu.dolphinemu.features.settings.model.view.IntSliderSetting
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsSearchResult
import org.dolphinemu.dolphinemu.features.settings.model.view.SingleChoiceSetting
import org.dolphinemu.dolphinemu.features.settings.model.view.SingleChoiceSettingDynamicDescriptions
import org.dolphinemu.dolphinemu.features.settings.model.view.SliderSetting
import org.dolphinemu.dolphinemu.features.settings.model.view.StringSingleChoiceSetting
import org.dolphinemu.dolphinemu.features.settings.model.view.SubmenuSetting
import org.dolphinemu.dolphinemu.features.settings.model.view.SwitchSetting
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.DateTimeSettingViewHolder
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.FilePickerViewHolder
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.HeaderHyperLinkViewHolder
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.HeaderViewHolder
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.InputStringSettingViewHolder
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.RunRunnableViewHolder
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHolder
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingsSearchResultViewHolder
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SingleChoiceViewHolder
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SliderViewHolder
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SubmenuViewHolder
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SwitchSettingViewHolder
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper
import org.dolphinemu.dolphinemu.utils.Log
@@ -42,14 +74,13 @@ import org.dolphinemu.dolphinemu.utils.PermissionsHandler
import java.io.File
import java.io.IOException
import java.io.RandomAccessFile
import java.util.*
import java.util.Calendar
import java.util.TimeZone
import kotlin.math.roundToInt
class SettingsAdapter(
private val fragmentView: SettingsFragmentView,
private val context: Context
) :
RecyclerView.Adapter<SettingViewHolder>(), DialogInterface.OnClickListener,
private val fragmentView: SettingsFragmentView, private val context: Context
) : RecyclerView.Adapter<SettingViewHolder>(), DialogInterface.OnClickListener,
Slider.OnChangeListener {
private var settingsList: ArrayList<SettingsItem>? = null
private var clickedItem: SettingsItem? = null
@@ -68,55 +99,59 @@ class SettingsAdapter(
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
SettingsItem.TYPE_HEADER -> HeaderViewHolder(
ListItemHeaderBinding.inflate(inflater, parent, false),
this
ListItemHeaderBinding.inflate(inflater, parent, false), this
)
SettingsItem.TYPE_SWITCH -> SwitchSettingViewHolder(
ListItemSettingSwitchBinding.inflate(inflater, parent, false),
this
ListItemSettingSwitchBinding.inflate(inflater, parent, false), this
)
SettingsItem.TYPE_STRING_SINGLE_CHOICE,
SettingsItem.TYPE_SINGLE_CHOICE_DYNAMIC_DESCRIPTIONS,
SettingsItem.TYPE_SINGLE_CHOICE -> SingleChoiceViewHolder(
ListItemSettingBinding.inflate(inflater, parent, false),
this
SettingsItem.TYPE_STRING_SINGLE_CHOICE, SettingsItem.TYPE_SINGLE_CHOICE_DYNAMIC_DESCRIPTIONS, SettingsItem.TYPE_SINGLE_CHOICE -> SingleChoiceViewHolder(
ListItemSettingBinding.inflate(inflater, parent, false), this
)
SettingsItem.TYPE_SLIDER -> SliderViewHolder(
ListItemSettingBinding.inflate(inflater, parent, false),
this,
context
ListItemSettingBinding.inflate(inflater, parent, false), this, context
)
SettingsItem.TYPE_SUBMENU -> SubmenuViewHolder(
ListItemSubmenuBinding.inflate(inflater, parent, false),
this
ListItemSubmenuBinding.inflate(inflater, parent, false), this
)
SettingsItem.TYPE_INPUT_MAPPING_CONTROL -> InputMappingControlSettingViewHolder(
ListItemMappingBinding.inflate(inflater, parent, false),
this
ListItemMappingBinding.inflate(inflater, parent, false), this
)
SettingsItem.TYPE_FILE_PICKER,
SettingsItem.TYPE_DIRECTORY_PICKER -> FilePickerViewHolder(
ListItemSettingBinding.inflate(inflater, parent, false),
this
SettingsItem.TYPE_FILE_PICKER, SettingsItem.TYPE_DIRECTORY_PICKER -> FilePickerViewHolder(
ListItemSettingBinding.inflate(inflater, parent, false), this
)
SettingsItem.TYPE_RUN_RUNNABLE -> RunRunnableViewHolder(
ListItemSettingBinding.inflate(inflater, parent, false),
this, context
ListItemSettingBinding.inflate(inflater, parent, false), this, context
)
SettingsItem.TYPE_STRING -> InputStringSettingViewHolder(
ListItemSettingBinding.inflate(inflater, parent, false), this
)
SettingsItem.TYPE_HYPERLINK_HEADER -> HeaderHyperLinkViewHolder(
ListItemHeaderBinding.inflate(inflater, parent, false), this
)
SettingsItem.TYPE_DATETIME_CHOICE -> DateTimeSettingViewHolder(
ListItemSettingBinding.inflate(inflater, parent, false), this
)
SettingsItem.TYPE_SEARCH_RESULT -> SettingsSearchResultViewHolder(
ListItemSearchResultBinding.inflate(inflater, parent, false), this
)
else -> throw IllegalArgumentException("Invalid view type: $viewType")
}
}
override fun onBindViewHolder(holder: SettingViewHolder, position: Int) {
holder.clearSearchResultHighlight()
holder.bind(getItem(position))
}
@@ -143,7 +178,7 @@ class SettingsAdapter(
fun clearSetting(item: SettingsItem) {
item.clear(settings!!)
fragmentView.onSettingChanged()
fragmentView.onSettingChanged(item)
}
fun notifyAllSettingsChanged() {
@@ -153,7 +188,7 @@ class SettingsAdapter(
fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
item.setChecked(settings!!, checked)
fragmentView.onSettingChanged()
fragmentView.onSettingChanged(item)
}
fun onInputStringClick(item: InputStringSetting, position: Int) {
@@ -161,29 +196,24 @@ class SettingsAdapter(
val binding = DialogInputStringBinding.inflate(inflater)
val input = binding.input
input.setText(item.selectedValue)
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
.setView(binding.root)
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setView(binding.root)
.setMessage(item.description)
.setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int ->
val editTextInput = input.text.toString()
if (item.selectedValue != editTextInput) {
notifyItemChanged(position)
fragmentView.onSettingChanged()
fragmentView.onSettingChanged(item)
}
item.setSelectedValue(fragmentView.settings!!, editTextInput)
}
.setNegativeButton(R.string.cancel, null)
.show()
}.setNegativeButton(R.string.cancel, null).show()
}
fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
clickedItem = item
clickedPosition = position
val value = getSelectionForSingleChoiceValue(item)
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
.setTitle(item.name)
.setSingleChoiceItems(item.choicesId, value, this)
.show()
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
.setSingleChoiceItems(item.choicesId, value, this).show()
}
fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) {
@@ -193,35 +223,26 @@ class SettingsAdapter(
val choices = item.choices
val noChoicesAvailableString = item.noChoicesAvailableString
dialog = if (noChoicesAvailableString != 0 && choices.isEmpty()) {
MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
.setTitle(item.name)
.setMessage(noChoicesAvailableString)
.setPositiveButton(R.string.ok, null)
.show()
MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
.setMessage(noChoicesAvailableString).setPositiveButton(R.string.ok, null).show()
} else {
MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
.setTitle(item.name)
MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
.setSingleChoiceItems(
item.choices, item.selectedValueIndex,
this
)
.show()
item.choices, item.selectedValueIndex, this
).show()
}
}
fun onSingleChoiceDynamicDescriptionsClick(
item: SingleChoiceSettingDynamicDescriptions,
position: Int
item: SingleChoiceSettingDynamicDescriptions, position: Int
) {
clickedItem = item
clickedPosition = position
val value = getSelectionForSingleChoiceDynamicDescriptionsValue(item)
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
.setTitle(item.name)
.setSingleChoiceItems(item.choicesId, value, this)
.show()
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
.setSingleChoiceItems(item.choicesId, value, this).show()
}
fun onSliderClick(item: SliderSetting, position: Int) {
@@ -251,6 +272,7 @@ class SettingsAdapter(
slider.valueTo = item.max
slider.stepSize = item.stepSize
}
is IntSliderSetting -> {
slider.valueFrom = item.min.toFloat()
slider.valueTo = item.max.toFloat()
@@ -260,29 +282,27 @@ class SettingsAdapter(
slider.value = (seekbarProgress / slider.stepSize).roundToInt() * slider.stepSize
slider.addOnChangeListener(this)
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
.setTitle(item.name)
.setView(binding.root)
.setPositiveButton(R.string.ok, this)
.show()
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
.setView(binding.root).setPositiveButton(R.string.ok, this).show()
}
fun onSubmenuClick(item: SubmenuSetting) {
fragmentView.loadSubMenu(item.menuKey)
}
fun onSearchResultClick(item: SettingsSearchResult) {
fragmentView.loadSearchResult(item.menuKey, item.settingPosition, item.navigationExtras)
}
fun onInputMappingClick(item: InputMappingControlSetting, position: Int) {
if (item.controller.getDefaultDevice().isEmpty() && !fragmentView.isMappingAllDevices) {
MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
.setMessage(R.string.input_binding_no_device)
.setPositiveButton(R.string.ok, this)
.show()
MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setMessage(R.string.input_binding_no_device)
.setPositiveButton(R.string.ok, this).show()
return
}
val dialog = MotionAlertDialog(
fragmentView.fragmentActivity, item,
fragmentView.isMappingAllDevices
fragmentView.fragmentActivity, item, fragmentView.isMappingAllDevices
)
val background = ContextCompat.getDrawable(context, R.drawable.dialog_round)
@@ -296,18 +316,16 @@ class SettingsAdapter(
dialog.setTitle(R.string.input_binding)
dialog.setMessage(
String.format(
context.getString(R.string.input_binding_description),
item.name
context.getString(R.string.input_binding_description), item.name
)
)
dialog.setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(R.string.cancel), this)
dialog.setButton(
AlertDialog.BUTTON_NEUTRAL,
context.getString(R.string.clear)
AlertDialog.BUTTON_NEUTRAL, context.getString(R.string.clear)
) { _: DialogInterface?, _: Int -> item.clearValue() }
dialog.setOnDismissListener {
notifyItemChanged(position)
fragmentView.onSettingChanged()
fragmentView.onSettingChanged(item)
}
dialog.setCanceledOnTouchOutside(false)
dialog.show()
@@ -317,10 +335,7 @@ class SettingsAdapter(
val inflater = LayoutInflater.from(context)
val binding = DialogAdvancedMappingBinding.inflate(inflater)
val dialog = AdvancedMappingDialog(
context,
binding,
item.controlReference,
item.controller
context, binding, item.controlReference, item.controller
)
val background = ContextCompat.getDrawable(context, R.drawable.dialog_round)
@@ -338,12 +353,11 @@ class SettingsAdapter(
) { _: DialogInterface?, _: Int ->
item.value = dialog.expression
notifyItemChanged(position)
fragmentView.onSettingChanged()
fragmentView.onSettingChanged(item)
}
dialog.setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(R.string.cancel), this)
dialog.setButton(
AlertDialog.BUTTON_NEUTRAL,
context.getString(R.string.clear)
AlertDialog.BUTTON_NEUTRAL, context.getString(R.string.clear)
) { _: DialogInterface?, _: Int -> }
dialog.setCanceledOnTouchOutside(false)
dialog.show()
@@ -361,14 +375,12 @@ class SettingsAdapter(
val directoryPicker = item as DirectoryPicker
if (!PermissionsHandler.isExternalStorageLegacy()) {
MaterialAlertDialogBuilder(context)
.setMessage(R.string.path_not_changeable_scoped_storage)
MaterialAlertDialogBuilder(context).setMessage(R.string.path_not_changeable_scoped_storage)
.setPositiveButton(R.string.ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
.show()
} else {
val intent = FileBrowserHelper.createDirectoryPickerIntent(
fragmentView.fragmentActivity,
FileBrowserHelper.GAME_EXTENSIONS
fragmentView.fragmentActivity, FileBrowserHelper.GAME_EXTENSIONS
)
directoryPicker.launcher.launch(intent)
}
@@ -400,32 +412,24 @@ class SettingsAdapter(
calendar.timeZone = TimeZone.getTimeZone("UTC")
// Start and end epoch times available for the Wii's date picker
val calendarConstraints = CalendarConstraints.Builder()
.setStart(946684800000L)
.setEnd(2082672000000L)
.build()
val calendarConstraints =
CalendarConstraints.Builder().setStart(946684800000L).setEnd(2082672000000L).build()
var timeFormat = TimeFormat.CLOCK_12H
if (DateFormat.is24HourFormat(fragmentView.fragmentActivity)) {
timeFormat = TimeFormat.CLOCK_24H
}
val datePicker = MaterialDatePicker.Builder.datePicker()
.setSelection(storedTime)
.setTitleText(R.string.select_rtc_date)
.setCalendarConstraints(calendarConstraints)
.build()
val timePicker = MaterialTimePicker.Builder()
.setTimeFormat(timeFormat)
.setHour(calendar[Calendar.HOUR_OF_DAY])
.setMinute(calendar[Calendar.MINUTE])
.setTitleText(R.string.select_rtc_time)
val datePicker = MaterialDatePicker.Builder.datePicker().setSelection(storedTime)
.setTitleText(R.string.select_rtc_date).setCalendarConstraints(calendarConstraints)
.build()
val timePicker = MaterialTimePicker.Builder().setTimeFormat(timeFormat)
.setHour(calendar[Calendar.HOUR_OF_DAY]).setMinute(calendar[Calendar.MINUTE])
.setTitleText(R.string.select_rtc_time).build()
datePicker.addOnPositiveButtonClickListener {
timePicker.show(
fragmentView.fragmentActivity.supportFragmentManager,
"TimePicker"
fragmentView.fragmentActivity.supportFragmentManager, "TimePicker"
)
}
timePicker.addOnPositiveButtonClickListener {
@@ -435,7 +439,7 @@ class SettingsAdapter(
val rtcString = "0x" + java.lang.Long.toHexString(epochTime)
if (item.getSelectedValue() != rtcString) {
notifyItemChanged(clickedPosition)
fragmentView.onSettingChanged()
fragmentView.onSettingChanged(item)
}
item.setSelectedValue(fragmentView.settings!!, rtcString)
clickedItem = null
@@ -448,7 +452,7 @@ class SettingsAdapter(
if (filePicker.getSelectedValue() != selectedFile) {
notifyItemChanged(clickedPosition)
fragmentView.onSettingChanged()
fragmentView.onSettingChanged(filePicker)
}
filePicker.setSelectedValue(fragmentView.settings!!, selectedFile)
@@ -470,44 +474,50 @@ class SettingsAdapter(
val scSetting = clickedItem as SingleChoiceSetting
val value = getValueForSingleChoiceSelection(scSetting, which)
if (scSetting.selectedValue != value) fragmentView.onSettingChanged()
if (scSetting.selectedValue != value) fragmentView.onSettingChanged(scSetting)
scSetting.setSelectedValue(settings!!, value)
closeDialog()
}
is SingleChoiceSettingDynamicDescriptions -> {
val scSetting = clickedItem as SingleChoiceSettingDynamicDescriptions
val value = getValueForSingleChoiceDynamicDescriptionsSelection(scSetting, which)
if (scSetting.selectedValue != value) fragmentView.onSettingChanged()
if (scSetting.selectedValue != value) fragmentView.onSettingChanged(scSetting)
scSetting.setSelectedValue(settings!!, value)
closeDialog()
}
is StringSingleChoiceSetting -> {
val scSetting = clickedItem as StringSingleChoiceSetting
val value = scSetting.getValueAt(which)
if (scSetting.selectedValue != value) fragmentView.onSettingChanged()
if (scSetting.selectedValue != value) fragmentView.onSettingChanged(scSetting)
scSetting.setSelectedValue(settings!!, value)
closeDialog()
}
is IntSliderSetting -> {
val sliderSetting = clickedItem as IntSliderSetting
if (sliderSetting.selectedValue != seekbarProgress.toInt()) {
fragmentView.onSettingChanged()
fragmentView.onSettingChanged(sliderSetting)
}
sliderSetting.setSelectedValue(settings!!, seekbarProgress.toInt())
closeDialog()
}
is FloatSliderSetting -> {
val sliderSetting = clickedItem as FloatSliderSetting
if (sliderSetting.selectedValue != seekbarProgress) fragmentView.onSettingChanged()
if (sliderSetting.selectedValue != seekbarProgress) {
fragmentView.onSettingChanged(sliderSetting)
}
sliderSetting.setSelectedValue(settings!!, seekbarProgress)
@@ -540,6 +550,7 @@ class SettingsAdapter(
override fun onViewRecycled(holder: SettingViewHolder) {
super.onViewRecycled(holder)
holder.clearSearchResultHighlight()
holder.onViewRecycled()
}
@@ -587,8 +598,7 @@ class SettingsAdapter(
}
private fun getValueForSingleChoiceDynamicDescriptionsSelection(
item: SingleChoiceSettingDynamicDescriptions,
which: Int
item: SingleChoiceSettingDynamicDescriptions, which: Int
): Int {
val valuesId = item.valuesId
return if (valuesId > 0) {

View File

@@ -24,17 +24,22 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.FragmentSettingsBinding
import org.dolphinemu.dolphinemu.features.settings.model.Settings
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHolder
import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult
import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable
import java.util.*
import kotlin.collections.ArrayList
import java.util.EnumMap
class SettingsFragment : Fragment(), SettingsFragmentView {
private lateinit var presenter: SettingsFragmentPresenter
@@ -51,6 +56,11 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
SettingsActivityResultLaunchers(this) { adapter }
private var oldControllerSettingsWarningHeight = 0
private var hasScrolledToSearchResult = false
private var highlightedSearchResult: SettingsItem? = null
private var highlightedSearchResultPosition = RecyclerView.NO_POSITION
private var searchIndexWarmupJob: Job? = null
private var searchJob: Job? = null
private var binding: FragmentSettingsBinding? = null
@@ -82,9 +92,7 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
binding = FragmentSettingsBinding.inflate(inflater, container, false)
return binding!!.root
@@ -92,7 +100,11 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (titles.containsKey(menuTag)) {
activityView!!.setToolbarTitle(getString(titles[menuTag]!!))
activityView!!.setToolbarState(
getString(titles[menuTag]!!),
menuTag != MenuTag.SETTINGS,
menuTag == MenuTag.SETTINGS
)
}
val manager = LinearLayoutManager(activity)
@@ -107,10 +119,13 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
setInsets()
val activity = requireActivity() as SettingsActivityView
presenter.invalidateSearchIndex()
presenter.onViewCreated(menuTag, activity.settings)
}
override fun onDestroyView() {
clearSearchResultHighlight()
searchJob?.cancel()
super.onDestroyView()
binding = null
}
@@ -129,7 +144,81 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
}
override fun showSettingsList(settingsList: ArrayList<SettingsItem>) {
adapter!!.setSettings(settingsList)
val query = activityView?.settingsSearchQuery.orEmpty()
val isShowingSearch =
menuTag == MenuTag.SETTINGS && activityView?.isSettingsSearchActive == true
if (!isShowingSearch) {
adapter!!.setSettings(settingsList)
}
if (menuTag == MenuTag.SETTINGS) {
warmUpSearchIndex()
if (isShowingSearch) {
applySettingsFilter(query)
}
}
val position = arguments?.getInt(
ARGUMENT_SCROLL_TO_SETTING_POSITION, RecyclerView.NO_POSITION
) ?: RecyclerView.NO_POSITION
if (!hasScrolledToSearchResult && position in settingsList.indices) {
hasScrolledToSearchResult = true
binding?.listSettings?.post {
val recyclerView = binding?.listSettings ?: return@post
(recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(
position, 0
)
highlightSearchResult(position, settingsList[position])
}
}
}
fun filterSettings(query: String) {
if (!this::presenter.isInitialized || presenter.settings == null) {
return
}
applySettingsFilter(query)
}
private fun applySettingsFilter(query: String) {
searchJob?.cancel()
if (query.isBlank()) {
val results = if (activityView?.isSettingsSearchActive == true) {
arrayListOf()
} else {
presenter.getSettingsList()
}
showSearchResults(query, results)
return
}
searchJob = viewLifecycleOwner.lifecycleScope.launch {
delay(SEARCH_QUERY_DEBOUNCE_MS)
val results = presenter.searchSettings(query)
if (activityView?.settingsSearchQuery == query) {
showSearchResults(query, results)
}
}
}
private fun warmUpSearchIndex() {
if (searchIndexWarmupJob?.isActive == true) {
return
}
searchIndexWarmupJob = viewLifecycleOwner.lifecycleScope.launch {
presenter.prepareSearchIndex()
}
}
private fun showSearchResults(query: String, results: ArrayList<SettingsItem>) {
adapter!!.setSettings(results)
binding?.textNoSearchResults?.text =
getString(R.string.search_settings_no_results, query.trim())
binding?.textNoSearchResults?.visibility =
if (query.isNotBlank() && results.isEmpty()) View.VISIBLE else View.GONE
binding?.listSettings?.visibility =
if (query.isNotBlank() && results.isEmpty()) View.GONE else View.VISIBLE
}
override fun loadSubMenu(menuKey: MenuTag) {
@@ -139,13 +228,35 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
}
activityView!!.showSettingsFragment(
menuKey,
null,
true,
requireArguments().getString(ARGUMENT_GAME_ID)!!
menuKey, null, true, requireArguments().getString(ARGUMENT_GAME_ID)!!
)
}
override fun loadSearchResult(menuKey: MenuTag, settingPosition: Int, extras: Bundle?) {
activityView!!.showSearchResult(
menuKey, settingPosition, requireArguments().getString(ARGUMENT_GAME_ID)!!, extras
)
}
private fun highlightSearchResult(position: Int, setting: SettingsItem) {
val recyclerView = binding?.listSettings ?: return
highlightedSearchResult = setting
highlightedSearchResultPosition = position
recyclerView.post {
(recyclerView.findViewHolderForAdapterPosition(position) as? SettingViewHolder)
?.highlightSearchResult()
}
}
private fun clearSearchResultHighlight() {
val recyclerView = binding?.listSettings
(recyclerView?.findViewHolderForAdapterPosition(
highlightedSearchResultPosition
) as? SettingViewHolder)?.clearSearchResultHighlight()
highlightedSearchResult = null
highlightedSearchResultPosition = RecyclerView.NO_POSITION
}
override fun showDialogFragment(fragment: DialogFragment) {
activityView!!.showDialogFragment(fragment)
}
@@ -157,7 +268,11 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
override val settings: Settings?
get() = presenter.settings
override fun onSettingChanged() {
override fun onSettingChanged(setting: SettingsItem?) {
if (setting == null || setting === highlightedSearchResult) {
clearSearchResultHighlight()
}
presenter.invalidateSearchIndex()
activityView!!.onSettingChanged()
}
@@ -174,6 +289,10 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
return activityView!!.hasMenuTagActionForValue(menuTag, value)
}
override fun getMenuTagActionExtras(menuTag: MenuTag, value: Int): Bundle? {
return activityView!!.getMenuTagActionExtras(menuTag, value)
}
override var isMappingAllDevices: Boolean
get() = activityView!!.isMappingAllDevices
set(allDevices) {
@@ -203,17 +322,13 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
}
val msg = "${presenter.gpuDriver!!.name} ${presenter.gpuDriver!!.driverVersion}"
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.gpu_driver_dialog_title))
.setMessage(msg)
.setNegativeButton(android.R.string.cancel, null)
MaterialAlertDialogBuilder(requireContext()).setTitle(getString(R.string.gpu_driver_dialog_title))
.setMessage(msg).setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(R.string.gpu_driver_dialog_system) { _: DialogInterface?, _: Int ->
presenter.useSystemDriver()
}
.setPositiveButton(R.string.gpu_driver_dialog_install) { _: DialogInterface?, _: Int ->
}.setPositiveButton(R.string.gpu_driver_dialog_install) { _: DialogInterface?, _: Int ->
askForDriverFile()
}
.show()
}.show()
}
override fun getFragmentLifecycle(): Lifecycle {
@@ -230,16 +345,12 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
override fun onDriverInstallDone(result: GpuDriverInstallResult) {
val view = binding?.root ?: return
Snackbar
.make(view, resolveInstallResultString(result), Snackbar.LENGTH_LONG)
.show()
Snackbar.make(view, resolveInstallResultString(result), Snackbar.LENGTH_LONG).show()
}
override fun onDriverUninstallDone() {
Toast.makeText(
requireContext(),
R.string.gpu_driver_dialog_uninstall_done,
Toast.LENGTH_SHORT
requireContext(), R.string.gpu_driver_dialog_uninstall_done, Toast.LENGTH_SHORT
).show()
}
@@ -256,6 +367,8 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
companion object {
private const val ARGUMENT_MENU_TAG = "menu_tag"
private const val ARGUMENT_GAME_ID = "game_id"
const val ARGUMENT_SCROLL_TO_SETTING_POSITION = "scroll_to_setting_position"
private const val SEARCH_QUERY_DEBOUNCE_MS = 120L
private val titles: MutableMap<MenuTag, Int> = EnumMap(MenuTag::class.java)
init {

View File

@@ -2,6 +2,7 @@
package org.dolphinemu.dolphinemu.features.settings.ui
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
@@ -50,6 +51,12 @@ interface SettingsFragmentView {
* @param menuKey Identifier for the settings group that should be shown.
*/
fun loadSubMenu(menuKey: MenuTag)
/**
* Opens the settings screen containing a search result and scrolls to the result.
*/
fun loadSearchResult(menuKey: MenuTag, settingPosition: Int, extras: Bundle?)
fun showDialogFragment(fragment: DialogFragment)
/**
@@ -67,7 +74,7 @@ interface SettingsFragmentView {
/**
* Have the fragment tell the containing Activity that a Setting was modified.
*/
fun onSettingChanged()
fun onSettingChanged(setting: SettingsItem? = null)
/**
* Refetches the values of all controller settings.
@@ -95,6 +102,11 @@ interface SettingsFragmentView {
*/
fun hasMenuTagActionForValue(menuTag: MenuTag, value: Int): Boolean
/**
* Returns the arguments used when opening a navigable setting's associated screen.
*/
fun getMenuTagActionExtras(menuTag: MenuTag, value: Int): Bundle?
/**
* Controls whether the input mapping dialog should detect inputs from all devices,
* not just the device configured for the controller.

View File

@@ -2,14 +2,20 @@
package org.dolphinemu.dolphinemu.features.settings.ui.viewholder
import android.animation.ValueAnimator
import android.content.DialogInterface
import android.graphics.Paint
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.view.View
import android.view.View.OnLongClickListener
import android.view.animation.DecelerateInterpolator
import android.widget.TextView
import android.widget.Toast
import androidx.lifecycle.LifecycleOwner
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.dolphinemu.dolphinemu.DolphinApplication
import org.dolphinemu.dolphinemu.R
@@ -21,6 +27,9 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
LifecycleViewHolder(itemView, adapter.getFragmentLifecycle()),
LifecycleOwner, View.OnClickListener, OnLongClickListener {
private val defaultBackground: Drawable? = itemView.background
private var searchResultHighlightAnimator: ValueAnimator? = null
init {
itemView.setOnClickListener(this)
itemView.setOnLongClickListener(this)
@@ -39,6 +48,35 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
}
}
fun highlightSearchResult() {
clearSearchResultHighlight()
val highlight = ColorDrawable(
MaterialColors.getColor(
itemView, com.google.android.material.R.attr.colorSecondaryContainer
)
).apply { alpha = 0 }
itemView.background = if (defaultBackground == null) {
highlight
} else {
LayerDrawable(arrayOf(highlight, defaultBackground))
}
searchResultHighlightAnimator = ValueAnimator.ofInt(
0, SEARCH_RESULT_HIGHLIGHT_MAX_ALPHA
).apply {
duration = SEARCH_RESULT_HIGHLIGHT_FADE_IN_DURATION_MS
interpolator = DecelerateInterpolator()
addUpdateListener { highlight.alpha = it.animatedValue as Int }
start()
}
}
fun clearSearchResultHighlight() {
searchResultHighlightAnimator?.cancel()
searchResultHighlightAnimator = null
itemView.background = defaultBackground
}
/**
* Called by the adapter to set this ViewHolder's child views to display the list item
* it must now represent.
@@ -102,4 +140,9 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
Toast.LENGTH_SHORT
).show()
}
companion object {
private const val SEARCH_RESULT_HIGHLIGHT_FADE_IN_DURATION_MS = 180L
private const val SEARCH_RESULT_HIGHLIGHT_MAX_ALPHA = 255
}
}

View File

@@ -0,0 +1,28 @@
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.settings.ui.viewholder
import android.view.View
import org.dolphinemu.dolphinemu.databinding.ListItemSearchResultBinding
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsSearchResult
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter
class SettingsSearchResultViewHolder(
private val binding: ListItemSearchResultBinding, adapter: SettingsAdapter
) : SettingViewHolder(binding.root, adapter) {
private lateinit var result: SettingsSearchResult
override val item: SettingsItem
get() = result
override fun bind(item: SettingsItem) {
result = item as SettingsSearchResult
binding.textSettingName.text = item.name
binding.textSettingDescription.text = item.description
}
override fun onClick(clicked: View) {
adapter.onSearchResultClick(result)
}
}

View File

@@ -1,20 +1,20 @@
package org.dolphinemu.dolphinemu.utils
import androidx.appcompat.app.AppCompatActivity
import org.dolphinemu.dolphinemu.R
import android.os.Build
import androidx.core.content.ContextCompat
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.WindowCompat
import org.dolphinemu.dolphinemu.ui.main.ThemeProvider
import android.content.res.Configuration
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.elevation.ElevationOverlayProvider
import com.google.android.material.color.MaterialColors
import android.os.Build
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.preference.PreferenceManager
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.color.MaterialColors
import com.google.android.material.elevation.ElevationOverlayProvider
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.ui.main.ThemeProvider
object ThemeHelper {
@@ -52,8 +52,7 @@ object ThemeHelper {
.getInt(CURRENT_THEME_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
activity.delegate.localNightMode = themeMode
val windowController = WindowCompat.getInsetsController(
activity.window,
activity.window.decorView
activity.window, activity.window.decorView
)
val systemReportedThemeMode =
activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
@@ -62,6 +61,7 @@ object ThemeHelper {
Configuration.UI_MODE_NIGHT_NO -> setLightModeSystemBars(windowController)
Configuration.UI_MODE_NIGHT_YES -> setDarkModeSystemBars(windowController)
}
AppCompatDelegate.MODE_NIGHT_NO -> setLightModeSystemBars(windowController)
AppCompatDelegate.MODE_NIGHT_YES -> setDarkModeSystemBars(windowController)
}
@@ -83,66 +83,50 @@ object ThemeHelper {
@JvmStatic
fun saveTheme(activity: AppCompatActivity, themeValue: Int) {
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
.edit()
.putInt(CURRENT_THEME, themeValue)
.apply()
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
.putInt(CURRENT_THEME, themeValue).apply()
activity.recreate()
}
@JvmStatic
fun deleteThemeKey(activity: AppCompatActivity) {
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
.edit()
.remove(CURRENT_THEME)
.apply()
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
.remove(CURRENT_THEME).apply()
activity.recreate()
}
@JvmStatic
fun saveThemeMode(activity: AppCompatActivity, themeModeValue: Int) {
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
.edit()
.putInt(CURRENT_THEME_MODE, themeModeValue)
.apply()
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
.putInt(CURRENT_THEME_MODE, themeModeValue).apply()
setThemeMode(activity)
}
@JvmStatic
fun deleteThemeModeKey(activity: AppCompatActivity) {
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
.edit()
.remove(CURRENT_THEME_MODE)
.apply()
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
.remove(CURRENT_THEME_MODE).apply()
setThemeMode(activity)
}
@JvmStatic
fun saveBackgroundSetting(activity: AppCompatActivity, backgroundValue: Boolean) {
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
.edit()
.putBoolean(USE_BLACK_BACKGROUNDS, backgroundValue)
.apply()
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
.putBoolean(USE_BLACK_BACKGROUNDS, backgroundValue).apply()
activity.recreate()
}
@JvmStatic
fun deleteBackgroundSetting(activity: AppCompatActivity) {
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
.edit()
.remove(USE_BLACK_BACKGROUNDS)
.apply()
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
.remove(USE_BLACK_BACKGROUNDS).apply()
activity.recreate()
}
@JvmStatic
fun resetThemePreferences(activity: AppCompatActivity, applyImmediately: Boolean = false) {
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
.edit()
.remove(CURRENT_THEME)
.remove(CURRENT_THEME_MODE)
.remove(USE_BLACK_BACKGROUNDS)
.apply()
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
.remove(CURRENT_THEME).remove(CURRENT_THEME_MODE).remove(USE_BLACK_BACKGROUNDS).apply()
activity.delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
activity.delegate.applyDayNight()
if (applyImmediately) {
@@ -170,7 +154,7 @@ object ThemeHelper {
activity: AppCompatActivity, toolbar: MaterialToolbar, appBarLayout: AppBarLayout
) {
appBarLayout.addOnOffsetChangedListener { layout: AppBarLayout, verticalOffset: Int ->
if (-verticalOffset >= layout.totalScrollRange / 2) {
if (layout.totalScrollRange > 0 && -verticalOffset >= layout.totalScrollRange / 2) {
@ColorInt val color =
ElevationOverlayProvider(appBarLayout.context).compositeOverlay(
MaterialColors.getColor(appBarLayout, R.attr.colorSurface),
@@ -180,8 +164,7 @@ object ThemeHelper {
setStatusBarColor(activity, color)
} else {
@ColorInt val statusBarColor = ContextCompat.getColor(
activity.applicationContext,
android.R.color.transparent
activity.applicationContext, android.R.color.transparent
)
@ColorInt val appBarColor = MaterialColors.getColor(toolbar, R.attr.colorSurface)
toolbar.setBackgroundColor(appBarColor)
@@ -198,8 +181,7 @@ object ThemeHelper {
setStatusBarColor(activity, color)
} else {
@ColorInt val statusBarColor = ContextCompat.getColor(
activity.applicationContext,
android.R.color.transparent
activity.applicationContext, android.R.color.transparent
)
setStatusBarColor(activity, statusBarColor)
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="180"
android:fromAlpha="0"
android:interpolator="@android:anim/decelerate_interpolator"
android:startOffset="60"
android:toAlpha="1" />

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="90"
android:fromAlpha="1"
android:interpolator="@android:anim/accelerate_interpolator"
android:toAlpha="0" />

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="0.45" android:color="?attr/colorOutline" />
</selector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M9.5,3a6.5,6.5 0,1 0,0 13a6.5,6.5 0,0 0,0 -13zM9.5,5a4.5,4.5 0,1 1,0 9a4.5,4.5 0,0 1,0 -9zM14.65,13.24l5.56,5.56l-1.41,1.41l-5.56,-5.56z" />
</vector>

View File

@@ -1,22 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
<androidx.coordinatorlayout.widget.CoordinatorLayout android:id="@+id/coordinator_main"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<FrameLayout
android:id="@+id/frame_content_settings"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<TextView
android:id="@+id/old_controller_settings_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="?attr/colorErrorContainer"
android:clickable="true"
android:focusable="false"
android:text="@string/old_controller_settings"
android:textColor="?attr/colorOnErrorContainer"
android:visibility="invisible" />
<View
android:id="@+id/workaround_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="bottom"
android:background="@android:color/transparent"
android:clickable="true"
android:focusable="false" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="@android:color/transparent"
app:backgroundTint="@android:color/transparent"
app:elevation="0dp">
<com.google.android.material.appbar.CollapsingToolbarLayout
style="?attr/collapsingToolbarLayoutMediumStyle"
android:id="@+id/toolbar_settings_layout"
style="?attr/collapsingToolbarLayoutMediumStyle"
android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
app:contentScrim="@android:color/transparent"
@@ -31,33 +59,94 @@
</com.google.android.material.appbar.CollapsingToolbarLayout>
<FrameLayout
android:id="@+id/settings_search_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
android:paddingBottom="@dimen/spacing_medlarge">
<com.google.android.material.card.MaterialCardView
android:id="@+id/settings_search_preview"
android:layout_width="match_parent"
android:layout_height="56dp"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
android:layout_marginEnd="@dimen/spacing_large"
android:layout_marginStart="@dimen/spacing_large"
app:cardBackgroundColor="?attr/colorSurfaceVariant"
app:cardCornerRadius="28dp"
app:cardElevation="0dp"
app:strokeColor="@color/settings_search_outline"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingEnd="@dimen/spacing_large"
android:paddingStart="20dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@null"
app:srcCompat="@drawable/ic_search"
app:tint="?android:attr/textColorSecondary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_large"
android:text="@string/search_settings"
android:textColor="?android:attr/textColorSecondary"
android:textSize="18sp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</FrameLayout>
<LinearLayout
android:id="@+id/settings_search_mode_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
android:orientation="vertical"
android:visibility="gone">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/settings_search_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
app:navigationContentDescription="@string/search_settings_back"
app:navigationIcon="?attr/homeAsUpIndicator">
<androidx.appcompat.widget.SearchView
android:id="@+id/settings_search"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:imeOptions="actionSearch"
android:inputType="text"
app:iconifiedByDefault="false"
app:queryBackground="@android:color/transparent"
app:queryHint="@string/search_settings"
app:searchIcon="@null" />
</com.google.android.material.appbar.MaterialToolbar>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/settings_search_outline" />
</LinearLayout>
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="@+id/frame_content_settings"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<TextView
android:id="@+id/old_controller_settings_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="?attr/colorErrorContainer"
android:text="@string/old_controller_settings"
android:textColor="?attr/colorOnErrorContainer"
android:visibility="invisible"
android:clickable="true"
android:focusable="false" />
<View
android:id="@+id/workaround_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="bottom"
android:clickable="true"
android:focusable="false"
android:background="@android:color/transparent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -11,4 +10,16 @@
android:layout_height="match_parent"
android:clipToPadding="false" />
<TextView
android:id="@+id/text_no_search_results"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:padding="@dimen/spacing_xtralarge"
android:text="@string/search_settings_no_results"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="?android:attr/textColorSecondary"
android:visibility="gone" />
</FrameLayout>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:minHeight="64dp"
android:orientation="vertical"
android:paddingBottom="@dimen/spacing_large"
android:paddingEnd="@dimen/spacing_large"
android:paddingStart="@dimen/spacing_large"
android:paddingTop="@dimen/spacing_large">
<TextView
android:id="@+id/text_setting_name"
style="@style/TextAppearance.MaterialComponents.Headline5"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="viewStart"
android:textSize="16sp"
tools:text="Internal Resolution" />
<TextView
android:id="@+id/text_setting_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_small"
android:ellipsize="end"
android:maxLines="1"
android:textAlignment="viewStart"
android:textColor="?android:attr/textColorSecondary"
tools:text="Graphics Settings Enhancements" />
</LinearLayout>

View File

@@ -62,6 +62,10 @@
<!-- Main Preference Fragment -->
<string name="settings">Settings</string>
<string name="search_settings">Search settings</string>
<string name="search_settings_back">Back to settings</string>
<string name="search_settings_no_results">No settings found for “%1$s”</string>
<string name="search_settings_category_path">%1$s %2$s</string>
<string name="game_settings">Game Settings: %1$s</string>
<string name="config">Config</string>
<string name="graphics_settings">Graphics Settings</string>