mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2026-08-24 03:19:17 -05:00
Merge pull request #14742 from Simonx22/android/settings-search-next
Android: Add global settings search
This commit is contained in:
@@ -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,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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
10
Source/Android/app/src/main/res/drawable/ic_search.xml
Normal file
10
Source/Android/app/src/main/res/drawable/ic_search.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user