mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2026-09-18 02:40:11 +00:00
Android: Add global settings search
This commit is contained in:
+1
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+19
@@ -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
|
||||
}
|
||||
+197
-34
@@ -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)
|
||||
}
|
||||
|
||||
+87
-43
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
+27
-2
@@ -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.
|
||||
|
||||
+23
-12
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+133
-3
@@ -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 {
|
||||
|
||||
+410
-359
File diff suppressed because it is too large
Load Diff
+13
-1
@@ -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.
|
||||
|
||||
+43
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+29
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user