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

Android: Add global settings search
This commit is contained in:
JosJuice
2026-08-23 12:19:09 +02:00
committed by GitHub
20 changed files with 1302 additions and 663 deletions
@@ -86,5 +86,6 @@ abstract class SettingsItem {
const val TYPE_STRING = 12 const val TYPE_STRING = 12
const val TYPE_HYPERLINK_HEADER = 13 const val TYPE_HYPERLINK_HEADER = 13
const val TYPE_DATETIME_CHOICE = 14 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.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import android.view.Menu
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.animation.PathInterpolator
import android.widget.Toast import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@@ -39,6 +42,17 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
private var dialog: AlertDialog? = null private var dialog: AlertDialog? = null
private var toolbarLayout: CollapsingToolbarLayout? = null private var toolbarLayout: CollapsingToolbarLayout? = null
private var binding: ActivitySettingsBinding? = 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 themeId: Int = 0
override var isMappingAllDevices = false override var isMappingAllDevices = false
@@ -76,8 +90,11 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
presenter = SettingsActivityPresenter(this, settings) presenter = SettingsActivityPresenter(this, settings)
presenter!!.onCreate(savedInstanceState, menuTag, gameID, revision, isWii, this) presenter!!.onCreate(savedInstanceState, menuTag, gameID, revision, isWii, this)
toolbarLayout = binding!!.toolbarSettingsLayout toolbarLayout = binding!!.toolbarSettingsLayout
expandedToolbarHeight = toolbarLayout!!.layoutParams.height
setSupportActionBar(binding!!.toolbarSettings) setSupportActionBar(binding!!.toolbarSettings)
supportActionBar!!.setDisplayHomeAsUpEnabled(true) supportActionBar!!.setDisplayHomeAsUpEnabled(true)
setUpSettingsSearch()
setUpBackNavigation()
// TODO: Remove this when CollapsingToolbarLayouts are fixed by Google // TODO: Remove this when CollapsingToolbarLayouts are fixed by Google
// https://github.com/material-components/material-components-android/issues/1310 // 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) enableScrollTint(this, binding!!.toolbarSettings, binding!!.appbarSettings)
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { private fun setUpSettingsSearch() {
val inflater = menuInflater searchView = binding!!.settingsSearch
inflater.inflate(R.menu.menu_settings, menu) searchView.setQuery(settingsSearchQuery, false)
return true 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) { override fun onSaveInstanceState(outState: Bundle) {
// Critical: If super method is not called, rotations will be busted. // Critical: If super method is not called, rotations will be busted.
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putBoolean(KEY_MAPPING_ALL_DEVICES, isMappingAllDevices) outState.putBoolean(KEY_MAPPING_ALL_DEVICES, isMappingAllDevices)
presenter!!.onSaveInstanceState(outState)
} }
override fun onStart() { override fun onStart() {
@@ -128,10 +207,17 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
} }
override fun showSettingsFragment( override fun showSettingsFragment(
menuTag: MenuTag, extras: Bundle?, addToStack: Boolean, gameId: String
) {
replaceSettingsFragment(menuTag, extras, addToStack, gameId, false)
}
private fun replaceSettingsFragment(
menuTag: MenuTag, menuTag: MenuTag,
extras: Bundle?, extras: Bundle?,
addToStack: Boolean, addToStack: Boolean,
gameId: String gameId: String,
isSearchResult: Boolean
) { ) {
if (!addToStack && fragment != null) return if (!addToStack && fragment != null) return
val transaction = supportFragmentManager.beginTransaction() val transaction = supportFragmentManager.beginTransaction()
@@ -140,15 +226,18 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
transaction.setCustomAnimations( transaction.setCustomAnimations(
R.anim.anim_settings_fragment_in, R.anim.anim_settings_fragment_in,
R.anim.anim_settings_fragment_out, R.anim.anim_settings_fragment_out,
0, if (isSearchResult) R.anim.anim_settings_search_pop_in else 0,
R.anim.anim_pop_settings_fragment_out if (isSearchResult) {
R.anim.anim_settings_search_pop_out
} else {
R.anim.anim_pop_settings_fragment_out
}
) )
} }
transaction.addToBackStack(null) transaction.addToBackStack(null)
} }
transaction.replace( transaction.replace(
R.id.frame_content_settings, R.id.frame_content_settings, newInstance(menuTag, gameId, extras), FRAGMENT_TAG
newInstance(menuTag, gameId, extras), FRAGMENT_TAG
) )
transaction.commit() transaction.commit()
} }
@@ -157,16 +246,22 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
fragment.show(supportFragmentManager, FRAGMENT_DIALOG_TAG) 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 { private fun areSystemAnimationsEnabled(): Boolean {
val duration = android.provider.Settings.Global.getFloat( val duration = android.provider.Settings.Global.getFloat(
contentResolver, contentResolver, android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, 1f
android.provider.Settings.Global.ANIMATOR_DURATION_SCALE,
1f
) )
val transition = android.provider.Settings.Global.getFloat( val transition = android.provider.Settings.Global.getFloat(
contentResolver, contentResolver, android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE, 1f
android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE,
1f
) )
return duration != 0f && transition != 0f return duration != 0f && transition != 0f
} }
@@ -183,10 +278,8 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
override fun showLoading() { override fun showLoading() {
if (dialog == null) { if (dialog == null) {
dialog = MaterialAlertDialogBuilder(this) dialog = MaterialAlertDialogBuilder(this).setTitle(getString(R.string.load_settings))
.setTitle(getString(R.string.load_settings)) .setView(R.layout.dialog_indeterminate_progress).create()
.setView(R.layout.dialog_indeterminate_progress)
.create()
} }
dialog!!.show() dialog!!.show()
} }
@@ -196,12 +289,10 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
} }
override fun showGameIniJunkDeletionQuestion() { override fun showGameIniJunkDeletionQuestion() {
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this).setTitle(getString(R.string.game_ini_junk_title))
.setTitle(getString(R.string.game_ini_junk_title))
.setMessage(getString(R.string.game_ini_junk_question)) .setMessage(getString(R.string.game_ini_junk_question))
.setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> presenter!!.clearGameSettings() } .setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> presenter!!.clearGameSettings() }
.setNegativeButton(R.string.no, null) .setNegativeButton(R.string.no, null).show()
.show()
} }
override fun onSettingsFileLoaded(settings: Settings) { override fun onSettingsFileLoaded(settings: Settings) {
@@ -229,13 +320,78 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
return presenter!!.hasMenuTagActionForValue(menuTag, value) 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 { override fun onSupportNavigateUp(): Boolean {
onBackPressed() onBackPressedDispatcher.onBackPressed()
return true return true
} }
override fun setToolbarTitle(title: String) { override fun setToolbarState(title: String, showHeadline: Boolean, showSearch: Boolean) {
binding!!.toolbarSettingsLayout.title = title 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 { 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 KEY_MAPPING_ALL_DEVICES = "all_devices"
private const val FRAGMENT_TAG = "settings" private const val FRAGMENT_TAG = "settings"
private const val FRAGMENT_DIALOG_TAG = "settings_dialog" 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 @JvmStatic
fun launch( fun launch(
context: Context, context: Context, menuTag: MenuTag?, gameId: String?, revision: Int, isWii: Boolean
menuTag: MenuTag?,
gameId: String?,
revision: Int,
isWii: Boolean
) { ) {
val settings = Intent(context, SettingsActivity::class.java) val settings = Intent(context, SettingsActivity::class.java)
settings.putExtra(ARG_MENU_TAG, menuTag) settings.putExtra(ARG_MENU_TAG, menuTag)
@@ -296,8 +454,7 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
val settings = Intent(context, SettingsActivity::class.java) val settings = Intent(context, SettingsActivity::class.java)
settings.putExtra(ARG_MENU_TAG, menuTag) settings.putExtra(ARG_MENU_TAG, menuTag)
settings.putExtra( settings.putExtra(
ARG_IS_WII, ARG_IS_WII, !NativeLibrary.IsRunning() || NativeLibrary.IsEmulatingWii()
!NativeLibrary.IsRunning() || NativeLibrary.IsEmulatingWii()
) )
context.startActivity(settings) context.startActivity(settings)
} }
@@ -10,14 +10,17 @@ import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner
import org.dolphinemu.dolphinemu.utils.Log import org.dolphinemu.dolphinemu.utils.Log
class SettingsActivityPresenter( class SettingsActivityPresenter(
private val activityView: SettingsActivityView, private val activityView: SettingsActivityView, var settings: Settings?
var settings: Settings?
) { ) {
private var menuTag: MenuTag? = null private var menuTag: MenuTag? = null
private var gameId: String? = null private var gameId: String? = null
private var revision = 0 private var revision = 0
private var isWii = false private var isWii = false
private lateinit var activity: AppCompatActivity private lateinit var activity: AppCompatActivity
var settingsSearchQuery = ""
private set
var isSettingsSearchActive = false
private set
fun onCreate( fun onCreate(
savedInstanceState: Bundle?, savedInstanceState: Bundle?,
@@ -32,6 +35,43 @@ class SettingsActivityPresenter(
this.revision = revision this.revision = revision
this.isWii = isWii this.isWii = isWii
this.activity = activity 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() { fun onDestroy() {
@@ -85,55 +125,49 @@ class SettingsActivityPresenter(
} }
fun onMenuTagAction(menuTag: MenuTag, value: Int) { fun onMenuTagAction(menuTag: MenuTag, value: Int) {
if (menuTag.isSerialPort1Menu) { val action = getMenuTagAction(menuTag, value) ?: return
// Not disabled or dummy activityView.showSettingsFragment(action.menuTag, action.extras, true, gameId!!)
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!!)
}
}
} }
fun hasMenuTagActionForValue(menuTag: MenuTag, value: Int): Boolean { 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 // Not disabled or dummy
return value != 0 && value != 255 menuTag.isSerialPort1Menu && value != 0 && value != 255 -> MenuTagAction(
} menuTag, Bundle().apply {
if (menuTag.isGCPadMenu) { putInt(SettingsFragmentPresenter.ARG_SERIALPORT1_TYPE, value)
})
// Not disabled // Not disabled
return value != 0 menuTag.isGCPadMenu && value != 0 -> MenuTagAction(
} menuTag, Bundle().apply {
if (menuTag.isWiimoteMenu) { putInt(SettingsFragmentPresenter.ARG_CONTROLLER_TYPE, value)
})
// Emulated Wii Remote // Emulated Wii Remote
return value == 1 menuTag.isWiimoteMenu && value == 1 -> MenuTagAction(menuTag, null)
}
return if (menuTag.isWiimoteExtensionMenu) {
// Not disabled // Not disabled
value != 0 menuTag.isWiimoteExtensionMenu && value != 0 -> MenuTagAction(
} else false 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. * Abstraction for the Activity that manages SettingsFragments.
*/ */
interface SettingsActivityView { 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. * Show a new SettingsFragment.
* *
@@ -17,12 +27,19 @@ interface SettingsActivityView {
* @param addToStack Whether or not this fragment should replace a previous one. * @param addToStack Whether or not this fragment should replace a previous one.
*/ */
fun showSettingsFragment( fun showSettingsFragment(
menuTag: MenuTag, menuTag: MenuTag, extras: Bundle?, addToStack: Boolean, gameId: String
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. * Shows a DialogFragment.
* *
@@ -86,6 +103,11 @@ interface SettingsActivityView {
*/ */
fun hasMenuTagActionForValue(menuTag: MenuTag, value: Int): Boolean 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 * Show loading dialog while loading the settings
*/ */
@@ -102,9 +124,9 @@ interface SettingsActivityView {
fun showGameIniJunkDeletionQuestion() 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, * Returns whether the input mapping dialog should detect inputs from all devices,
* not just the device configured for the controller. * 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.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat import com.google.android.material.timepicker.TimeFormat
import org.dolphinemu.dolphinemu.R 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.model.view.InputMappingControlSetting
import org.dolphinemu.dolphinemu.features.input.ui.AdvancedMappingDialog import org.dolphinemu.dolphinemu.features.input.ui.AdvancedMappingDialog
import org.dolphinemu.dolphinemu.features.input.ui.MotionAlertDialog import org.dolphinemu.dolphinemu.features.input.ui.MotionAlertDialog
import org.dolphinemu.dolphinemu.features.input.ui.viewholder.InputMappingControlSettingViewHolder import org.dolphinemu.dolphinemu.features.input.ui.viewholder.InputMappingControlSettingViewHolder
import org.dolphinemu.dolphinemu.features.settings.model.Settings import org.dolphinemu.dolphinemu.features.settings.model.Settings
import org.dolphinemu.dolphinemu.features.settings.model.view.* import org.dolphinemu.dolphinemu.features.settings.model.view.DateTimeChoiceSetting
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.* 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.DirectoryInitialization
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper import org.dolphinemu.dolphinemu.utils.FileBrowserHelper
import org.dolphinemu.dolphinemu.utils.Log import org.dolphinemu.dolphinemu.utils.Log
@@ -42,14 +74,13 @@ import org.dolphinemu.dolphinemu.utils.PermissionsHandler
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.RandomAccessFile import java.io.RandomAccessFile
import java.util.* import java.util.Calendar
import java.util.TimeZone
import kotlin.math.roundToInt import kotlin.math.roundToInt
class SettingsAdapter( class SettingsAdapter(
private val fragmentView: SettingsFragmentView, private val fragmentView: SettingsFragmentView, private val context: Context
private val context: Context ) : RecyclerView.Adapter<SettingViewHolder>(), DialogInterface.OnClickListener,
) :
RecyclerView.Adapter<SettingViewHolder>(), DialogInterface.OnClickListener,
Slider.OnChangeListener { Slider.OnChangeListener {
private var settingsList: ArrayList<SettingsItem>? = null private var settingsList: ArrayList<SettingsItem>? = null
private var clickedItem: SettingsItem? = null private var clickedItem: SettingsItem? = null
@@ -68,55 +99,59 @@ class SettingsAdapter(
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
return when (viewType) { return when (viewType) {
SettingsItem.TYPE_HEADER -> HeaderViewHolder( SettingsItem.TYPE_HEADER -> HeaderViewHolder(
ListItemHeaderBinding.inflate(inflater, parent, false), ListItemHeaderBinding.inflate(inflater, parent, false), this
this
) )
SettingsItem.TYPE_SWITCH -> SwitchSettingViewHolder( SettingsItem.TYPE_SWITCH -> SwitchSettingViewHolder(
ListItemSettingSwitchBinding.inflate(inflater, parent, false), ListItemSettingSwitchBinding.inflate(inflater, parent, false), this
this
) )
SettingsItem.TYPE_STRING_SINGLE_CHOICE,
SettingsItem.TYPE_SINGLE_CHOICE_DYNAMIC_DESCRIPTIONS, SettingsItem.TYPE_STRING_SINGLE_CHOICE, SettingsItem.TYPE_SINGLE_CHOICE_DYNAMIC_DESCRIPTIONS, SettingsItem.TYPE_SINGLE_CHOICE -> SingleChoiceViewHolder(
SettingsItem.TYPE_SINGLE_CHOICE -> SingleChoiceViewHolder( ListItemSettingBinding.inflate(inflater, parent, false), this
ListItemSettingBinding.inflate(inflater, parent, false),
this
) )
SettingsItem.TYPE_SLIDER -> SliderViewHolder( SettingsItem.TYPE_SLIDER -> SliderViewHolder(
ListItemSettingBinding.inflate(inflater, parent, false), ListItemSettingBinding.inflate(inflater, parent, false), this, context
this,
context
) )
SettingsItem.TYPE_SUBMENU -> SubmenuViewHolder( SettingsItem.TYPE_SUBMENU -> SubmenuViewHolder(
ListItemSubmenuBinding.inflate(inflater, parent, false), ListItemSubmenuBinding.inflate(inflater, parent, false), this
this
) )
SettingsItem.TYPE_INPUT_MAPPING_CONTROL -> InputMappingControlSettingViewHolder( SettingsItem.TYPE_INPUT_MAPPING_CONTROL -> InputMappingControlSettingViewHolder(
ListItemMappingBinding.inflate(inflater, parent, false), ListItemMappingBinding.inflate(inflater, parent, false), this
this
) )
SettingsItem.TYPE_FILE_PICKER,
SettingsItem.TYPE_DIRECTORY_PICKER -> FilePickerViewHolder( SettingsItem.TYPE_FILE_PICKER, SettingsItem.TYPE_DIRECTORY_PICKER -> FilePickerViewHolder(
ListItemSettingBinding.inflate(inflater, parent, false), ListItemSettingBinding.inflate(inflater, parent, false), this
this
) )
SettingsItem.TYPE_RUN_RUNNABLE -> RunRunnableViewHolder( SettingsItem.TYPE_RUN_RUNNABLE -> RunRunnableViewHolder(
ListItemSettingBinding.inflate(inflater, parent, false), ListItemSettingBinding.inflate(inflater, parent, false), this, context
this, context
) )
SettingsItem.TYPE_STRING -> InputStringSettingViewHolder( SettingsItem.TYPE_STRING -> InputStringSettingViewHolder(
ListItemSettingBinding.inflate(inflater, parent, false), this ListItemSettingBinding.inflate(inflater, parent, false), this
) )
SettingsItem.TYPE_HYPERLINK_HEADER -> HeaderHyperLinkViewHolder( SettingsItem.TYPE_HYPERLINK_HEADER -> HeaderHyperLinkViewHolder(
ListItemHeaderBinding.inflate(inflater, parent, false), this ListItemHeaderBinding.inflate(inflater, parent, false), this
) )
SettingsItem.TYPE_DATETIME_CHOICE -> DateTimeSettingViewHolder( SettingsItem.TYPE_DATETIME_CHOICE -> DateTimeSettingViewHolder(
ListItemSettingBinding.inflate(inflater, parent, false), this ListItemSettingBinding.inflate(inflater, parent, false), this
) )
SettingsItem.TYPE_SEARCH_RESULT -> SettingsSearchResultViewHolder(
ListItemSearchResultBinding.inflate(inflater, parent, false), this
)
else -> throw IllegalArgumentException("Invalid view type: $viewType") else -> throw IllegalArgumentException("Invalid view type: $viewType")
} }
} }
override fun onBindViewHolder(holder: SettingViewHolder, position: Int) { override fun onBindViewHolder(holder: SettingViewHolder, position: Int) {
holder.clearSearchResultHighlight()
holder.bind(getItem(position)) holder.bind(getItem(position))
} }
@@ -143,7 +178,7 @@ class SettingsAdapter(
fun clearSetting(item: SettingsItem) { fun clearSetting(item: SettingsItem) {
item.clear(settings!!) item.clear(settings!!)
fragmentView.onSettingChanged() fragmentView.onSettingChanged(item)
} }
fun notifyAllSettingsChanged() { fun notifyAllSettingsChanged() {
@@ -153,7 +188,7 @@ class SettingsAdapter(
fun onBooleanClick(item: SwitchSetting, checked: Boolean) { fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
item.setChecked(settings!!, checked) item.setChecked(settings!!, checked)
fragmentView.onSettingChanged() fragmentView.onSettingChanged(item)
} }
fun onInputStringClick(item: InputStringSetting, position: Int) { fun onInputStringClick(item: InputStringSetting, position: Int) {
@@ -161,29 +196,24 @@ class SettingsAdapter(
val binding = DialogInputStringBinding.inflate(inflater) val binding = DialogInputStringBinding.inflate(inflater)
val input = binding.input val input = binding.input
input.setText(item.selectedValue) input.setText(item.selectedValue)
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity) dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setView(binding.root)
.setView(binding.root)
.setMessage(item.description) .setMessage(item.description)
.setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int ->
val editTextInput = input.text.toString() val editTextInput = input.text.toString()
if (item.selectedValue != editTextInput) { if (item.selectedValue != editTextInput) {
notifyItemChanged(position) notifyItemChanged(position)
fragmentView.onSettingChanged() fragmentView.onSettingChanged(item)
} }
item.setSelectedValue(fragmentView.settings!!, editTextInput) item.setSelectedValue(fragmentView.settings!!, editTextInput)
} }.setNegativeButton(R.string.cancel, null).show()
.setNegativeButton(R.string.cancel, null)
.show()
} }
fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) { fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
clickedItem = item clickedItem = item
clickedPosition = position clickedPosition = position
val value = getSelectionForSingleChoiceValue(item) val value = getSelectionForSingleChoiceValue(item)
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity) dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
.setTitle(item.name) .setSingleChoiceItems(item.choicesId, value, this).show()
.setSingleChoiceItems(item.choicesId, value, this)
.show()
} }
fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) { fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) {
@@ -193,35 +223,26 @@ class SettingsAdapter(
val choices = item.choices val choices = item.choices
val noChoicesAvailableString = item.noChoicesAvailableString val noChoicesAvailableString = item.noChoicesAvailableString
dialog = if (noChoicesAvailableString != 0 && choices.isEmpty()) { dialog = if (noChoicesAvailableString != 0 && choices.isEmpty()) {
MaterialAlertDialogBuilder(fragmentView.fragmentActivity) MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
.setTitle(item.name) .setMessage(noChoicesAvailableString).setPositiveButton(R.string.ok, null).show()
.setMessage(noChoicesAvailableString)
.setPositiveButton(R.string.ok, null)
.show()
} else { } else {
MaterialAlertDialogBuilder(fragmentView.fragmentActivity) MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
.setTitle(item.name)
.setSingleChoiceItems( .setSingleChoiceItems(
item.choices, item.selectedValueIndex, item.choices, item.selectedValueIndex, this
this ).show()
)
.show()
} }
} }
fun onSingleChoiceDynamicDescriptionsClick( fun onSingleChoiceDynamicDescriptionsClick(
item: SingleChoiceSettingDynamicDescriptions, item: SingleChoiceSettingDynamicDescriptions, position: Int
position: Int
) { ) {
clickedItem = item clickedItem = item
clickedPosition = position clickedPosition = position
val value = getSelectionForSingleChoiceDynamicDescriptionsValue(item) val value = getSelectionForSingleChoiceDynamicDescriptionsValue(item)
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity) dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
.setTitle(item.name) .setSingleChoiceItems(item.choicesId, value, this).show()
.setSingleChoiceItems(item.choicesId, value, this)
.show()
} }
fun onSliderClick(item: SliderSetting, position: Int) { fun onSliderClick(item: SliderSetting, position: Int) {
@@ -251,6 +272,7 @@ class SettingsAdapter(
slider.valueTo = item.max slider.valueTo = item.max
slider.stepSize = item.stepSize slider.stepSize = item.stepSize
} }
is IntSliderSetting -> { is IntSliderSetting -> {
slider.valueFrom = item.min.toFloat() slider.valueFrom = item.min.toFloat()
slider.valueTo = item.max.toFloat() slider.valueTo = item.max.toFloat()
@@ -260,29 +282,27 @@ class SettingsAdapter(
slider.value = (seekbarProgress / slider.stepSize).roundToInt() * slider.stepSize slider.value = (seekbarProgress / slider.stepSize).roundToInt() * slider.stepSize
slider.addOnChangeListener(this) slider.addOnChangeListener(this)
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity) dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
.setTitle(item.name) .setView(binding.root).setPositiveButton(R.string.ok, this).show()
.setView(binding.root)
.setPositiveButton(R.string.ok, this)
.show()
} }
fun onSubmenuClick(item: SubmenuSetting) { fun onSubmenuClick(item: SubmenuSetting) {
fragmentView.loadSubMenu(item.menuKey) fragmentView.loadSubMenu(item.menuKey)
} }
fun onSearchResultClick(item: SettingsSearchResult) {
fragmentView.loadSearchResult(item.menuKey, item.settingPosition, item.navigationExtras)
}
fun onInputMappingClick(item: InputMappingControlSetting, position: Int) { fun onInputMappingClick(item: InputMappingControlSetting, position: Int) {
if (item.controller.getDefaultDevice().isEmpty() && !fragmentView.isMappingAllDevices) { if (item.controller.getDefaultDevice().isEmpty() && !fragmentView.isMappingAllDevices) {
MaterialAlertDialogBuilder(fragmentView.fragmentActivity) MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setMessage(R.string.input_binding_no_device)
.setMessage(R.string.input_binding_no_device) .setPositiveButton(R.string.ok, this).show()
.setPositiveButton(R.string.ok, this)
.show()
return return
} }
val dialog = MotionAlertDialog( val dialog = MotionAlertDialog(
fragmentView.fragmentActivity, item, fragmentView.fragmentActivity, item, fragmentView.isMappingAllDevices
fragmentView.isMappingAllDevices
) )
val background = ContextCompat.getDrawable(context, R.drawable.dialog_round) val background = ContextCompat.getDrawable(context, R.drawable.dialog_round)
@@ -296,18 +316,16 @@ class SettingsAdapter(
dialog.setTitle(R.string.input_binding) dialog.setTitle(R.string.input_binding)
dialog.setMessage( dialog.setMessage(
String.format( String.format(
context.getString(R.string.input_binding_description), context.getString(R.string.input_binding_description), item.name
item.name
) )
) )
dialog.setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(R.string.cancel), this) dialog.setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(R.string.cancel), this)
dialog.setButton( dialog.setButton(
AlertDialog.BUTTON_NEUTRAL, AlertDialog.BUTTON_NEUTRAL, context.getString(R.string.clear)
context.getString(R.string.clear)
) { _: DialogInterface?, _: Int -> item.clearValue() } ) { _: DialogInterface?, _: Int -> item.clearValue() }
dialog.setOnDismissListener { dialog.setOnDismissListener {
notifyItemChanged(position) notifyItemChanged(position)
fragmentView.onSettingChanged() fragmentView.onSettingChanged(item)
} }
dialog.setCanceledOnTouchOutside(false) dialog.setCanceledOnTouchOutside(false)
dialog.show() dialog.show()
@@ -317,10 +335,7 @@ class SettingsAdapter(
val inflater = LayoutInflater.from(context) val inflater = LayoutInflater.from(context)
val binding = DialogAdvancedMappingBinding.inflate(inflater) val binding = DialogAdvancedMappingBinding.inflate(inflater)
val dialog = AdvancedMappingDialog( val dialog = AdvancedMappingDialog(
context, context, binding, item.controlReference, item.controller
binding,
item.controlReference,
item.controller
) )
val background = ContextCompat.getDrawable(context, R.drawable.dialog_round) val background = ContextCompat.getDrawable(context, R.drawable.dialog_round)
@@ -338,12 +353,11 @@ class SettingsAdapter(
) { _: DialogInterface?, _: Int -> ) { _: DialogInterface?, _: Int ->
item.value = dialog.expression item.value = dialog.expression
notifyItemChanged(position) notifyItemChanged(position)
fragmentView.onSettingChanged() fragmentView.onSettingChanged(item)
} }
dialog.setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(R.string.cancel), this) dialog.setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(R.string.cancel), this)
dialog.setButton( dialog.setButton(
AlertDialog.BUTTON_NEUTRAL, AlertDialog.BUTTON_NEUTRAL, context.getString(R.string.clear)
context.getString(R.string.clear)
) { _: DialogInterface?, _: Int -> } ) { _: DialogInterface?, _: Int -> }
dialog.setCanceledOnTouchOutside(false) dialog.setCanceledOnTouchOutside(false)
dialog.show() dialog.show()
@@ -361,14 +375,12 @@ class SettingsAdapter(
val directoryPicker = item as DirectoryPicker val directoryPicker = item as DirectoryPicker
if (!PermissionsHandler.isExternalStorageLegacy()) { if (!PermissionsHandler.isExternalStorageLegacy()) {
MaterialAlertDialogBuilder(context) MaterialAlertDialogBuilder(context).setMessage(R.string.path_not_changeable_scoped_storage)
.setMessage(R.string.path_not_changeable_scoped_storage)
.setPositiveButton(R.string.ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() } .setPositiveButton(R.string.ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
.show() .show()
} else { } else {
val intent = FileBrowserHelper.createDirectoryPickerIntent( val intent = FileBrowserHelper.createDirectoryPickerIntent(
fragmentView.fragmentActivity, fragmentView.fragmentActivity, FileBrowserHelper.GAME_EXTENSIONS
FileBrowserHelper.GAME_EXTENSIONS
) )
directoryPicker.launcher.launch(intent) directoryPicker.launcher.launch(intent)
} }
@@ -400,32 +412,24 @@ class SettingsAdapter(
calendar.timeZone = TimeZone.getTimeZone("UTC") calendar.timeZone = TimeZone.getTimeZone("UTC")
// Start and end epoch times available for the Wii's date picker // Start and end epoch times available for the Wii's date picker
val calendarConstraints = CalendarConstraints.Builder() val calendarConstraints =
.setStart(946684800000L) CalendarConstraints.Builder().setStart(946684800000L).setEnd(2082672000000L).build()
.setEnd(2082672000000L)
.build()
var timeFormat = TimeFormat.CLOCK_12H var timeFormat = TimeFormat.CLOCK_12H
if (DateFormat.is24HourFormat(fragmentView.fragmentActivity)) { if (DateFormat.is24HourFormat(fragmentView.fragmentActivity)) {
timeFormat = TimeFormat.CLOCK_24H timeFormat = TimeFormat.CLOCK_24H
} }
val datePicker = MaterialDatePicker.Builder.datePicker() val datePicker = MaterialDatePicker.Builder.datePicker().setSelection(storedTime)
.setSelection(storedTime) .setTitleText(R.string.select_rtc_date).setCalendarConstraints(calendarConstraints)
.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() .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 { datePicker.addOnPositiveButtonClickListener {
timePicker.show( timePicker.show(
fragmentView.fragmentActivity.supportFragmentManager, fragmentView.fragmentActivity.supportFragmentManager, "TimePicker"
"TimePicker"
) )
} }
timePicker.addOnPositiveButtonClickListener { timePicker.addOnPositiveButtonClickListener {
@@ -435,7 +439,7 @@ class SettingsAdapter(
val rtcString = "0x" + java.lang.Long.toHexString(epochTime) val rtcString = "0x" + java.lang.Long.toHexString(epochTime)
if (item.getSelectedValue() != rtcString) { if (item.getSelectedValue() != rtcString) {
notifyItemChanged(clickedPosition) notifyItemChanged(clickedPosition)
fragmentView.onSettingChanged() fragmentView.onSettingChanged(item)
} }
item.setSelectedValue(fragmentView.settings!!, rtcString) item.setSelectedValue(fragmentView.settings!!, rtcString)
clickedItem = null clickedItem = null
@@ -448,7 +452,7 @@ class SettingsAdapter(
if (filePicker.getSelectedValue() != selectedFile) { if (filePicker.getSelectedValue() != selectedFile) {
notifyItemChanged(clickedPosition) notifyItemChanged(clickedPosition)
fragmentView.onSettingChanged() fragmentView.onSettingChanged(filePicker)
} }
filePicker.setSelectedValue(fragmentView.settings!!, selectedFile) filePicker.setSelectedValue(fragmentView.settings!!, selectedFile)
@@ -470,44 +474,50 @@ class SettingsAdapter(
val scSetting = clickedItem as SingleChoiceSetting val scSetting = clickedItem as SingleChoiceSetting
val value = getValueForSingleChoiceSelection(scSetting, which) val value = getValueForSingleChoiceSelection(scSetting, which)
if (scSetting.selectedValue != value) fragmentView.onSettingChanged() if (scSetting.selectedValue != value) fragmentView.onSettingChanged(scSetting)
scSetting.setSelectedValue(settings!!, value) scSetting.setSelectedValue(settings!!, value)
closeDialog() closeDialog()
} }
is SingleChoiceSettingDynamicDescriptions -> { is SingleChoiceSettingDynamicDescriptions -> {
val scSetting = clickedItem as SingleChoiceSettingDynamicDescriptions val scSetting = clickedItem as SingleChoiceSettingDynamicDescriptions
val value = getValueForSingleChoiceDynamicDescriptionsSelection(scSetting, which) val value = getValueForSingleChoiceDynamicDescriptionsSelection(scSetting, which)
if (scSetting.selectedValue != value) fragmentView.onSettingChanged() if (scSetting.selectedValue != value) fragmentView.onSettingChanged(scSetting)
scSetting.setSelectedValue(settings!!, value) scSetting.setSelectedValue(settings!!, value)
closeDialog() closeDialog()
} }
is StringSingleChoiceSetting -> { is StringSingleChoiceSetting -> {
val scSetting = clickedItem as StringSingleChoiceSetting val scSetting = clickedItem as StringSingleChoiceSetting
val value = scSetting.getValueAt(which) val value = scSetting.getValueAt(which)
if (scSetting.selectedValue != value) fragmentView.onSettingChanged() if (scSetting.selectedValue != value) fragmentView.onSettingChanged(scSetting)
scSetting.setSelectedValue(settings!!, value) scSetting.setSelectedValue(settings!!, value)
closeDialog() closeDialog()
} }
is IntSliderSetting -> { is IntSliderSetting -> {
val sliderSetting = clickedItem as IntSliderSetting val sliderSetting = clickedItem as IntSliderSetting
if (sliderSetting.selectedValue != seekbarProgress.toInt()) { if (sliderSetting.selectedValue != seekbarProgress.toInt()) {
fragmentView.onSettingChanged() fragmentView.onSettingChanged(sliderSetting)
} }
sliderSetting.setSelectedValue(settings!!, seekbarProgress.toInt()) sliderSetting.setSelectedValue(settings!!, seekbarProgress.toInt())
closeDialog() closeDialog()
} }
is FloatSliderSetting -> { is FloatSliderSetting -> {
val sliderSetting = clickedItem as FloatSliderSetting val sliderSetting = clickedItem as FloatSliderSetting
if (sliderSetting.selectedValue != seekbarProgress) fragmentView.onSettingChanged() if (sliderSetting.selectedValue != seekbarProgress) {
fragmentView.onSettingChanged(sliderSetting)
}
sliderSetting.setSelectedValue(settings!!, seekbarProgress) sliderSetting.setSelectedValue(settings!!, seekbarProgress)
@@ -540,6 +550,7 @@ class SettingsAdapter(
override fun onViewRecycled(holder: SettingViewHolder) { override fun onViewRecycled(holder: SettingViewHolder) {
super.onViewRecycled(holder) super.onViewRecycled(holder)
holder.clearSearchResultHighlight()
holder.onViewRecycled() holder.onViewRecycled()
} }
@@ -587,8 +598,7 @@ class SettingsAdapter(
} }
private fun getValueForSingleChoiceDynamicDescriptionsSelection( private fun getValueForSingleChoiceDynamicDescriptionsSelection(
item: SingleChoiceSettingDynamicDescriptions, item: SingleChoiceSettingDynamicDescriptions, which: Int
which: Int
): Int { ): Int {
val valuesId = item.valuesId val valuesId = item.valuesId
return if (valuesId > 0) { return if (valuesId > 0) {
@@ -24,17 +24,22 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar 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.R
import org.dolphinemu.dolphinemu.databinding.FragmentSettingsBinding import org.dolphinemu.dolphinemu.databinding.FragmentSettingsBinding
import org.dolphinemu.dolphinemu.features.settings.model.Settings import org.dolphinemu.dolphinemu.features.settings.model.Settings
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem 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.GpuDriverInstallResult
import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable
import java.util.* import java.util.EnumMap
import kotlin.collections.ArrayList
class SettingsFragment : Fragment(), SettingsFragmentView { class SettingsFragment : Fragment(), SettingsFragmentView {
private lateinit var presenter: SettingsFragmentPresenter private lateinit var presenter: SettingsFragmentPresenter
@@ -51,6 +56,11 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
SettingsActivityResultLaunchers(this) { adapter } SettingsActivityResultLaunchers(this) { adapter }
private var oldControllerSettingsWarningHeight = 0 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 private var binding: FragmentSettingsBinding? = null
@@ -82,9 +92,7 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
} }
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
container: ViewGroup?,
savedInstanceState: Bundle?
): View { ): View {
binding = FragmentSettingsBinding.inflate(inflater, container, false) binding = FragmentSettingsBinding.inflate(inflater, container, false)
return binding!!.root return binding!!.root
@@ -92,7 +100,11 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (titles.containsKey(menuTag)) { if (titles.containsKey(menuTag)) {
activityView!!.setToolbarTitle(getString(titles[menuTag]!!)) activityView!!.setToolbarState(
getString(titles[menuTag]!!),
menuTag != MenuTag.SETTINGS,
menuTag == MenuTag.SETTINGS
)
} }
val manager = LinearLayoutManager(activity) val manager = LinearLayoutManager(activity)
@@ -107,10 +119,13 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
setInsets() setInsets()
val activity = requireActivity() as SettingsActivityView val activity = requireActivity() as SettingsActivityView
presenter.invalidateSearchIndex()
presenter.onViewCreated(menuTag, activity.settings) presenter.onViewCreated(menuTag, activity.settings)
} }
override fun onDestroyView() { override fun onDestroyView() {
clearSearchResultHighlight()
searchJob?.cancel()
super.onDestroyView() super.onDestroyView()
binding = null binding = null
} }
@@ -129,7 +144,81 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
} }
override fun showSettingsList(settingsList: ArrayList<SettingsItem>) { 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) { override fun loadSubMenu(menuKey: MenuTag) {
@@ -139,13 +228,35 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
} }
activityView!!.showSettingsFragment( activityView!!.showSettingsFragment(
menuKey, menuKey, null, true, requireArguments().getString(ARGUMENT_GAME_ID)!!
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) { override fun showDialogFragment(fragment: DialogFragment) {
activityView!!.showDialogFragment(fragment) activityView!!.showDialogFragment(fragment)
} }
@@ -157,7 +268,11 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
override val settings: Settings? override val settings: Settings?
get() = presenter.settings get() = presenter.settings
override fun onSettingChanged() { override fun onSettingChanged(setting: SettingsItem?) {
if (setting == null || setting === highlightedSearchResult) {
clearSearchResultHighlight()
}
presenter.invalidateSearchIndex()
activityView!!.onSettingChanged() activityView!!.onSettingChanged()
} }
@@ -174,6 +289,10 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
return activityView!!.hasMenuTagActionForValue(menuTag, value) return activityView!!.hasMenuTagActionForValue(menuTag, value)
} }
override fun getMenuTagActionExtras(menuTag: MenuTag, value: Int): Bundle? {
return activityView!!.getMenuTagActionExtras(menuTag, value)
}
override var isMappingAllDevices: Boolean override var isMappingAllDevices: Boolean
get() = activityView!!.isMappingAllDevices get() = activityView!!.isMappingAllDevices
set(allDevices) { set(allDevices) {
@@ -203,17 +322,13 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
} }
val msg = "${presenter.gpuDriver!!.name} ${presenter.gpuDriver!!.driverVersion}" val msg = "${presenter.gpuDriver!!.name} ${presenter.gpuDriver!!.driverVersion}"
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext()).setTitle(getString(R.string.gpu_driver_dialog_title))
.setTitle(getString(R.string.gpu_driver_dialog_title)) .setMessage(msg).setNegativeButton(android.R.string.cancel, null)
.setMessage(msg)
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(R.string.gpu_driver_dialog_system) { _: DialogInterface?, _: Int -> .setNeutralButton(R.string.gpu_driver_dialog_system) { _: DialogInterface?, _: Int ->
presenter.useSystemDriver() presenter.useSystemDriver()
} }.setPositiveButton(R.string.gpu_driver_dialog_install) { _: DialogInterface?, _: Int ->
.setPositiveButton(R.string.gpu_driver_dialog_install) { _: DialogInterface?, _: Int ->
askForDriverFile() askForDriverFile()
} }.show()
.show()
} }
override fun getFragmentLifecycle(): Lifecycle { override fun getFragmentLifecycle(): Lifecycle {
@@ -230,16 +345,12 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
override fun onDriverInstallDone(result: GpuDriverInstallResult) { override fun onDriverInstallDone(result: GpuDriverInstallResult) {
val view = binding?.root ?: return val view = binding?.root ?: return
Snackbar Snackbar.make(view, resolveInstallResultString(result), Snackbar.LENGTH_LONG).show()
.make(view, resolveInstallResultString(result), Snackbar.LENGTH_LONG)
.show()
} }
override fun onDriverUninstallDone() { override fun onDriverUninstallDone() {
Toast.makeText( Toast.makeText(
requireContext(), requireContext(), R.string.gpu_driver_dialog_uninstall_done, Toast.LENGTH_SHORT
R.string.gpu_driver_dialog_uninstall_done,
Toast.LENGTH_SHORT
).show() ).show()
} }
@@ -256,6 +367,8 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
companion object { companion object {
private const val ARGUMENT_MENU_TAG = "menu_tag" private const val ARGUMENT_MENU_TAG = "menu_tag"
private const val ARGUMENT_GAME_ID = "game_id" 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) private val titles: MutableMap<MenuTag, Int> = EnumMap(MenuTag::class.java)
init { init {
@@ -2,6 +2,7 @@
package org.dolphinemu.dolphinemu.features.settings.ui package org.dolphinemu.dolphinemu.features.settings.ui
import android.os.Bundle
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@@ -50,6 +51,12 @@ interface SettingsFragmentView {
* @param menuKey Identifier for the settings group that should be shown. * @param menuKey Identifier for the settings group that should be shown.
*/ */
fun loadSubMenu(menuKey: MenuTag) 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) fun showDialogFragment(fragment: DialogFragment)
/** /**
@@ -67,7 +74,7 @@ interface SettingsFragmentView {
/** /**
* Have the fragment tell the containing Activity that a Setting was modified. * 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. * Refetches the values of all controller settings.
@@ -95,6 +102,11 @@ interface SettingsFragmentView {
*/ */
fun hasMenuTagActionForValue(menuTag: MenuTag, value: Int): Boolean 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, * Controls whether the input mapping dialog should detect inputs from all devices,
* not just the device configured for the controller. * not just the device configured for the controller.
@@ -2,14 +2,20 @@
package org.dolphinemu.dolphinemu.features.settings.ui.viewholder package org.dolphinemu.dolphinemu.features.settings.ui.viewholder
import android.animation.ValueAnimator
import android.content.DialogInterface import android.content.DialogInterface
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Typeface 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
import android.view.View.OnLongClickListener import android.view.View.OnLongClickListener
import android.view.animation.DecelerateInterpolator
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.dolphinemu.dolphinemu.DolphinApplication import org.dolphinemu.dolphinemu.DolphinApplication
import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.R
@@ -21,6 +27,9 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
LifecycleViewHolder(itemView, adapter.getFragmentLifecycle()), LifecycleViewHolder(itemView, adapter.getFragmentLifecycle()),
LifecycleOwner, View.OnClickListener, OnLongClickListener { LifecycleOwner, View.OnClickListener, OnLongClickListener {
private val defaultBackground: Drawable? = itemView.background
private var searchResultHighlightAnimator: ValueAnimator? = null
init { init {
itemView.setOnClickListener(this) itemView.setOnClickListener(this)
itemView.setOnLongClickListener(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 * Called by the adapter to set this ViewHolder's child views to display the list item
* it must now represent. * it must now represent.
@@ -102,4 +140,9 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).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 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 android.content.res.Configuration
import com.google.android.material.appbar.MaterialToolbar import android.os.Build
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.elevation.ElevationOverlayProvider
import com.google.android.material.color.MaterialColors
import androidx.annotation.ColorInt 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 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 { object ThemeHelper {
@@ -52,8 +52,7 @@ object ThemeHelper {
.getInt(CURRENT_THEME_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) .getInt(CURRENT_THEME_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
activity.delegate.localNightMode = themeMode activity.delegate.localNightMode = themeMode
val windowController = WindowCompat.getInsetsController( val windowController = WindowCompat.getInsetsController(
activity.window, activity.window, activity.window.decorView
activity.window.decorView
) )
val systemReportedThemeMode = val systemReportedThemeMode =
activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK 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_NO -> setLightModeSystemBars(windowController)
Configuration.UI_MODE_NIGHT_YES -> setDarkModeSystemBars(windowController) Configuration.UI_MODE_NIGHT_YES -> setDarkModeSystemBars(windowController)
} }
AppCompatDelegate.MODE_NIGHT_NO -> setLightModeSystemBars(windowController) AppCompatDelegate.MODE_NIGHT_NO -> setLightModeSystemBars(windowController)
AppCompatDelegate.MODE_NIGHT_YES -> setDarkModeSystemBars(windowController) AppCompatDelegate.MODE_NIGHT_YES -> setDarkModeSystemBars(windowController)
} }
@@ -83,66 +83,50 @@ object ThemeHelper {
@JvmStatic @JvmStatic
fun saveTheme(activity: AppCompatActivity, themeValue: Int) { fun saveTheme(activity: AppCompatActivity, themeValue: Int) {
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext) PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
.edit() .putInt(CURRENT_THEME, themeValue).apply()
.putInt(CURRENT_THEME, themeValue)
.apply()
activity.recreate() activity.recreate()
} }
@JvmStatic @JvmStatic
fun deleteThemeKey(activity: AppCompatActivity) { fun deleteThemeKey(activity: AppCompatActivity) {
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext) PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
.edit() .remove(CURRENT_THEME).apply()
.remove(CURRENT_THEME)
.apply()
activity.recreate() activity.recreate()
} }
@JvmStatic @JvmStatic
fun saveThemeMode(activity: AppCompatActivity, themeModeValue: Int) { fun saveThemeMode(activity: AppCompatActivity, themeModeValue: Int) {
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext) PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
.edit() .putInt(CURRENT_THEME_MODE, themeModeValue).apply()
.putInt(CURRENT_THEME_MODE, themeModeValue)
.apply()
setThemeMode(activity) setThemeMode(activity)
} }
@JvmStatic @JvmStatic
fun deleteThemeModeKey(activity: AppCompatActivity) { fun deleteThemeModeKey(activity: AppCompatActivity) {
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext) PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
.edit() .remove(CURRENT_THEME_MODE).apply()
.remove(CURRENT_THEME_MODE)
.apply()
setThemeMode(activity) setThemeMode(activity)
} }
@JvmStatic @JvmStatic
fun saveBackgroundSetting(activity: AppCompatActivity, backgroundValue: Boolean) { fun saveBackgroundSetting(activity: AppCompatActivity, backgroundValue: Boolean) {
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext) PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
.edit() .putBoolean(USE_BLACK_BACKGROUNDS, backgroundValue).apply()
.putBoolean(USE_BLACK_BACKGROUNDS, backgroundValue)
.apply()
activity.recreate() activity.recreate()
} }
@JvmStatic @JvmStatic
fun deleteBackgroundSetting(activity: AppCompatActivity) { fun deleteBackgroundSetting(activity: AppCompatActivity) {
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext) PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
.edit() .remove(USE_BLACK_BACKGROUNDS).apply()
.remove(USE_BLACK_BACKGROUNDS)
.apply()
activity.recreate() activity.recreate()
} }
@JvmStatic @JvmStatic
fun resetThemePreferences(activity: AppCompatActivity, applyImmediately: Boolean = false) { fun resetThemePreferences(activity: AppCompatActivity, applyImmediately: Boolean = false) {
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext) PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
.edit() .remove(CURRENT_THEME).remove(CURRENT_THEME_MODE).remove(USE_BLACK_BACKGROUNDS).apply()
.remove(CURRENT_THEME)
.remove(CURRENT_THEME_MODE)
.remove(USE_BLACK_BACKGROUNDS)
.apply()
activity.delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM activity.delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
activity.delegate.applyDayNight() activity.delegate.applyDayNight()
if (applyImmediately) { if (applyImmediately) {
@@ -170,7 +154,7 @@ object ThemeHelper {
activity: AppCompatActivity, toolbar: MaterialToolbar, appBarLayout: AppBarLayout activity: AppCompatActivity, toolbar: MaterialToolbar, appBarLayout: AppBarLayout
) { ) {
appBarLayout.addOnOffsetChangedListener { layout: AppBarLayout, verticalOffset: Int -> appBarLayout.addOnOffsetChangedListener { layout: AppBarLayout, verticalOffset: Int ->
if (-verticalOffset >= layout.totalScrollRange / 2) { if (layout.totalScrollRange > 0 && -verticalOffset >= layout.totalScrollRange / 2) {
@ColorInt val color = @ColorInt val color =
ElevationOverlayProvider(appBarLayout.context).compositeOverlay( ElevationOverlayProvider(appBarLayout.context).compositeOverlay(
MaterialColors.getColor(appBarLayout, R.attr.colorSurface), MaterialColors.getColor(appBarLayout, R.attr.colorSurface),
@@ -180,8 +164,7 @@ object ThemeHelper {
setStatusBarColor(activity, color) setStatusBarColor(activity, color)
} else { } else {
@ColorInt val statusBarColor = ContextCompat.getColor( @ColorInt val statusBarColor = ContextCompat.getColor(
activity.applicationContext, activity.applicationContext, android.R.color.transparent
android.R.color.transparent
) )
@ColorInt val appBarColor = MaterialColors.getColor(toolbar, R.attr.colorSurface) @ColorInt val appBarColor = MaterialColors.getColor(toolbar, R.attr.colorSurface)
toolbar.setBackgroundColor(appBarColor) toolbar.setBackgroundColor(appBarColor)
@@ -198,8 +181,7 @@ object ThemeHelper {
setStatusBarColor(activity, color) setStatusBarColor(activity, color)
} else { } else {
@ColorInt val statusBarColor = ContextCompat.getColor( @ColorInt val statusBarColor = ContextCompat.getColor(
activity.applicationContext, activity.applicationContext, android.R.color.transparent
android.R.color.transparent
) )
setStatusBarColor(activity, statusBarColor) 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>
@@ -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"?> <?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:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator_main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSurface"> 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 <com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_settings" android:id="@+id/appbar_settings"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:background="@android:color/transparent"
app:backgroundTint="@android:color/transparent"
app:elevation="0dp"> app:elevation="0dp">
<com.google.android.material.appbar.CollapsingToolbarLayout <com.google.android.material.appbar.CollapsingToolbarLayout
style="?attr/collapsingToolbarLayoutMediumStyle"
android:id="@+id/toolbar_settings_layout" android:id="@+id/toolbar_settings_layout"
style="?attr/collapsingToolbarLayoutMediumStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize" android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
app:contentScrim="@android:color/transparent" app:contentScrim="@android:color/transparent"
@@ -31,33 +59,94 @@
</com.google.android.material.appbar.CollapsingToolbarLayout> </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> </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> </androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@@ -11,4 +10,16 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" /> 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> </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 --> <!-- Main Preference Fragment -->
<string name="settings">Settings</string> <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="game_settings">Game Settings: %1$s</string>
<string name="config">Config</string> <string name="config">Config</string>
<string name="graphics_settings">Graphics Settings</string> <string name="graphics_settings">Graphics Settings</string>