From 893be0996e6c58901f2169ca0162e5252a52e13b Mon Sep 17 00:00:00 2001 From: Simonx22 Date: Sat, 18 Jul 2026 15:45:12 -0400 Subject: [PATCH 1/2] Android: Add global settings search --- .../settings/model/view/SettingsItem.kt | 1 + .../model/view/SettingsSearchResult.kt | 19 + .../features/settings/ui/SettingsActivity.kt | 231 +++++- .../settings/ui/SettingsActivityPresenter.kt | 130 ++- .../settings/ui/SettingsActivityView.kt | 29 +- .../features/settings/ui/SettingsAdapter.kt | 35 +- .../features/settings/ui/SettingsFragment.kt | 136 +++- .../settings/ui/SettingsFragmentPresenter.kt | 769 ++++++++++-------- .../settings/ui/SettingsFragmentView.kt | 14 +- .../ui/viewholder/SettingViewHolder.kt | 43 + .../SettingsSearchResultViewHolder.kt | 29 + .../dolphinemu/utils/ThemeHelper.kt | 5 +- .../res/anim/anim_settings_search_pop_in.xml | 7 + .../res/anim/anim_settings_search_pop_out.xml | 6 + .../res/color/settings_search_outline.xml | 4 + .../app/src/main/res/drawable/ic_search.xml | 10 + .../src/main/res/layout/activity_settings.xml | 149 +++- .../src/main/res/layout/fragment_settings.xml | 15 +- .../res/layout/list_item_search_result.xml | 36 + .../app/src/main/res/values/strings.xml | 4 + 20 files changed, 1185 insertions(+), 487 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/SettingsSearchResult.kt create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingsSearchResultViewHolder.kt create mode 100644 Source/Android/app/src/main/res/anim/anim_settings_search_pop_in.xml create mode 100644 Source/Android/app/src/main/res/anim/anim_settings_search_pop_out.xml create mode 100644 Source/Android/app/src/main/res/color/settings_search_outline.xml create mode 100644 Source/Android/app/src/main/res/drawable/ic_search.xml create mode 100644 Source/Android/app/src/main/res/layout/list_item_search_result.xml diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/SettingsItem.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/SettingsItem.kt index 4d41676a14..20cfdcb2d0 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/SettingsItem.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/SettingsItem.kt @@ -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 } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/SettingsSearchResult.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/SettingsSearchResult.kt new file mode 100644 index 0000000000..dc786851ac --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/view/SettingsSearchResult.kt @@ -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 +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt index 68f957a626..1135dca463 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt @@ -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) } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityPresenter.kt index 7627a7b5c6..1615911fb6 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityPresenter.kt @@ -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" } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityView.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityView.kt index 1cd39f6fe4..44f02bc3f8 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityView.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityView.kt @@ -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. diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt index ae236d3764..2492a0113a 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt @@ -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() } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt index 1289d30f61..7e6f646478 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt @@ -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) { - 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) { + 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 = EnumMap(MenuTag::class.java) init { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt index a9ca9c3c3d..210a34b23f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt @@ -12,7 +12,11 @@ import androidx.appcompat.app.AppCompatActivity import androidx.collection.ArraySet import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.dolphinemu.dolphinemu.NativeLibrary import org.dolphinemu.dolphinemu.R @@ -29,28 +33,63 @@ import org.dolphinemu.dolphinemu.features.input.model.view.InputDeviceSetting import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting import org.dolphinemu.dolphinemu.features.input.ui.ProfileDialog import org.dolphinemu.dolphinemu.features.input.ui.ProfileDialogPresenter -import org.dolphinemu.dolphinemu.features.settings.model.* -import org.dolphinemu.dolphinemu.features.settings.model.view.* +import org.dolphinemu.dolphinemu.features.settings.model.AbstractBooleanSetting +import org.dolphinemu.dolphinemu.features.settings.model.AbstractIntSetting +import org.dolphinemu.dolphinemu.features.settings.model.AchievementModel import org.dolphinemu.dolphinemu.features.settings.model.AchievementModel.logout +import org.dolphinemu.dolphinemu.features.settings.model.AdHocBooleanSetting +import org.dolphinemu.dolphinemu.features.settings.model.AdHocStringSetting +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting +import org.dolphinemu.dolphinemu.features.settings.model.FloatSetting +import org.dolphinemu.dolphinemu.features.settings.model.IntSetting +import org.dolphinemu.dolphinemu.features.settings.model.PostProcessing +import org.dolphinemu.dolphinemu.features.settings.model.ScaledIntSetting +import org.dolphinemu.dolphinemu.features.settings.model.Settings +import org.dolphinemu.dolphinemu.features.settings.model.StringSetting +import org.dolphinemu.dolphinemu.features.settings.model.view.DateTimeChoiceSetting +import org.dolphinemu.dolphinemu.features.settings.model.view.DirectoryPicker +import org.dolphinemu.dolphinemu.features.settings.model.view.FilePicker +import org.dolphinemu.dolphinemu.features.settings.model.view.FloatSliderSetting +import org.dolphinemu.dolphinemu.features.settings.model.view.HeaderSetting +import org.dolphinemu.dolphinemu.features.settings.model.view.HyperLinkHeaderSetting +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.InvertedSwitchSetting +import org.dolphinemu.dolphinemu.features.settings.model.view.LogSwitchSetting +import org.dolphinemu.dolphinemu.features.settings.model.view.PercentSliderSetting +import org.dolphinemu.dolphinemu.features.settings.model.view.RunRunnable +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.StringSingleChoiceSetting +import org.dolphinemu.dolphinemu.features.settings.model.view.SubmenuSetting +import org.dolphinemu.dolphinemu.features.settings.model.view.SwitchSetting import org.dolphinemu.dolphinemu.model.GpuDriverMetadata -import org.dolphinemu.dolphinemu.utils.* -import kotlin.collections.ArrayList +import org.dolphinemu.dolphinemu.utils.BooleanSupplier +import org.dolphinemu.dolphinemu.utils.EGLHelper +import org.dolphinemu.dolphinemu.utils.GpuDriverHelper +import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult +import org.dolphinemu.dolphinemu.utils.ThemeHelper +import org.dolphinemu.dolphinemu.utils.ThreadUtil +import org.dolphinemu.dolphinemu.utils.WiiUtils +import java.util.Locale import kotlin.math.ceil import kotlin.math.floor class SettingsFragmentPresenter( - private val fragmentView: SettingsFragmentView, - private val context: Context + private val fragmentView: SettingsFragmentView, private val context: Context ) { private lateinit var menuTag: MenuTag private var gameId: String? = null private var settingsList: ArrayList? = null + private var searchableSettings: List? = null + private val searchIndexMutex = Mutex() + private var searchIndexGeneration = 0 private var hasOldControllerSettings = false - - private var serialPort1Type = 0 - private var controllerNumber = 0 - private var controllerType = 0 + private var menuExtras = Bundle() + private var shouldUpdateWarnings = true var gpuDriver: GpuDriverMetadata? = null private val libNameSetting: StringSetting = StringSetting.GFX_DRIVER_LIB_NAME @@ -58,23 +97,11 @@ class SettingsFragmentPresenter( fun onCreate(menuTag: MenuTag, gameId: String?, extras: Bundle) { this.gameId = gameId this.menuTag = menuTag + menuExtras = Bundle(extras) - if (menuTag.isGCPadMenu || menuTag.isWiimoteExtensionMenu) { - controllerNumber = menuTag.subType - controllerType = extras.getInt(ARG_CONTROLLER_TYPE) - } else if (menuTag.isWiimoteMenu || menuTag.isWiimoteSubmenu) { - controllerNumber = menuTag.subType - } else if (menuTag.isSerialPort1Menu) { - serialPort1Type = extras.getInt(ARG_SERIALPORT1_TYPE) - } else if ( - menuTag == MenuTag.GRAPHICS - && this.gameId.isNullOrEmpty() - && NativeLibrary.IsUninitialized() - && GpuDriverHelper.supportsCustomDriverLoading() - ) { - this.gpuDriver = - GpuDriverHelper.getInstalledDriverMetadata() - ?: GpuDriverHelper.getSystemDriverMetadata(context.applicationContext) + if (menuTag == MenuTag.GRAPHICS && this.gameId.isNullOrEmpty() && NativeLibrary.IsUninitialized() && GpuDriverHelper.supportsCustomDriverLoading()) { + this.gpuDriver = GpuDriverHelper.getInstalledDriverMetadata() + ?: GpuDriverHelper.getSystemDriverMetadata(context.applicationContext) } } @@ -100,85 +127,222 @@ class SettingsFragmentPresenter( } fun loadSettingsList() { - val sl = ArrayList() - when (menuTag) { - MenuTag.SETTINGS -> addTopLevelSettings(sl) - MenuTag.CONFIG -> addConfigSettings(sl) - MenuTag.CONFIG_GENERAL -> addGeneralSettings(sl) - MenuTag.CONFIG_INTERFACE -> addInterfaceSettings(sl) - MenuTag.CONFIG_AUDIO -> addAudioSettings(sl) - MenuTag.CONFIG_PATHS -> addPathsSettings(sl) - MenuTag.CONFIG_GAME_CUBE -> addGameCubeSettings(sl) - MenuTag.CONFIG_WII -> addWiiSettings(sl) - MenuTag.CONFIG_ACHIEVEMENTS -> addAchievementSettings(sl); - MenuTag.CONFIG_ADVANCED -> addAdvancedSettings(sl) - MenuTag.GRAPHICS -> addGraphicsSettings(sl) - MenuTag.CONFIG_SERIALPORT1 -> addSerialPortSubSettings(sl, serialPort1Type) - MenuTag.GCPAD_TYPE -> addGcPadSettings(sl) - MenuTag.WIIMOTE -> addWiimoteSettings(sl) - MenuTag.ENHANCEMENTS -> addEnhanceSettings(sl) - MenuTag.COLOR_CORRECTION -> addColorCorrectionSettings(sl) - MenuTag.STEREOSCOPY -> addStereoSettings(sl) - MenuTag.HACKS -> addHackSettings(sl) - MenuTag.STATISTICS -> addStatisticsSettings(sl) - MenuTag.ADVANCED_GRAPHICS -> addAdvancedGraphicsSettings(sl) - MenuTag.CONFIG_LOG -> addLogConfigurationSettings(sl) - MenuTag.DEBUG -> addDebugSettings(sl) - MenuTag.GCPAD_1, - MenuTag.GCPAD_2, - MenuTag.GCPAD_3, - MenuTag.GCPAD_4 -> addGcPadSubSettings( - sl, - controllerNumber, - controllerType - ) + invalidateSearchIndex() + settingsList = tryBuildSettingsList(menuTag, menuExtras, true) + ?: throw UnsupportedOperationException("Unimplemented menu") + fragmentView.showSettingsList(settingsList!!) + } - MenuTag.WIIMOTE_1, - MenuTag.WIIMOTE_2, - MenuTag.WIIMOTE_3, - MenuTag.WIIMOTE_4 -> addWiimoteSubSettings( - sl, - controllerNumber - ) + fun getSettingsList(): ArrayList = settingsList ?: arrayListOf() - MenuTag.WIIMOTE_EXTENSION_1, - MenuTag.WIIMOTE_EXTENSION_2, - MenuTag.WIIMOTE_EXTENSION_3, - MenuTag.WIIMOTE_EXTENSION_4 -> addExtensionTypeSettings( - sl, - controllerNumber, - controllerType - ) + suspend fun prepareSearchIndex() { + getSearchIndex() + } - MenuTag.WIIMOTE_GENERAL_1, - MenuTag.WIIMOTE_GENERAL_2, - MenuTag.WIIMOTE_GENERAL_3, - MenuTag.WIIMOTE_GENERAL_4 -> addWiimoteGeneralSubSettings( - sl, - controllerNumber - ) - - MenuTag.WIIMOTE_MOTION_SIMULATION_1, - MenuTag.WIIMOTE_MOTION_SIMULATION_2, - MenuTag.WIIMOTE_MOTION_SIMULATION_3, - MenuTag.WIIMOTE_MOTION_SIMULATION_4 -> addWiimoteMotionSimulationSubSettings( - sl, - controllerNumber - ) - - MenuTag.WIIMOTE_MOTION_INPUT_1, - MenuTag.WIIMOTE_MOTION_INPUT_2, - MenuTag.WIIMOTE_MOTION_INPUT_3, - MenuTag.WIIMOTE_MOTION_INPUT_4 -> addWiimoteMotionInputSubSettings( - sl, - controllerNumber - ) - - else -> throw UnsupportedOperationException("Unimplemented menu") + suspend fun searchSettings(query: String): ArrayList { + val normalizedQuery = query.trim().lowercase(Locale.ROOT) + if (normalizedQuery.isEmpty()) { + return getSettingsList() } - settingsList = sl - fragmentView.showSettingsList(settingsList!!) + val terms = normalizedQuery.split(Regex("\\s+")) + val index = getSearchIndex() + return withContext(Dispatchers.Default) { + index.asSequence() + .filter { setting -> terms.all(setting.normalizedSearchText::contains) } + .map { setting -> + val score = when { + setting.normalizedName == normalizedQuery -> 0 + setting.normalizedName.startsWith(normalizedQuery) -> 1 + terms.all(setting.normalizedName::contains) -> 2 + setting.normalizedCategory.contains(normalizedQuery) -> 3 + else -> 4 + } + score to SettingsSearchResult( + setting.name, + setting.description, + setting.menuTag, + setting.position, + setting.navigationExtras + ) + } + .sortedWith(compareBy> { it.first }.thenBy(String.CASE_INSENSITIVE_ORDER) { it.second.name.toString() }) + .mapTo(ArrayList()) { it.second } + } + } + + private suspend fun getSearchIndex(): List { + return searchIndexMutex.withLock { + searchableSettings?.let { return@withLock it } + + val generation = searchIndexGeneration + val index = withContext(Dispatchers.IO) { + buildSearchIndex() + } + if (generation == searchIndexGeneration) { + searchableSettings = index + } + index + } + } + + private suspend fun buildSearchIndex(): List { + return getSearchableMenus().flatMap { searchableMenu -> + currentCoroutineContext().ensureActive() + searchableMenu.settings.withIndex().asSequence() + .filterNot { it.value is HeaderSetting || it.value is SubmenuSetting } + .map { indexedItem -> + val item = indexedItem.value + val name = item.name.toString() + val description = item.description.toString() + SearchableSetting( + name = name, + description = searchableMenu.category, + menuTag = searchableMenu.menuTag, + navigationExtras = searchableMenu.navigationExtras, + position = indexedItem.index, + normalizedName = name.lowercase(Locale.ROOT), + normalizedCategory = searchableMenu.category.lowercase(Locale.ROOT), + normalizedSearchText = "$name $description ${searchableMenu.category}".lowercase( + Locale.ROOT + ) + ) + }.toList() + } + } + + fun invalidateSearchIndex() { + searchableSettings = null + searchIndexGeneration++ + } + + private suspend fun getSearchableMenus(): List { + val menus = mutableListOf() + val visitedMenus = mutableSetOf() + + suspend fun visitMenu( + menuTag: MenuTag, + category: String, + navigationExtras: Bundle? = null, + actionValue: Int? = null + ) { + currentCoroutineContext().ensureActive() + if (!visitedMenus.add("$menuTag:$actionValue")) { + return + } + + val items = tryBuildSettingsList(menuTag, navigationExtras ?: Bundle(), false) ?: return + if (menuTag != MenuTag.SETTINGS) { + menus += SearchableMenu(menuTag, category, navigationExtras, items) + } + + suspend fun visitMenuAction( + childMenuTag: MenuTag?, selectedValue: Int, childCategory: String + ) { + if (childMenuTag != null && fragmentView.hasMenuTagActionForValue( + childMenuTag, selectedValue + ) + ) { + visitMenu( + childMenuTag, + childCategory, + fragmentView.getMenuTagActionExtras(childMenuTag, selectedValue), + selectedValue + ) + } + } + + for (item in items) { + currentCoroutineContext().ensureActive() + val childCategory = if (category.isEmpty()) item.name.toString() + else context.getString( + R.string.search_settings_category_path, category, item.name + ) + + when (item) { + is SubmenuSetting -> visitMenu(item.menuKey, childCategory) + is SingleChoiceSetting -> { + visitMenuAction(item.menuTag, item.selectedValue, childCategory) + } + + is StringSingleChoiceSetting -> { + visitMenuAction(item.menuTag, item.selectedValueIndex, childCategory) + } + } + } + } + + visitMenu(MenuTag.SETTINGS, "") + return menus + } + + private fun tryBuildSettingsList( + targetMenuTag: MenuTag, extras: Bundle, updateWarnings: Boolean + ): ArrayList? { + val sl = ArrayList() + var isSupportedMenu = true + val previousMenuTag = menuTag + val previousShouldUpdateWarnings = shouldUpdateWarnings + menuTag = targetMenuTag + shouldUpdateWarnings = updateWarnings + try { + when (targetMenuTag) { + MenuTag.SETTINGS -> addTopLevelSettings(sl) + MenuTag.CONFIG -> addConfigSettings(sl) + MenuTag.CONFIG_GENERAL -> addGeneralSettings(sl) + MenuTag.CONFIG_INTERFACE -> addInterfaceSettings(sl) + MenuTag.CONFIG_AUDIO -> addAudioSettings(sl) + MenuTag.CONFIG_PATHS -> addPathsSettings(sl) + MenuTag.CONFIG_GAME_CUBE -> addGameCubeSettings(sl) + MenuTag.CONFIG_WII -> addWiiSettings(sl) + MenuTag.CONFIG_ACHIEVEMENTS -> addAchievementSettings(sl) + MenuTag.CONFIG_ADVANCED -> addAdvancedSettings(sl) + MenuTag.GRAPHICS -> addGraphicsSettings(sl) + MenuTag.CONFIG_SERIALPORT1 -> addSerialPortSubSettings( + sl, extras.getInt(ARG_SERIALPORT1_TYPE) + ) + + MenuTag.GCPAD_TYPE -> addGcPadSettings(sl) + MenuTag.WIIMOTE -> addWiimoteSettings(sl) + MenuTag.ENHANCEMENTS -> addEnhanceSettings(sl) + MenuTag.COLOR_CORRECTION -> addColorCorrectionSettings(sl) + MenuTag.STEREOSCOPY -> addStereoSettings(sl) + MenuTag.HACKS -> addHackSettings(sl) + MenuTag.STATISTICS -> addStatisticsSettings(sl) + MenuTag.ADVANCED_GRAPHICS -> addAdvancedGraphicsSettings(sl) + MenuTag.CONFIG_LOG -> addLogConfigurationSettings(sl) + MenuTag.DEBUG -> addDebugSettings(sl) + MenuTag.GCPAD_1, MenuTag.GCPAD_2, MenuTag.GCPAD_3, MenuTag.GCPAD_4 -> addGcPadSubSettings( + sl, targetMenuTag.subType, extras.getInt(ARG_CONTROLLER_TYPE) + ) + + MenuTag.WIIMOTE_1, MenuTag.WIIMOTE_2, MenuTag.WIIMOTE_3, MenuTag.WIIMOTE_4 -> addWiimoteSubSettings( + sl, targetMenuTag.subType + ) + + MenuTag.WIIMOTE_EXTENSION_1, MenuTag.WIIMOTE_EXTENSION_2, MenuTag.WIIMOTE_EXTENSION_3, MenuTag.WIIMOTE_EXTENSION_4 -> addExtensionTypeSettings( + sl, targetMenuTag.subType, extras.getInt(ARG_CONTROLLER_TYPE) + ) + + MenuTag.WIIMOTE_GENERAL_1, MenuTag.WIIMOTE_GENERAL_2, MenuTag.WIIMOTE_GENERAL_3, MenuTag.WIIMOTE_GENERAL_4 -> addWiimoteGeneralSubSettings( + sl, targetMenuTag.subType + ) + + MenuTag.WIIMOTE_MOTION_SIMULATION_1, MenuTag.WIIMOTE_MOTION_SIMULATION_2, MenuTag.WIIMOTE_MOTION_SIMULATION_3, MenuTag.WIIMOTE_MOTION_SIMULATION_4 -> addWiimoteMotionSimulationSubSettings( + sl, targetMenuTag.subType + ) + + MenuTag.WIIMOTE_MOTION_INPUT_1, MenuTag.WIIMOTE_MOTION_INPUT_2, MenuTag.WIIMOTE_MOTION_INPUT_3, MenuTag.WIIMOTE_MOTION_INPUT_4 -> addWiimoteMotionInputSubSettings( + sl, targetMenuTag.subType + ) + + else -> isSupportedMenu = false + } + } finally { + menuTag = previousMenuTag + shouldUpdateWarnings = previousShouldUpdateWarnings + } + return if (isSupportedMenu) sl else null } private fun addTopLevelSettings(sl: ArrayList) { @@ -205,9 +369,9 @@ class SettingsFragmentPresenter( sl.add(SubmenuSetting(context, R.string.log_submenu, MenuTag.CONFIG_LOG)) sl.add(SubmenuSetting(context, R.string.debug_submenu, MenuTag.DEBUG)) sl.add( - RunRunnable(context, R.string.user_data_submenu, 0, 0, 0, false) - { UserDataActivity.launch(context) } - ) + RunRunnable( + context, R.string.user_data_submenu, 0, 0, 0, false + ) { UserDataActivity.launch(context) }) } private fun addGeneralSettings(sl: ArrayList) { @@ -221,10 +385,7 @@ class SettingsFragmentPresenter( ) sl.add( SwitchSetting( - context, - BooleanSetting.MAIN_ENABLE_CHEATS, - R.string.enable_cheats, - 0 + context, BooleanSetting.MAIN_ENABLE_CHEATS, R.string.enable_cheats, 0 ) ) sl.add( @@ -237,10 +398,7 @@ class SettingsFragmentPresenter( ) sl.add( SwitchSetting( - context, - BooleanSetting.MAIN_AUTO_DISC_CHANGE, - R.string.auto_disc_change, - 0 + context, BooleanSetting.MAIN_AUTO_DISC_CHANGE, R.string.auto_disc_change, 0 ) ) sl.add( @@ -268,10 +426,7 @@ class SettingsFragmentPresenter( ) sl.add( SwitchSetting( - context, - BooleanSetting.MAIN_ANALYTICS_ENABLED, - R.string.analytics, - 0 + context, BooleanSetting.MAIN_ANALYTICS_ENABLED, R.string.analytics, 0 ) ) sl.add( @@ -282,8 +437,7 @@ class SettingsFragmentPresenter( R.string.analytics_new_id_confirmation, 0, true - ) { NativeLibrary.GenerateNewStatisticsId() } - ) + ) { NativeLibrary.GenerateNewStatisticsId() }) sl.add( SwitchSetting( context, @@ -298,8 +452,9 @@ class SettingsFragmentPresenter( // Hide the orientation setting if the device only supports one orientation. Old devices which // support both portrait and landscape may report support for neither, so we use ==, not &&. val packageManager = context.packageManager - if (packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_PORTRAIT) == - packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_LANDSCAPE) + if (packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_PORTRAIT) == packageManager.hasSystemFeature( + PackageManager.FEATURE_SCREEN_LANDSCAPE + ) ) { sl.add( SingleChoiceSetting( @@ -368,8 +523,7 @@ class SettingsFragmentPresenter( override fun setInt(settings: Settings, newValue: Int) { IntSetting.MAIN_INTERFACE_THEME.setInt(settings, newValue) ThemeHelper.saveTheme( - (fragmentView.fragmentActivity as AppCompatActivity), - newValue + (fragmentView.fragmentActivity as AppCompatActivity), newValue ) } } @@ -417,8 +571,7 @@ class SettingsFragmentPresenter( override fun setInt(settings: Settings, newValue: Int) { IntSetting.MAIN_INTERFACE_THEME_MODE.setInt(settings, newValue) ThemeHelper.saveThemeMode( - (fragmentView.fragmentActivity as AppCompatActivity), - newValue + (fragmentView.fragmentActivity as AppCompatActivity), newValue ) } } @@ -451,8 +604,7 @@ class SettingsFragmentPresenter( override fun setBoolean(settings: Settings, newValue: Boolean) { BooleanSetting.MAIN_USE_BLACK_BACKGROUNDS.setBoolean(settings, newValue) ThemeHelper.saveBackgroundSetting( - (fragmentView.fragmentActivity as AppCompatActivity), - newValue + (fragmentView.fragmentActivity as AppCompatActivity), newValue ) } } @@ -501,17 +653,16 @@ class SettingsFragmentPresenter( } override val isOverridden: Boolean - get() = BooleanSetting.MAIN_DSP_HLE.isOverridden || - BooleanSetting.MAIN_DSP_JIT.isOverridden + get() = BooleanSetting.MAIN_DSP_HLE.isOverridden || BooleanSetting.MAIN_DSP_JIT.isOverridden override val isRuntimeEditable: Boolean - get() = BooleanSetting.MAIN_DSP_HLE.isRuntimeEditable && - BooleanSetting.MAIN_DSP_JIT.isRuntimeEditable + get() = BooleanSetting.MAIN_DSP_HLE.isRuntimeEditable && BooleanSetting.MAIN_DSP_JIT.isRuntimeEditable override fun delete(settings: Settings): Boolean { // Not short circuiting - return BooleanSetting.MAIN_DSP_HLE.delete(settings) and - BooleanSetting.MAIN_DSP_JIT.delete(settings) + return BooleanSetting.MAIN_DSP_HLE.delete(settings) and BooleanSetting.MAIN_DSP_JIT.delete( + settings + ) } } @@ -566,14 +717,7 @@ class SettingsFragmentPresenter( ) sl.add( IntSliderSetting( - context, - IntSetting.MAIN_AUDIO_VOLUME, - R.string.audio_volume, - 0, - 0, - 100, - "%", - 1 + context, IntSetting.MAIN_AUDIO_VOLUME, R.string.audio_volume, 0, 0, 100, "%", 1 ) ) } @@ -581,10 +725,7 @@ class SettingsFragmentPresenter( private fun addPathsSettings(sl: ArrayList) { sl.add( SwitchSetting( - context, - BooleanSetting.MAIN_RECURSIVE_ISO_PATHS, - R.string.search_subfolders, - 0 + context, BooleanSetting.MAIN_RECURSIVE_ISO_PATHS, R.string.search_subfolders, 0 ) ) sl.add( @@ -802,10 +943,7 @@ class SettingsFragmentPresenter( ) sl.add( SwitchSetting( - context, - BooleanSetting.MAIN_ALLOW_SD_WRITES, - R.string.wii_sd_card_allow_writes, - 0 + context, BooleanSetting.MAIN_ALLOW_SD_WRITES, R.string.wii_sd_card_allow_writes, 0 ) ) sl.add( @@ -847,8 +985,7 @@ class SettingsFragmentPresenter( R.string.wii_sd_card_folder_to_file_confirmation, 0, false - ) { convertOnThread { WiiUtils.syncSdFolderToSdImage() } } - ) + ) { convertOnThread { WiiUtils.syncSdFolderToSdImage() } }) sl.add( RunRunnable( context, @@ -857,16 +994,12 @@ class SettingsFragmentPresenter( R.string.wii_sd_card_file_to_folder_confirmation, 0, false - ) { convertOnThread { WiiUtils.syncSdImageToSdFolder() } } - ) + ) { convertOnThread { WiiUtils.syncSdImageToSdFolder() } }) sl.add(HeaderSetting(context, R.string.wii_wiimote_settings, 0)) sl.add( SwitchSetting( - context, - BooleanSetting.SYSCONF_WIIMOTE_MOTOR, - R.string.wiimote_rumble, - 0 + context, BooleanSetting.SYSCONF_WIIMOTE_MOTOR, R.string.wiimote_rumble, 0 ) ) sl.add( @@ -939,18 +1072,12 @@ class SettingsFragmentPresenter( ) sl.add( SwitchSetting( - context, - BooleanSetting.MAIN_EMULATE_WII_SPEAK, - R.string.emulate_wii_speak, - 0 + context, BooleanSetting.MAIN_EMULATE_WII_SPEAK, R.string.emulate_wii_speak, 0 ) ) sl.add( SwitchSetting( - context, - BooleanSetting.MAIN_WII_SPEAK_MUTED, - R.string.mute_wii_speak, - 0 + context, BooleanSetting.MAIN_WII_SPEAK_MUTED, R.string.mute_wii_speak, 0 ) ) } @@ -962,10 +1089,8 @@ class SettingsFragmentPresenter( override fun setBoolean(settings: Settings, newValue: Boolean) { BooleanSetting.ACHIEVEMENTS_ENABLED.setBoolean(settings, newValue) - if (newValue) - AchievementModel.init() - else - AchievementModel.shutdown() + if (newValue) AchievementModel.init() + else AchievementModel.shutdown() loadSettingsList() } @@ -985,38 +1110,25 @@ class SettingsFragmentPresenter( sl.add( SwitchSetting( - context, - achievementsEnabledSetting, - R.string.achievements_enabled, - 0 + context, achievementsEnabledSetting, R.string.achievements_enabled, 0 ) ) if (BooleanSetting.ACHIEVEMENTS_ENABLED.boolean) { if (StringSetting.ACHIEVEMENTS_API_TOKEN.string == "") { sl.add( RunRunnable( - context, - R.string.achievements_login, - 0, - 0, - 0, - false + context, R.string.achievements_login, 0, 0, 0, false ) { - fragmentView.showDialogFragment(LoginDialog(this)) - loadSettingsList() + fragmentView.showDialogFragment(LoginDialog(this)) + loadSettingsList() }) } else { sl.add( RunRunnable( - context, - R.string.achievements_logout, - 0, - 0, - 0, - false + context, R.string.achievements_logout, 0, 0, 0, false ) { - logout() - loadSettingsList() + logout() + loadSettingsList() }) } sl.add( @@ -1112,17 +1224,16 @@ class SettingsFragmentPresenter( } override val isOverridden: Boolean - get() = BooleanSetting.MAIN_SYNC_ON_SKIP_IDLE.isOverridden || - BooleanSetting.MAIN_SYNC_GPU.isOverridden + get() = BooleanSetting.MAIN_SYNC_ON_SKIP_IDLE.isOverridden || BooleanSetting.MAIN_SYNC_GPU.isOverridden override val isRuntimeEditable: Boolean - get() = BooleanSetting.MAIN_SYNC_ON_SKIP_IDLE.isRuntimeEditable && - BooleanSetting.MAIN_SYNC_GPU.isRuntimeEditable + get() = BooleanSetting.MAIN_SYNC_ON_SKIP_IDLE.isRuntimeEditable && BooleanSetting.MAIN_SYNC_GPU.isRuntimeEditable override fun delete(settings: Settings): Boolean { // Not short circuiting - return BooleanSetting.MAIN_SYNC_ON_SKIP_IDLE.delete(settings) and - BooleanSetting.MAIN_SYNC_GPU.delete(settings) + return BooleanSetting.MAIN_SYNC_ON_SKIP_IDLE.delete(settings) and BooleanSetting.MAIN_SYNC_GPU.delete( + settings + ) } } @@ -1243,26 +1354,12 @@ class SettingsFragmentPresenter( ) sl.add( IntSliderSetting( - context, - mem1Size, - R.string.main_mem1_size, - 0, - 24, - 64, - "MB", - 1 + context, mem1Size, R.string.main_mem1_size, 0, 24, 64, "MB", 1 ) ) sl.add( IntSliderSetting( - context, - mem2Size, - R.string.main_mem2_size, - 0, - 64, - 128, - "MB", - 1 + context, mem2Size, R.string.main_mem2_size, 0, 64, 128, "MB", 1 ) ) @@ -1289,10 +1386,7 @@ class SettingsFragmentPresenter( ) sl.add( DateTimeChoiceSetting( - context, - StringSetting.MAIN_CUSTOM_RTC_VALUE, - R.string.set_custom_rtc, - 0 + context, StringSetting.MAIN_CUSTOM_RTC_VALUE, R.string.set_custom_rtc, 0 ) ) @@ -1520,42 +1614,29 @@ class SettingsFragmentPresenter( sl.add(HeaderSetting(context, R.string.graphics_more_settings, 0)) sl.add( SubmenuSetting( - context, - R.string.enhancements_submenu, - MenuTag.ENHANCEMENTS + context, R.string.enhancements_submenu, MenuTag.ENHANCEMENTS ) ) sl.add( SubmenuSetting( - context, - R.string.hacks_submenu, - MenuTag.HACKS + context, R.string.hacks_submenu, MenuTag.HACKS ) ) sl.add( SubmenuSetting( - context, - R.string.statistics_submenu, - MenuTag.STATISTICS + context, R.string.statistics_submenu, MenuTag.STATISTICS ) ) sl.add( SubmenuSetting( - context, - R.string.advanced_graphics_submenu, - MenuTag.ADVANCED_GRAPHICS + context, R.string.advanced_graphics_submenu, MenuTag.ADVANCED_GRAPHICS ) ) - if ( - this.gpuDriver != null && this.gameId.isNullOrEmpty() - && NativeLibrary.IsUninitialized() - && GpuDriverHelper.supportsCustomDriverLoading() - ) { + if (this.gpuDriver != null && this.gameId.isNullOrEmpty() && NativeLibrary.IsUninitialized() && GpuDriverHelper.supportsCustomDriverLoading()) { sl.add( SubmenuSetting( - context, - R.string.gpu_driver_submenu, MenuTag.GPU_DRIVERS + context, R.string.gpu_driver_submenu, MenuTag.GPU_DRIVERS ) ) } @@ -1604,9 +1685,7 @@ class SettingsFragmentPresenter( ) sl.add( SubmenuSetting( - context, - R.string.color_correction_submenu, - MenuTag.COLOR_CORRECTION + context, R.string.color_correction_submenu, MenuTag.COLOR_CORRECTION ) ) @@ -1697,9 +1776,7 @@ class SettingsFragmentPresenter( ) { sl.add( SubmenuSetting( - context, - R.string.stereoscopy_submenu, - MenuTag.STEREOSCOPY + context, R.string.stereoscopy_submenu, MenuTag.STEREOSCOPY ) ) } @@ -1743,10 +1820,7 @@ class SettingsFragmentPresenter( ) add( SwitchSetting( - context, - BooleanSetting.GFX_CC_CORRECT_GAMMA, - R.string.correct_sdr_gamma, - 0 + context, BooleanSetting.GFX_CC_CORRECT_GAMMA, R.string.correct_sdr_gamma, 0 ) ) } @@ -2053,10 +2127,10 @@ class SettingsFragmentPresenter( IntSetting.GFX_CROP_CUSTOM_LEFT, R.string.crop_custom_left, R.string.crop_custom_left_description, - min=0, - max=640, - units="px", - stepSize=1, + min = 0, + max = 640, + units = "px", + stepSize = 1, ) ) sl.add( @@ -2075,18 +2149,12 @@ class SettingsFragmentPresenter( sl.add(HeaderSetting(context, R.string.misc, 0)) sl.add( SwitchSetting( - context, - BooleanSetting.SYSCONF_PROGRESSIVE_SCAN, - R.string.progressive_scan, - 0 + context, BooleanSetting.SYSCONF_PROGRESSIVE_SCAN, R.string.progressive_scan, 0 ) ) sl.add( SwitchSetting( - context, - BooleanSetting.GFX_VSYNC, - R.string.vsync, - R.string.vsync_description + context, BooleanSetting.GFX_VSYNC, R.string.vsync, R.string.vsync_description ) ) sl.add( @@ -2242,35 +2310,21 @@ class SettingsFragmentPresenter( IntSetting.LOGGER_VERBOSITY, R.string.log_verbosity, 0, - getLogVerbosityEntries(), getLogVerbosityValues() + getLogVerbosityEntries(), + getLogVerbosityValues() ) ) sl.add( RunRunnable( - context, - R.string.log_enable_all, - 0, - R.string.log_enable_all_confirmation, - 0, - true + context, R.string.log_enable_all, 0, R.string.log_enable_all_confirmation, 0, true ) { setAllLogTypes(true) }) sl.add( RunRunnable( - context, - R.string.log_disable_all, - 0, - R.string.log_disable_all_confirmation, - 0, - true + context, R.string.log_disable_all, 0, R.string.log_disable_all_confirmation, 0, true ) { setAllLogTypes(false) }) sl.add( RunRunnable( - context, - R.string.log_clear, - 0, - R.string.log_clear_confirmation, - 0, - true + context, R.string.log_clear, 0, R.string.log_clear_confirmation, 0, true ) { SettingsAdapter.clearLog() }) sl.add(HeaderSetting(context, R.string.log_types, 0)) @@ -2283,10 +2337,7 @@ class SettingsFragmentPresenter( sl.add(HeaderSetting(context, R.string.debug_warning, 0)) sl.add( InvertedSwitchSetting( - context, - BooleanSetting.MAIN_FASTMEM, - R.string.debug_fastmem, - 0 + context, BooleanSetting.MAIN_FASTMEM, R.string.debug_fastmem, 0 ) ) sl.add( @@ -2299,10 +2350,7 @@ class SettingsFragmentPresenter( ) sl.add( InvertedSwitchSetting( - context, - BooleanSetting.MAIN_FASTMEM_ARENA, - R.string.debug_fastmem_arena, - 0 + context, BooleanSetting.MAIN_FASTMEM_ARENA, R.string.debug_fastmem_arena, 0 ) ) sl.add( @@ -2321,7 +2369,7 @@ class SettingsFragmentPresenter( BooleanSetting.MAIN_DEBUG_JIT_ENABLE_PROFILING, R.string.debug_jit_enable_block_profiling, 0 - ) + ) ) sl.add( RunRunnable( @@ -2331,26 +2379,16 @@ class SettingsFragmentPresenter( R.string.debug_jit_wipe_block_profiling_data_alert, 0, true - ) { NativeLibrary.WipeJitBlockProfilingData() } - ) + ) { NativeLibrary.WipeJitBlockProfilingData() }) sl.add( RunRunnable( - context, - R.string.debug_jit_write_block_log_dump, - 0, - 0, - 0, - true - ) { NativeLibrary.WriteJitBlockLogDump() } - ) + context, R.string.debug_jit_write_block_log_dump, 0, 0, 0, true + ) { NativeLibrary.WriteJitBlockLogDump() }) sl.add(HeaderSetting(context, R.string.debug_jit_header, 0)) sl.add( SwitchSetting( - context, - BooleanSetting.MAIN_DEBUG_JIT_OFF, - R.string.debug_jitoff, - 0 + context, BooleanSetting.MAIN_DEBUG_JIT_OFF, R.string.debug_jitoff, 0 ) ) sl.add( @@ -2387,18 +2425,12 @@ class SettingsFragmentPresenter( ) sl.add( SwitchSetting( - context, - BooleanSetting.MAIN_DEBUG_JIT_INTEGER_OFF, - R.string.debug_jitintegeroff, - 0 + context, BooleanSetting.MAIN_DEBUG_JIT_INTEGER_OFF, R.string.debug_jitintegeroff, 0 ) ) sl.add( SwitchSetting( - context, - BooleanSetting.MAIN_DEBUG_JIT_PAIRED_OFF, - R.string.debug_jitpairedoff, - 0 + context, BooleanSetting.MAIN_DEBUG_JIT_PAIRED_OFF, R.string.debug_jitpairedoff, 0 ) ) sl.add( @@ -2411,10 +2443,7 @@ class SettingsFragmentPresenter( ) sl.add( SwitchSetting( - context, - BooleanSetting.MAIN_DEBUG_JIT_BRANCH_OFF, - R.string.debug_jitbranchoff, - 0 + context, BooleanSetting.MAIN_DEBUG_JIT_BRANCH_OFF, R.string.debug_jitbranchoff, 0 ) ) sl.add( @@ -2474,7 +2503,9 @@ class SettingsFragmentPresenter( ) } - private fun addGcPadSubSettings(sl: ArrayList, gcPadNumber: Int, gcPadType: Int) { + private fun addGcPadSubSettings( + sl: ArrayList, gcPadNumber: Int, gcPadType: Int + ) { when (gcPadType) { 6, 8, 9, 10, 11 -> { // Emulated @@ -2484,9 +2515,10 @@ class SettingsFragmentPresenter( addControllerPerGameSettings(sl, gcPad, gcPadNumber) } else { addControllerMetaSettings(sl, gcPad) - addControllerMappingSettings(sl, gcPad, null) + addControllerMappingSettings(sl, gcPad, gcPadNumber, null) } } + 7 -> { // Emulated keyboard controller val gcKeyboard = EmulatedController.getGcKeyboard(gcPadNumber) @@ -2496,9 +2528,12 @@ class SettingsFragmentPresenter( } else { sl.add(HeaderSetting(context, R.string.keyboard_controller_warning, 0)) addControllerMetaSettings(sl, gcKeyboard) - addControllerMappingSettings(sl, gcKeyboard, null) + addControllerMappingSettings( + sl, gcKeyboard, gcPadNumber, null + ) } } + 12 -> { // Adapter sl.add( @@ -2521,7 +2556,9 @@ class SettingsFragmentPresenter( } } - private fun addWiimoteSubSettings(sl: ArrayList, wiimoteNumber: Int) { + private fun addWiimoteSubSettings( + sl: ArrayList, wiimoteNumber: Int + ) { val wiimote = EmulatedController.getWiimote(wiimoteNumber) if (!TextUtils.isEmpty(gameId)) { @@ -2557,39 +2594,40 @@ class SettingsFragmentPresenter( addControllerMappingSettings( sl, wiimote, + wiimoteNumber, ArraySet(listOf(ControlGroup.TYPE_ATTACHMENTS, ControlGroup.TYPE_OTHER)) ) } } private fun addExtensionTypeSettings( - sl: ArrayList, - wiimoteNumber: Int, - extensionType: Int + sl: ArrayList, wiimoteNumber: Int, extensionType: Int ) { addContainerMappingSettings( sl, EmulatedController.getWiimote(wiimoteNumber), EmulatedController.getWiimoteAttachment(wiimoteNumber, extensionType), + wiimoteNumber, null ) } - private fun addWiimoteGeneralSubSettings(sl: ArrayList, wiimoteNumber: Int) { + private fun addWiimoteGeneralSubSettings( + sl: ArrayList, wiimoteNumber: Int + ) { addControllerMappingSettings( sl, EmulatedController.getWiimote(wiimoteNumber), + wiimoteNumber, setOf(ControlGroup.TYPE_BUTTONS) ) } private fun addWiimoteMotionSimulationSubSettings( - sl: ArrayList, - wiimoteNumber: Int + sl: ArrayList, wiimoteNumber: Int ) { addControllerMappingSettings( - sl, EmulatedController.getWiimote(wiimoteNumber), - ArraySet( + sl, EmulatedController.getWiimote(wiimoteNumber), wiimoteNumber, ArraySet( listOf( ControlGroup.TYPE_FORCE, ControlGroup.TYPE_TILT, @@ -2600,10 +2638,11 @@ class SettingsFragmentPresenter( ) } - private fun addWiimoteMotionInputSubSettings(sl: ArrayList, wiimoteNumber: Int) { + private fun addWiimoteMotionInputSubSettings( + sl: ArrayList, wiimoteNumber: Int + ) { addControllerMappingSettings( - sl, EmulatedController.getWiimote(wiimoteNumber), - ArraySet( + sl, EmulatedController.getWiimote(wiimoteNumber), wiimoteNumber, ArraySet( listOf( ControlGroup.TYPE_IMU_ACCELEROMETER, ControlGroup.TYPE_IMU_GYROSCOPE, @@ -2621,9 +2660,7 @@ class SettingsFragmentPresenter( * @param controllerNumber The index of the controller, 0-3. */ private fun addControllerPerGameSettings( - sl: ArrayList, - controller: EmulatedController, - controllerNumber: Int + sl: ArrayList, controller: EmulatedController, controllerNumber: Int ) { val profiles = ProfileDialogPresenter(menuTag).getProfileNames(false) val profileKey = controller.getProfileKey() + "Profile" + (controllerNumber + 1) @@ -2648,15 +2685,11 @@ class SettingsFragmentPresenter( * @param controller The controller to add settings for. */ private fun addControllerMetaSettings( - sl: ArrayList, - controller: EmulatedController + sl: ArrayList, controller: EmulatedController ) { sl.add( InputDeviceSetting( - context, - R.string.input_device, - 0, - controller + context, R.string.input_device, 0, controller ) ) @@ -2698,12 +2731,7 @@ class SettingsFragmentPresenter( ) { clearControllerSettings(controller) }) sl.add( RunRunnable( - context, - R.string.input_profiles, - 0, - 0, - 0, - true + context, R.string.input_profiles, 0, 0, 0, true ) { fragmentView.showDialogFragment(ProfileDialog.create(menuTag)) }) updateOldControllerSettingsWarningVisibility(controller) @@ -2714,14 +2742,18 @@ class SettingsFragmentPresenter( * * @param sl The list to place controller settings into. * @param controller The controller to add settings for. + * @param controllerNumber The zero-based controller number. * @param groupTypeFilter If this is non-null, only groups whose types match this are considered. */ private fun addControllerMappingSettings( - sl: ArrayList, - controller: EmulatedController, - groupTypeFilter: Set? + sl: ArrayList, + controller: EmulatedController, + controllerNumber: Int, + groupTypeFilter: Set? ) { - addContainerMappingSettings(sl, controller, controller, groupTypeFilter) + addContainerMappingSettings( + sl, controller, controller, controllerNumber, groupTypeFilter + ) } /** @@ -2730,12 +2762,14 @@ class SettingsFragmentPresenter( * @param sl The list to place controller settings into. * @param controller The encompassing controller. * @param container The container of control groups to add settings for. + * @param controllerNumber The zero-based controller number. * @param groupTypeFilter If this is non-null, only groups whose types match this are considered. */ private fun addContainerMappingSettings( sl: ArrayList, controller: EmulatedController, container: ControlGroupContainer, + controllerNumber: Int, groupTypeFilter: Set? ) { updateOldControllerSettingsWarningVisibility(controller) @@ -2751,10 +2785,7 @@ class SettingsFragmentPresenter( if (group.getDefaultEnabledValue() != ControlGroup.DEFAULT_ENABLED_ALWAYS) { sl.add( SwitchSetting( - context, - ControlGroupEnabledSetting(group), - R.string.enabled, - 0 + context, ControlGroupEnabledSetting(group), R.string.enabled, 0 ) ) } @@ -2768,8 +2799,11 @@ class SettingsFragmentPresenter( val attachmentSetting = group.getAttachmentSetting() sl.add( SingleChoiceSetting( - context, InputMappingIntSetting(attachmentSetting), - R.string.wiimote_extensions, 0, R.array.wiimoteExtensionsEntries, + context, + InputMappingIntSetting(attachmentSetting), + R.string.wiimote_extensions, + 0, + R.array.wiimoteExtensionsEntries, R.array.wiimoteExtensionsValues, MenuTag.getWiimoteExtensionMenuTag(controllerNumber) ) @@ -2813,10 +2847,14 @@ class SettingsFragmentPresenter( } private fun updateOldControllerSettingsWarningVisibility(controller: EmulatedController) { + if (!shouldUpdateWarnings) { + return + } + val defaultDevice = controller.getDefaultDevice() - hasOldControllerSettings = defaultDevice.startsWith("Android/") && - defaultDevice.endsWith("/Touchscreen") + hasOldControllerSettings = + defaultDevice.startsWith("Android/") && defaultDevice.endsWith("/Touchscreen") fragmentView.setOldControllerSettingsWarningVisibility(hasOldControllerSettings) } @@ -2836,10 +2874,7 @@ class SettingsFragmentPresenter( for (logType in NativeLibrary.GetLogTypeNames()) { AdHocBooleanSetting( - Settings.FILE_LOGGER, - Settings.SECTION_LOGGER_LOGS, - logType.first, - false + Settings.FILE_LOGGER, Settings.SECTION_LOGGER_LOGS, logType.first, false ).setBoolean(settings!!, value) } @@ -2851,8 +2886,7 @@ class SettingsFragmentPresenter( fragmentView.fragmentActivity, R.string.wii_converting, 0, - { context.resources.getString(if (f.get()) R.string.wii_convert_success else R.string.wii_convert_failure) } - ) + { context.resources.getString(if (f.get()) R.string.wii_convert_success else R.string.wii_convert_failure) }) } fun installDriver(uri: Uri) { @@ -2884,9 +2918,8 @@ class SettingsFragmentPresenter( GpuDriverHelper.uninstallDriver() withContext(Dispatchers.Main) { with(this@SettingsFragmentPresenter) { - this.gpuDriver = - GpuDriverHelper.getInstalledDriverMetadata() - ?: GpuDriverHelper.getSystemDriverMetadata(context.applicationContext) + this.gpuDriver = GpuDriverHelper.getInstalledDriverMetadata() + ?: GpuDriverHelper.getSystemDriverMetadata(context.applicationContext) this.libNameSetting.setString(this.settings!!, "") } fragmentView.onDriverUninstallDone() @@ -2898,6 +2931,24 @@ class SettingsFragmentPresenter( const val ARG_CONTROLLER_TYPE = "controller_type" const val ARG_SERIALPORT1_TYPE = "serialport1_type" + private data class SearchableSetting( + val name: String, + val description: String, + val menuTag: MenuTag, + val navigationExtras: Bundle?, + val position: Int, + val normalizedName: String, + val normalizedCategory: String, + val normalizedSearchText: String + ) + + private data class SearchableMenu( + val menuTag: MenuTag, + val category: String, + val navigationExtras: Bundle?, + val settings: List + ) + // Value obtained from LogLevel in Common/Logging/Log.h private fun getLogVerbosityEntries(): Int { // GetMaxLogLevel is effectively a constant, but we can't call it before loading diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.kt index c0b39400a7..4530792bee 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.kt @@ -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. diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingViewHolder.kt index dd5d4c97f0..f1ba059864 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingViewHolder.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingViewHolder.kt @@ -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 + } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingsSearchResultViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingsSearchResultViewHolder.kt new file mode 100644 index 0000000000..f9bad5042b --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingsSearchResultViewHolder.kt @@ -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) + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ThemeHelper.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ThemeHelper.kt index 2aa2122ad5..89ff3e0cb0 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ThemeHelper.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ThemeHelper.kt @@ -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), diff --git a/Source/Android/app/src/main/res/anim/anim_settings_search_pop_in.xml b/Source/Android/app/src/main/res/anim/anim_settings_search_pop_in.xml new file mode 100644 index 0000000000..338676f504 --- /dev/null +++ b/Source/Android/app/src/main/res/anim/anim_settings_search_pop_in.xml @@ -0,0 +1,7 @@ + + diff --git a/Source/Android/app/src/main/res/anim/anim_settings_search_pop_out.xml b/Source/Android/app/src/main/res/anim/anim_settings_search_pop_out.xml new file mode 100644 index 0000000000..f131732c71 --- /dev/null +++ b/Source/Android/app/src/main/res/anim/anim_settings_search_pop_out.xml @@ -0,0 +1,6 @@ + + diff --git a/Source/Android/app/src/main/res/color/settings_search_outline.xml b/Source/Android/app/src/main/res/color/settings_search_outline.xml new file mode 100644 index 0000000000..5a4a93e941 --- /dev/null +++ b/Source/Android/app/src/main/res/color/settings_search_outline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/Source/Android/app/src/main/res/drawable/ic_search.xml b/Source/Android/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000000..c85c3c5e61 --- /dev/null +++ b/Source/Android/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,10 @@ + + + + diff --git a/Source/Android/app/src/main/res/layout/activity_settings.xml b/Source/Android/app/src/main/res/layout/activity_settings.xml index 533893df12..fbecfd3886 100644 --- a/Source/Android/app/src/main/res/layout/activity_settings.xml +++ b/Source/Android/app/src/main/res/layout/activity_settings.xml @@ -1,22 +1,50 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - diff --git a/Source/Android/app/src/main/res/layout/fragment_settings.xml b/Source/Android/app/src/main/res/layout/fragment_settings.xml index 77b673b53c..5b8896d9f7 100644 --- a/Source/Android/app/src/main/res/layout/fragment_settings.xml +++ b/Source/Android/app/src/main/res/layout/fragment_settings.xml @@ -1,6 +1,5 @@ - @@ -11,4 +10,16 @@ android:layout_height="match_parent" android:clipToPadding="false" /> + + diff --git a/Source/Android/app/src/main/res/layout/list_item_search_result.xml b/Source/Android/app/src/main/res/layout/list_item_search_result.xml new file mode 100644 index 0000000000..237fc46c04 --- /dev/null +++ b/Source/Android/app/src/main/res/layout/list_item_search_result.xml @@ -0,0 +1,36 @@ + + + + + + + + diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 9d97169d95..33a418b013 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -62,6 +62,10 @@ Settings + Search settings + Back to settings + No settings found for “%1$s” + %1$s › %2$s Game Settings: %1$s Config Graphics Settings From 93706022d5d6974c96b75a129bf278b693b99a6e Mon Sep 17 00:00:00 2001 From: Simonx22 Date: Wed, 22 Jul 2026 14:27:04 -0400 Subject: [PATCH 2/2] Android: Reformat settings search files --- .../features/settings/ui/SettingsActivity.kt | 14 +- .../settings/ui/SettingsActivityPresenter.kt | 36 ++-- .../settings/ui/SettingsActivityView.kt | 5 +- .../features/settings/ui/SettingsAdapter.kt | 197 +++++++++--------- .../features/settings/ui/SettingsFragment.kt | 45 ++-- .../ui/viewholder/SettingViewHolder.kt | 4 +- .../SettingsSearchResultViewHolder.kt | 3 +- .../dolphinemu/utils/ThemeHelper.kt | 83 +++----- 8 files changed, 164 insertions(+), 223 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt index 1135dca463..19c08b9a05 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt @@ -127,15 +127,13 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide } refreshToolbarState() - val focusDelay = - if (areSystemAnimationsEnabled()) SEARCH_FOCUS_DELAY_MS else 0L + 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()) + WindowCompat.getInsetsController(window, searchView).show(WindowInsetsCompat.Type.ime()) }, focusDelay) } @@ -153,18 +151,14 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide private fun refreshToolbarState() { val title = currentToolbarTitle ?: getString(R.string.settings) setToolbarState( - title, - currentToolbarShowsHeadline, - currentToolbarShowsSearch + title, currentToolbarShowsHeadline, currentToolbarShowsSearch ) } private fun setUpBackNavigation() { onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { - if (supportFragmentManager.backStackEntryCount == 0 && - isSettingsSearchActive - ) { + if (supportFragmentManager.backStackEntryCount == 0 && isSettingsSearchActive) { exitSettingsSearch() return } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityPresenter.kt index 1615911fb6..5baee17f02 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityPresenter.kt @@ -10,8 +10,7 @@ import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner import org.dolphinemu.dolphinemu.utils.Log class SettingsActivityPresenter( - private val activityView: SettingsActivityView, - var settings: Settings? + private val activityView: SettingsActivityView, var settings: Settings? ) { private var menuTag: MenuTag? = null private var gameId: String? = null @@ -141,34 +140,25 @@ class SettingsActivityPresenter( private fun getMenuTagAction(menuTag: MenuTag, value: Int): MenuTagAction? { return when { // Not disabled or dummy - menuTag.isSerialPort1Menu && value != 0 && value != 255 -> - MenuTagAction( - menuTag, - Bundle().apply { - putInt(SettingsFragmentPresenter.ARG_SERIALPORT1_TYPE, value) - } - ) + menuTag.isSerialPort1Menu && value != 0 && value != 255 -> MenuTagAction( + menuTag, Bundle().apply { + putInt(SettingsFragmentPresenter.ARG_SERIALPORT1_TYPE, value) + }) // Not disabled - menuTag.isGCPadMenu && value != 0 -> - MenuTagAction( - menuTag, - Bundle().apply { - putInt(SettingsFragmentPresenter.ARG_CONTROLLER_TYPE, value) - } - ) + menuTag.isGCPadMenu && value != 0 -> MenuTagAction( + menuTag, Bundle().apply { + putInt(SettingsFragmentPresenter.ARG_CONTROLLER_TYPE, value) + }) // Emulated Wii Remote menuTag.isWiimoteMenu && value == 1 -> MenuTagAction(menuTag, null) // Not disabled - menuTag.isWiimoteExtensionMenu && value != 0 -> - MenuTagAction( - menuTag, - Bundle().apply { - putInt(SettingsFragmentPresenter.ARG_CONTROLLER_TYPE, value) - } - ) + menuTag.isWiimoteExtensionMenu && value != 0 -> MenuTagAction( + menuTag, Bundle().apply { + putInt(SettingsFragmentPresenter.ARG_CONTROLLER_TYPE, value) + }) else -> null } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityView.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityView.kt index 44f02bc3f8..95951f1ae9 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityView.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityView.kt @@ -27,10 +27,7 @@ interface SettingsActivityView { * @param addToStack Whether or not this fragment should replace a previous one. */ fun showSettingsFragment( - menuTag: MenuTag, - extras: Bundle?, - addToStack: Boolean, - gameId: String + menuTag: MenuTag, extras: Bundle?, addToStack: Boolean, gameId: String ) /** diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt index 2492a0113a..6252350d32 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt @@ -27,14 +27,46 @@ import com.google.android.material.slider.Slider import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.TimeFormat import org.dolphinemu.dolphinemu.R -import org.dolphinemu.dolphinemu.databinding.* +import org.dolphinemu.dolphinemu.databinding.DialogAdvancedMappingBinding +import org.dolphinemu.dolphinemu.databinding.DialogInputStringBinding +import org.dolphinemu.dolphinemu.databinding.DialogSliderBinding +import org.dolphinemu.dolphinemu.databinding.ListItemHeaderBinding +import org.dolphinemu.dolphinemu.databinding.ListItemMappingBinding +import org.dolphinemu.dolphinemu.databinding.ListItemSearchResultBinding +import org.dolphinemu.dolphinemu.databinding.ListItemSettingBinding +import org.dolphinemu.dolphinemu.databinding.ListItemSettingSwitchBinding +import org.dolphinemu.dolphinemu.databinding.ListItemSubmenuBinding import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting import org.dolphinemu.dolphinemu.features.input.ui.AdvancedMappingDialog import org.dolphinemu.dolphinemu.features.input.ui.MotionAlertDialog import org.dolphinemu.dolphinemu.features.input.ui.viewholder.InputMappingControlSettingViewHolder import org.dolphinemu.dolphinemu.features.settings.model.Settings -import org.dolphinemu.dolphinemu.features.settings.model.view.* -import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.* +import org.dolphinemu.dolphinemu.features.settings.model.view.DateTimeChoiceSetting +import org.dolphinemu.dolphinemu.features.settings.model.view.DirectoryPicker +import org.dolphinemu.dolphinemu.features.settings.model.view.FilePicker +import org.dolphinemu.dolphinemu.features.settings.model.view.FloatSliderSetting +import org.dolphinemu.dolphinemu.features.settings.model.view.InputStringSetting +import org.dolphinemu.dolphinemu.features.settings.model.view.IntSliderSetting +import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem +import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsSearchResult +import org.dolphinemu.dolphinemu.features.settings.model.view.SingleChoiceSetting +import org.dolphinemu.dolphinemu.features.settings.model.view.SingleChoiceSettingDynamicDescriptions +import org.dolphinemu.dolphinemu.features.settings.model.view.SliderSetting +import org.dolphinemu.dolphinemu.features.settings.model.view.StringSingleChoiceSetting +import org.dolphinemu.dolphinemu.features.settings.model.view.SubmenuSetting +import org.dolphinemu.dolphinemu.features.settings.model.view.SwitchSetting +import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.DateTimeSettingViewHolder +import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.FilePickerViewHolder +import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.HeaderHyperLinkViewHolder +import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.HeaderViewHolder +import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.InputStringSettingViewHolder +import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.RunRunnableViewHolder +import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHolder +import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingsSearchResultViewHolder +import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SingleChoiceViewHolder +import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SliderViewHolder +import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SubmenuViewHolder +import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SwitchSettingViewHolder import org.dolphinemu.dolphinemu.utils.DirectoryInitialization import org.dolphinemu.dolphinemu.utils.FileBrowserHelper import org.dolphinemu.dolphinemu.utils.Log @@ -42,14 +74,13 @@ import org.dolphinemu.dolphinemu.utils.PermissionsHandler import java.io.File import java.io.IOException import java.io.RandomAccessFile -import java.util.* +import java.util.Calendar +import java.util.TimeZone import kotlin.math.roundToInt class SettingsAdapter( - private val fragmentView: SettingsFragmentView, - private val context: Context -) : - RecyclerView.Adapter(), DialogInterface.OnClickListener, + private val fragmentView: SettingsFragmentView, private val context: Context +) : RecyclerView.Adapter(), DialogInterface.OnClickListener, Slider.OnChangeListener { private var settingsList: ArrayList? = null private var clickedItem: SettingsItem? = null @@ -68,53 +99,53 @@ class SettingsAdapter( val inflater = LayoutInflater.from(parent.context) return when (viewType) { SettingsItem.TYPE_HEADER -> HeaderViewHolder( - ListItemHeaderBinding.inflate(inflater, parent, false), - this + ListItemHeaderBinding.inflate(inflater, parent, false), this ) + SettingsItem.TYPE_SWITCH -> SwitchSettingViewHolder( - ListItemSettingSwitchBinding.inflate(inflater, parent, false), - this + ListItemSettingSwitchBinding.inflate(inflater, parent, false), this ) - SettingsItem.TYPE_STRING_SINGLE_CHOICE, - SettingsItem.TYPE_SINGLE_CHOICE_DYNAMIC_DESCRIPTIONS, - SettingsItem.TYPE_SINGLE_CHOICE -> SingleChoiceViewHolder( - ListItemSettingBinding.inflate(inflater, parent, false), - this + + SettingsItem.TYPE_STRING_SINGLE_CHOICE, SettingsItem.TYPE_SINGLE_CHOICE_DYNAMIC_DESCRIPTIONS, SettingsItem.TYPE_SINGLE_CHOICE -> SingleChoiceViewHolder( + ListItemSettingBinding.inflate(inflater, parent, false), this ) + SettingsItem.TYPE_SLIDER -> SliderViewHolder( - ListItemSettingBinding.inflate(inflater, parent, false), - this, - context + ListItemSettingBinding.inflate(inflater, parent, false), this, context ) + SettingsItem.TYPE_SUBMENU -> SubmenuViewHolder( - ListItemSubmenuBinding.inflate(inflater, parent, false), - this + ListItemSubmenuBinding.inflate(inflater, parent, false), this ) + SettingsItem.TYPE_INPUT_MAPPING_CONTROL -> InputMappingControlSettingViewHolder( - ListItemMappingBinding.inflate(inflater, parent, false), - this + ListItemMappingBinding.inflate(inflater, parent, false), this ) - SettingsItem.TYPE_FILE_PICKER, - SettingsItem.TYPE_DIRECTORY_PICKER -> FilePickerViewHolder( - ListItemSettingBinding.inflate(inflater, parent, false), - this + + SettingsItem.TYPE_FILE_PICKER, SettingsItem.TYPE_DIRECTORY_PICKER -> FilePickerViewHolder( + ListItemSettingBinding.inflate(inflater, parent, false), this ) + SettingsItem.TYPE_RUN_RUNNABLE -> RunRunnableViewHolder( - ListItemSettingBinding.inflate(inflater, parent, false), - this, context + ListItemSettingBinding.inflate(inflater, parent, false), this, context ) + SettingsItem.TYPE_STRING -> InputStringSettingViewHolder( ListItemSettingBinding.inflate(inflater, parent, false), this ) + SettingsItem.TYPE_HYPERLINK_HEADER -> HeaderHyperLinkViewHolder( ListItemHeaderBinding.inflate(inflater, parent, false), this ) + SettingsItem.TYPE_DATETIME_CHOICE -> DateTimeSettingViewHolder( ListItemSettingBinding.inflate(inflater, parent, false), this ) + SettingsItem.TYPE_SEARCH_RESULT -> SettingsSearchResultViewHolder( ListItemSearchResultBinding.inflate(inflater, parent, false), this ) + else -> throw IllegalArgumentException("Invalid view type: $viewType") } } @@ -165,8 +196,7 @@ class SettingsAdapter( val binding = DialogInputStringBinding.inflate(inflater) val input = binding.input input.setText(item.selectedValue) - dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity) - .setView(binding.root) + dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setView(binding.root) .setMessage(item.description) .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> val editTextInput = input.text.toString() @@ -175,19 +205,15 @@ class SettingsAdapter( fragmentView.onSettingChanged(item) } item.setSelectedValue(fragmentView.settings!!, editTextInput) - } - .setNegativeButton(R.string.cancel, null) - .show() + }.setNegativeButton(R.string.cancel, null).show() } fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) { clickedItem = item clickedPosition = position val value = getSelectionForSingleChoiceValue(item) - dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity) - .setTitle(item.name) - .setSingleChoiceItems(item.choicesId, value, this) - .show() + dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name) + .setSingleChoiceItems(item.choicesId, value, this).show() } fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) { @@ -197,35 +223,26 @@ class SettingsAdapter( val choices = item.choices val noChoicesAvailableString = item.noChoicesAvailableString dialog = if (noChoicesAvailableString != 0 && choices.isEmpty()) { - MaterialAlertDialogBuilder(fragmentView.fragmentActivity) - .setTitle(item.name) - .setMessage(noChoicesAvailableString) - .setPositiveButton(R.string.ok, null) - .show() + MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name) + .setMessage(noChoicesAvailableString).setPositiveButton(R.string.ok, null).show() } else { - MaterialAlertDialogBuilder(fragmentView.fragmentActivity) - .setTitle(item.name) + MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name) .setSingleChoiceItems( - item.choices, item.selectedValueIndex, - this - ) - .show() + item.choices, item.selectedValueIndex, this + ).show() } } fun onSingleChoiceDynamicDescriptionsClick( - item: SingleChoiceSettingDynamicDescriptions, - position: Int + item: SingleChoiceSettingDynamicDescriptions, position: Int ) { clickedItem = item clickedPosition = position val value = getSelectionForSingleChoiceDynamicDescriptionsValue(item) - dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity) - .setTitle(item.name) - .setSingleChoiceItems(item.choicesId, value, this) - .show() + dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name) + .setSingleChoiceItems(item.choicesId, value, this).show() } fun onSliderClick(item: SliderSetting, position: Int) { @@ -255,6 +272,7 @@ class SettingsAdapter( slider.valueTo = item.max slider.stepSize = item.stepSize } + is IntSliderSetting -> { slider.valueFrom = item.min.toFloat() slider.valueTo = item.max.toFloat() @@ -264,11 +282,8 @@ class SettingsAdapter( slider.value = (seekbarProgress / slider.stepSize).roundToInt() * slider.stepSize slider.addOnChangeListener(this) - dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity) - .setTitle(item.name) - .setView(binding.root) - .setPositiveButton(R.string.ok, this) - .show() + dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name) + .setView(binding.root).setPositiveButton(R.string.ok, this).show() } fun onSubmenuClick(item: SubmenuSetting) { @@ -281,16 +296,13 @@ class SettingsAdapter( fun onInputMappingClick(item: InputMappingControlSetting, position: Int) { if (item.controller.getDefaultDevice().isEmpty() && !fragmentView.isMappingAllDevices) { - MaterialAlertDialogBuilder(fragmentView.fragmentActivity) - .setMessage(R.string.input_binding_no_device) - .setPositiveButton(R.string.ok, this) - .show() + MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setMessage(R.string.input_binding_no_device) + .setPositiveButton(R.string.ok, this).show() return } val dialog = MotionAlertDialog( - fragmentView.fragmentActivity, item, - fragmentView.isMappingAllDevices + fragmentView.fragmentActivity, item, fragmentView.isMappingAllDevices ) val background = ContextCompat.getDrawable(context, R.drawable.dialog_round) @@ -304,14 +316,12 @@ class SettingsAdapter( dialog.setTitle(R.string.input_binding) dialog.setMessage( String.format( - context.getString(R.string.input_binding_description), - item.name + context.getString(R.string.input_binding_description), item.name ) ) dialog.setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(R.string.cancel), this) dialog.setButton( - AlertDialog.BUTTON_NEUTRAL, - context.getString(R.string.clear) + AlertDialog.BUTTON_NEUTRAL, context.getString(R.string.clear) ) { _: DialogInterface?, _: Int -> item.clearValue() } dialog.setOnDismissListener { notifyItemChanged(position) @@ -325,10 +335,7 @@ class SettingsAdapter( val inflater = LayoutInflater.from(context) val binding = DialogAdvancedMappingBinding.inflate(inflater) val dialog = AdvancedMappingDialog( - context, - binding, - item.controlReference, - item.controller + context, binding, item.controlReference, item.controller ) val background = ContextCompat.getDrawable(context, R.drawable.dialog_round) @@ -350,8 +357,7 @@ class SettingsAdapter( } dialog.setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(R.string.cancel), this) dialog.setButton( - AlertDialog.BUTTON_NEUTRAL, - context.getString(R.string.clear) + AlertDialog.BUTTON_NEUTRAL, context.getString(R.string.clear) ) { _: DialogInterface?, _: Int -> } dialog.setCanceledOnTouchOutside(false) dialog.show() @@ -369,14 +375,12 @@ class SettingsAdapter( val directoryPicker = item as DirectoryPicker if (!PermissionsHandler.isExternalStorageLegacy()) { - MaterialAlertDialogBuilder(context) - .setMessage(R.string.path_not_changeable_scoped_storage) + MaterialAlertDialogBuilder(context).setMessage(R.string.path_not_changeable_scoped_storage) .setPositiveButton(R.string.ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() } .show() } else { val intent = FileBrowserHelper.createDirectoryPickerIntent( - fragmentView.fragmentActivity, - FileBrowserHelper.GAME_EXTENSIONS + fragmentView.fragmentActivity, FileBrowserHelper.GAME_EXTENSIONS ) directoryPicker.launcher.launch(intent) } @@ -408,32 +412,24 @@ class SettingsAdapter( calendar.timeZone = TimeZone.getTimeZone("UTC") // Start and end epoch times available for the Wii's date picker - val calendarConstraints = CalendarConstraints.Builder() - .setStart(946684800000L) - .setEnd(2082672000000L) - .build() + val calendarConstraints = + CalendarConstraints.Builder().setStart(946684800000L).setEnd(2082672000000L).build() var timeFormat = TimeFormat.CLOCK_12H if (DateFormat.is24HourFormat(fragmentView.fragmentActivity)) { timeFormat = TimeFormat.CLOCK_24H } - val datePicker = MaterialDatePicker.Builder.datePicker() - .setSelection(storedTime) - .setTitleText(R.string.select_rtc_date) - .setCalendarConstraints(calendarConstraints) - .build() - val timePicker = MaterialTimePicker.Builder() - .setTimeFormat(timeFormat) - .setHour(calendar[Calendar.HOUR_OF_DAY]) - .setMinute(calendar[Calendar.MINUTE]) - .setTitleText(R.string.select_rtc_time) + val datePicker = MaterialDatePicker.Builder.datePicker().setSelection(storedTime) + .setTitleText(R.string.select_rtc_date).setCalendarConstraints(calendarConstraints) .build() + val timePicker = MaterialTimePicker.Builder().setTimeFormat(timeFormat) + .setHour(calendar[Calendar.HOUR_OF_DAY]).setMinute(calendar[Calendar.MINUTE]) + .setTitleText(R.string.select_rtc_time).build() datePicker.addOnPositiveButtonClickListener { timePicker.show( - fragmentView.fragmentActivity.supportFragmentManager, - "TimePicker" + fragmentView.fragmentActivity.supportFragmentManager, "TimePicker" ) } timePicker.addOnPositiveButtonClickListener { @@ -484,6 +480,7 @@ class SettingsAdapter( closeDialog() } + is SingleChoiceSettingDynamicDescriptions -> { val scSetting = clickedItem as SingleChoiceSettingDynamicDescriptions @@ -494,6 +491,7 @@ class SettingsAdapter( closeDialog() } + is StringSingleChoiceSetting -> { val scSetting = clickedItem as StringSingleChoiceSetting @@ -504,6 +502,7 @@ class SettingsAdapter( closeDialog() } + is IntSliderSetting -> { val sliderSetting = clickedItem as IntSliderSetting if (sliderSetting.selectedValue != seekbarProgress.toInt()) { @@ -512,6 +511,7 @@ class SettingsAdapter( sliderSetting.setSelectedValue(settings!!, seekbarProgress.toInt()) closeDialog() } + is FloatSliderSetting -> { val sliderSetting = clickedItem as FloatSliderSetting @@ -598,8 +598,7 @@ class SettingsAdapter( } private fun getValueForSingleChoiceDynamicDescriptionsSelection( - item: SingleChoiceSettingDynamicDescriptions, - which: Int + item: SingleChoiceSettingDynamicDescriptions, which: Int ): Int { val valuesId = item.valuesId return if (valuesId > 0) { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt index 7e6f646478..e0f96e77f1 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt @@ -39,8 +39,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHolder import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable -import java.util.* -import kotlin.collections.ArrayList +import java.util.EnumMap class SettingsFragment : Fragment(), SettingsFragmentView { private lateinit var presenter: SettingsFragmentPresenter @@ -93,9 +92,7 @@ class SettingsFragment : Fragment(), SettingsFragmentView { } override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = FragmentSettingsBinding.inflate(inflater, container, false) return binding!!.root @@ -161,15 +158,15 @@ class SettingsFragment : Fragment(), SettingsFragmentView { } val position = arguments?.getInt( - ARGUMENT_SCROLL_TO_SETTING_POSITION, - RecyclerView.NO_POSITION + 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) + (recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset( + position, 0 + ) highlightSearchResult(position, settingsList[position]) } } @@ -231,19 +228,13 @@ class SettingsFragment : Fragment(), SettingsFragmentView { } activityView!!.showSettingsFragment( - menuKey, - null, - true, - requireArguments().getString(ARGUMENT_GAME_ID)!! + menuKey, null, true, requireArguments().getString(ARGUMENT_GAME_ID)!! ) } override fun loadSearchResult(menuKey: MenuTag, settingPosition: Int, extras: Bundle?) { activityView!!.showSearchResult( - menuKey, - settingPosition, - requireArguments().getString(ARGUMENT_GAME_ID)!!, - extras + menuKey, settingPosition, requireArguments().getString(ARGUMENT_GAME_ID)!!, extras ) } @@ -331,17 +322,13 @@ class SettingsFragment : Fragment(), SettingsFragmentView { } val msg = "${presenter.gpuDriver!!.name} ${presenter.gpuDriver!!.driverVersion}" - MaterialAlertDialogBuilder(requireContext()) - .setTitle(getString(R.string.gpu_driver_dialog_title)) - .setMessage(msg) - .setNegativeButton(android.R.string.cancel, null) + MaterialAlertDialogBuilder(requireContext()).setTitle(getString(R.string.gpu_driver_dialog_title)) + .setMessage(msg).setNegativeButton(android.R.string.cancel, null) .setNeutralButton(R.string.gpu_driver_dialog_system) { _: DialogInterface?, _: Int -> presenter.useSystemDriver() - } - .setPositiveButton(R.string.gpu_driver_dialog_install) { _: DialogInterface?, _: Int -> + }.setPositiveButton(R.string.gpu_driver_dialog_install) { _: DialogInterface?, _: Int -> askForDriverFile() - } - .show() + }.show() } override fun getFragmentLifecycle(): Lifecycle { @@ -358,16 +345,12 @@ class SettingsFragment : Fragment(), SettingsFragmentView { override fun onDriverInstallDone(result: GpuDriverInstallResult) { val view = binding?.root ?: return - Snackbar - .make(view, resolveInstallResultString(result), Snackbar.LENGTH_LONG) - .show() + Snackbar.make(view, resolveInstallResultString(result), Snackbar.LENGTH_LONG).show() } override fun onDriverUninstallDone() { Toast.makeText( - requireContext(), - R.string.gpu_driver_dialog_uninstall_done, - Toast.LENGTH_SHORT + requireContext(), R.string.gpu_driver_dialog_uninstall_done, Toast.LENGTH_SHORT ).show() } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingViewHolder.kt index f1ba059864..77414fedce 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingViewHolder.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingViewHolder.kt @@ -4,11 +4,11 @@ package org.dolphinemu.dolphinemu.features.settings.ui.viewholder import android.animation.ValueAnimator import android.content.DialogInterface +import android.graphics.Paint +import android.graphics.Typeface import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable -import android.graphics.Paint -import android.graphics.Typeface import android.view.View import android.view.View.OnLongClickListener import android.view.animation.DecelerateInterpolator diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingsSearchResultViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingsSearchResultViewHolder.kt index f9bad5042b..9471d6bf43 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingsSearchResultViewHolder.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingsSearchResultViewHolder.kt @@ -9,8 +9,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsSearchResu import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter class SettingsSearchResultViewHolder( - private val binding: ListItemSearchResultBinding, - adapter: SettingsAdapter + private val binding: ListItemSearchResultBinding, adapter: SettingsAdapter ) : SettingViewHolder(binding.root, adapter) { private lateinit var result: SettingsSearchResult diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ThemeHelper.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ThemeHelper.kt index 89ff3e0cb0..8b41fc6c55 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ThemeHelper.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ThemeHelper.kt @@ -1,20 +1,20 @@ package org.dolphinemu.dolphinemu.utils -import androidx.appcompat.app.AppCompatActivity -import org.dolphinemu.dolphinemu.R -import android.os.Build -import androidx.core.content.ContextCompat -import androidx.appcompat.app.AppCompatDelegate -import androidx.core.view.WindowInsetsControllerCompat -import androidx.core.view.WindowCompat -import org.dolphinemu.dolphinemu.ui.main.ThemeProvider import android.content.res.Configuration -import com.google.android.material.appbar.MaterialToolbar -import com.google.android.material.appbar.AppBarLayout -import com.google.android.material.elevation.ElevationOverlayProvider -import com.google.android.material.color.MaterialColors +import android.os.Build import androidx.annotation.ColorInt +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.ContextCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsControllerCompat import androidx.preference.PreferenceManager +import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.color.MaterialColors +import com.google.android.material.elevation.ElevationOverlayProvider +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.ui.main.ThemeProvider object ThemeHelper { @@ -52,8 +52,7 @@ object ThemeHelper { .getInt(CURRENT_THEME_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) activity.delegate.localNightMode = themeMode val windowController = WindowCompat.getInsetsController( - activity.window, - activity.window.decorView + activity.window, activity.window.decorView ) val systemReportedThemeMode = activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK @@ -62,6 +61,7 @@ object ThemeHelper { Configuration.UI_MODE_NIGHT_NO -> setLightModeSystemBars(windowController) Configuration.UI_MODE_NIGHT_YES -> setDarkModeSystemBars(windowController) } + AppCompatDelegate.MODE_NIGHT_NO -> setLightModeSystemBars(windowController) AppCompatDelegate.MODE_NIGHT_YES -> setDarkModeSystemBars(windowController) } @@ -83,66 +83,50 @@ object ThemeHelper { @JvmStatic fun saveTheme(activity: AppCompatActivity, themeValue: Int) { - PreferenceManager.getDefaultSharedPreferences(activity.applicationContext) - .edit() - .putInt(CURRENT_THEME, themeValue) - .apply() + PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit() + .putInt(CURRENT_THEME, themeValue).apply() activity.recreate() } @JvmStatic fun deleteThemeKey(activity: AppCompatActivity) { - PreferenceManager.getDefaultSharedPreferences(activity.applicationContext) - .edit() - .remove(CURRENT_THEME) - .apply() + PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit() + .remove(CURRENT_THEME).apply() activity.recreate() } @JvmStatic fun saveThemeMode(activity: AppCompatActivity, themeModeValue: Int) { - PreferenceManager.getDefaultSharedPreferences(activity.applicationContext) - .edit() - .putInt(CURRENT_THEME_MODE, themeModeValue) - .apply() + PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit() + .putInt(CURRENT_THEME_MODE, themeModeValue).apply() setThemeMode(activity) } @JvmStatic fun deleteThemeModeKey(activity: AppCompatActivity) { - PreferenceManager.getDefaultSharedPreferences(activity.applicationContext) - .edit() - .remove(CURRENT_THEME_MODE) - .apply() + PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit() + .remove(CURRENT_THEME_MODE).apply() setThemeMode(activity) } @JvmStatic fun saveBackgroundSetting(activity: AppCompatActivity, backgroundValue: Boolean) { - PreferenceManager.getDefaultSharedPreferences(activity.applicationContext) - .edit() - .putBoolean(USE_BLACK_BACKGROUNDS, backgroundValue) - .apply() + PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit() + .putBoolean(USE_BLACK_BACKGROUNDS, backgroundValue).apply() activity.recreate() } @JvmStatic fun deleteBackgroundSetting(activity: AppCompatActivity) { - PreferenceManager.getDefaultSharedPreferences(activity.applicationContext) - .edit() - .remove(USE_BLACK_BACKGROUNDS) - .apply() + PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit() + .remove(USE_BLACK_BACKGROUNDS).apply() activity.recreate() } @JvmStatic fun resetThemePreferences(activity: AppCompatActivity, applyImmediately: Boolean = false) { - PreferenceManager.getDefaultSharedPreferences(activity.applicationContext) - .edit() - .remove(CURRENT_THEME) - .remove(CURRENT_THEME_MODE) - .remove(USE_BLACK_BACKGROUNDS) - .apply() + PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit() + .remove(CURRENT_THEME).remove(CURRENT_THEME_MODE).remove(USE_BLACK_BACKGROUNDS).apply() activity.delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM activity.delegate.applyDayNight() if (applyImmediately) { @@ -170,10 +154,7 @@ object ThemeHelper { activity: AppCompatActivity, toolbar: MaterialToolbar, appBarLayout: AppBarLayout ) { appBarLayout.addOnOffsetChangedListener { layout: AppBarLayout, verticalOffset: Int -> - if ( - layout.totalScrollRange > 0 && - -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), @@ -183,8 +164,7 @@ object ThemeHelper { setStatusBarColor(activity, color) } else { @ColorInt val statusBarColor = ContextCompat.getColor( - activity.applicationContext, - android.R.color.transparent + activity.applicationContext, android.R.color.transparent ) @ColorInt val appBarColor = MaterialColors.getColor(toolbar, R.attr.colorSurface) toolbar.setBackgroundColor(appBarColor) @@ -201,8 +181,7 @@ object ThemeHelper { setStatusBarColor(activity, color) } else { @ColorInt val statusBarColor = ContextCompat.getColor( - activity.applicationContext, - android.R.color.transparent + activity.applicationContext, android.R.color.transparent ) setStatusBarColor(activity, statusBarColor) }