Android: Add global settings search

This commit is contained in:
Simonx22
2026-07-25 11:36:57 -04:00
parent 707d3c7a73
commit 893be0996e
20 changed files with 1185 additions and 487 deletions
@@ -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
}
}
@@ -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
}
@@ -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,84 @@ 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 +213,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 +232,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 +252,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 +284,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 +295,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 +326,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 +436,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 +460,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)
}
@@ -18,6 +18,10 @@ class SettingsActivityPresenter(
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 +36,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 +126,58 @@ 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"
}
}
@@ -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.
*
@@ -23,6 +33,16 @@ interface SettingsActivityView {
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 +106,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 +127,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.
@@ -112,11 +112,15 @@ class SettingsAdapter(
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 +147,7 @@ class SettingsAdapter(
fun clearSetting(item: SettingsItem) {
item.clear(settings!!)
fragmentView.onSettingChanged()
fragmentView.onSettingChanged(item)
}
fun notifyAllSettingsChanged() {
@@ -153,7 +157,7 @@ class SettingsAdapter(
fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
item.setChecked(settings!!, checked)
fragmentView.onSettingChanged()
fragmentView.onSettingChanged(item)
}
fun onInputStringClick(item: InputStringSetting, position: Int) {
@@ -168,7 +172,7 @@ class SettingsAdapter(
val editTextInput = input.text.toString()
if (item.selectedValue != editTextInput) {
notifyItemChanged(position)
fragmentView.onSettingChanged()
fragmentView.onSettingChanged(item)
}
item.setSelectedValue(fragmentView.settings!!, editTextInput)
}
@@ -271,6 +275,10 @@ class SettingsAdapter(
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)
@@ -307,7 +315,7 @@ class SettingsAdapter(
) { _: DialogInterface?, _: Int -> item.clearValue() }
dialog.setOnDismissListener {
notifyItemChanged(position)
fragmentView.onSettingChanged()
fragmentView.onSettingChanged(item)
}
dialog.setCanceledOnTouchOutside(false)
dialog.show()
@@ -338,7 +346,7 @@ 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(
@@ -435,7 +443,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 +456,7 @@ class SettingsAdapter(
if (filePicker.getSelectedValue() != selectedFile) {
notifyItemChanged(clickedPosition)
fragmentView.onSettingChanged()
fragmentView.onSettingChanged(filePicker)
}
filePicker.setSelectedValue(fragmentView.settings!!, selectedFile)
@@ -470,7 +478,7 @@ 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)
@@ -480,7 +488,7 @@ class SettingsAdapter(
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)
@@ -490,7 +498,7 @@ class SettingsAdapter(
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)
@@ -499,7 +507,7 @@ class SettingsAdapter(
is IntSliderSetting -> {
val sliderSetting = clickedItem as IntSliderSetting
if (sliderSetting.selectedValue != seekbarProgress.toInt()) {
fragmentView.onSettingChanged()
fragmentView.onSettingChanged(sliderSetting)
}
sliderSetting.setSelectedValue(settings!!, seekbarProgress.toInt())
closeDialog()
@@ -507,7 +515,9 @@ class SettingsAdapter(
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()
}
@@ -24,13 +24,19 @@ 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.*
@@ -51,6 +57,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
@@ -92,7 +103,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 +122,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 +147,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) {
@@ -146,6 +238,34 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
)
}
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 +277,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 +298,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) {
@@ -256,6 +384,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 {
@@ -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.
@@ -2,14 +2,20 @@
package org.dolphinemu.dolphinemu.features.settings.ui.viewholder
import android.animation.ValueAnimator
import android.content.DialogInterface
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.graphics.Paint
import android.graphics.Typeface
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
}
}
@@ -0,0 +1,29 @@
// 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)
}
}
@@ -170,7 +170,10 @@ 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),
@@ -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" />
@@ -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" />
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>