From fd5c7b21ddc0e40df049773d9106e349072352f3 Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Thu, 24 Aug 2023 16:11:08 -0400 Subject: [PATCH] android: Add search for settings --- .../features/settings/ui/SettingsAdapter.kt | 3 +- .../features/settings/ui/SettingsFragment.kt | 34 ++++ .../fragments/SettingsSearchFragment.kt | 189 ++++++++++++++++++ .../yuzu/yuzu_emu/model/SettingsViewModel.kt | 7 + .../res/layout/fragment_settings_search.xml | 120 +++++++++++ .../app/src/main/res/menu/menu_settings.xml | 11 + .../res/navigation/settings_navigation.xml | 8 + .../app/src/main/res/values/strings.xml | 1 + 8 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt create mode 100644 src/android/app/src/main/res/layout/fragment_settings_search.xml create mode 100644 src/android/app/src/main/res/menu/menu_settings.xml diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt index ff1e64e0a..f5eba1222 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt @@ -12,6 +12,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.navigation.findNavController import androidx.recyclerview.widget.AsyncDifferConfig @@ -37,7 +38,7 @@ import org.yuzu.yuzu_emu.features.settings.ui.viewholder.* import org.yuzu.yuzu_emu.model.SettingsViewModel class SettingsAdapter( - private val fragment: SettingsFragment, + private val fragment: Fragment, private val context: Context ) : ListAdapter(AsyncDifferConfig.Builder(DiffCallback()).build()), DialogInterface.OnClickListener { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt index 5890b0642..0ea587a88 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt @@ -13,12 +13,14 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.navigation.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.divider.MaterialDividerItemDecoration import com.google.android.material.transition.MaterialSharedAxis import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding +import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.model.SettingsViewModel class SettingsFragment : Fragment() { @@ -84,11 +86,43 @@ class SettingsFragment : Fragment() { } } + settingsViewModel.isUsingSearch.observe(viewLifecycleOwner) { + if (it) { + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) + exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) + } else { + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) + } + } + + if (args.menuTag == SettingsFile.FILE_NAME_CONFIG) { + binding.toolbarSettings.inflateMenu(R.menu.menu_settings) + binding.toolbarSettings.setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_search -> { + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) + exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) + view.findNavController() + .navigate(R.id.action_settingsFragment_to_settingsSearchFragment) + true + } + + else -> false + } + } + } + presenter.onViewCreated() setInsets() } + override fun onResume() { + super.onResume() + settingsViewModel.setIsUsingSearch(false) + } + override fun onDetach() { super.onDetach() settingsAdapter?.closeDialog() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt new file mode 100644 index 000000000..4f93db4ad --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt @@ -0,0 +1,189 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.divider.MaterialDividerItemDecoration +import com.google.android.material.transition.MaterialSharedAxis +import info.debatty.java.stringsimilarity.Cosine +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.databinding.FragmentSettingsSearchBinding +import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem +import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter +import org.yuzu.yuzu_emu.model.SettingsViewModel +import org.yuzu.yuzu_emu.utils.NativeConfig + +class SettingsSearchFragment : Fragment() { + private var _binding: FragmentSettingsSearchBinding? = null + private val binding get() = _binding!! + + private var settingsAdapter: SettingsAdapter? = null + + private val settingsViewModel: SettingsViewModel by activityViewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) + returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentSettingsSearchBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + settingsViewModel.setIsUsingSearch(true) + + if (savedInstanceState != null) { + binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT)) + } + + settingsAdapter = SettingsAdapter(this, requireContext()) + + val dividerDecoration = MaterialDividerItemDecoration( + requireContext(), + LinearLayoutManager.VERTICAL + ) + dividerDecoration.isLastItemDecorated = false + binding.settingsList.apply { + adapter = settingsAdapter + layoutManager = LinearLayoutManager(requireContext()) + addItemDecoration(dividerDecoration) + } + + focusSearch() + + binding.backButton.setOnClickListener { settingsViewModel.setShouldNavigateBack(true) } + binding.searchBackground.setOnClickListener { focusSearch() } + binding.clearButton.setOnClickListener { binding.searchText.setText("") } + binding.searchText.doOnTextChanged { _, _, _, _ -> + search() + binding.settingsList.smoothScrollToPosition(0) + } + settingsViewModel.shouldReloadSettingsList.observe(viewLifecycleOwner) { + if (it) { + settingsViewModel.setShouldReloadSettingsList(false) + search() + } + } + + search() + + setInsets() + } + + override fun onDetach() { + super.onDetach() + settingsAdapter?.closeDialog() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putString(SEARCH_TEXT, binding.searchText.text.toString()) + } + + private fun search() { + val searchTerm = binding.searchText.text.toString().lowercase() + binding.clearButton.visibility = + if (searchTerm.isEmpty()) View.INVISIBLE else View.VISIBLE + if (searchTerm.isEmpty()) { + binding.noResultsView.visibility = View.VISIBLE + settingsAdapter?.submitList(emptyList()) + return + } + + val baseList = SettingsItem.settingsItems + val similarityAlgorithm = if (searchTerm.length > 2) Cosine() else Cosine(1) + val sortedList: List = baseList.mapNotNull { item -> + val title = getString(item.value.nameId).lowercase() + val similarity = similarityAlgorithm.similarity(searchTerm, title) + if (similarity > 0.08) { + Pair(similarity, item) + } else { + null + } + }.sortedByDescending { it.first }.mapNotNull { + val item = it.second.value + val pairedSettingKey = item.setting.pairedSettingKey + val optionalSetting: SettingsItem? = if (pairedSettingKey.isNotEmpty()) { + val pairedSettingValue = NativeConfig.getBoolean(pairedSettingKey, false) + if (pairedSettingValue) it.second.value else null + } else { + it.second.value + } + optionalSetting + } + settingsAdapter?.submitList(sortedList) + binding.noResultsView.visibility = + if (sortedList.isEmpty()) View.VISIBLE else View.INVISIBLE + } + + private fun focusSearch() { + binding.searchText.requestFocus() + val imm = requireActivity() + .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? + imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT) + } + + private fun setInsets() = + ViewCompat.setOnApplyWindowInsetsListener( + binding.root + ) { _: View, windowInsets: WindowInsetsCompat -> + val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med) + val sideMargin = resources.getDimensionPixelSize(R.dimen.spacing_medlarge) + val topMargin = resources.getDimensionPixelSize(R.dimen.spacing_chip) + + val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) + + val leftInsets = barInsets.left + cutoutInsets.left + val rightInsets = barInsets.right + cutoutInsets.right + + binding.settingsList.updatePadding(bottom = barInsets.bottom + extraListSpacing) + binding.frameSearch.updatePadding( + left = leftInsets + sideMargin, + top = barInsets.top + topMargin, + right = rightInsets + sideMargin + ) + binding.noResultsView.updatePadding( + left = leftInsets, + right = rightInsets, + bottom = barInsets.bottom + ) + + val mlpSettingsList = binding.settingsList.layoutParams as ViewGroup.MarginLayoutParams + mlpSettingsList.leftMargin = leftInsets + sideMargin + mlpSettingsList.rightMargin = rightInsets + sideMargin + binding.settingsList.layoutParams = mlpSettingsList + + val mlpDivider = binding.divider.layoutParams as ViewGroup.MarginLayoutParams + mlpDivider.leftMargin = leftInsets + sideMargin + mlpDivider.rightMargin = rightInsets + sideMargin + binding.divider.layoutParams = mlpDivider + + windowInsets + } + + companion object { + const val SEARCH_TEXT = "SearchText" + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt index 6f2276293..a0cb7225f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt @@ -27,6 +27,9 @@ class SettingsViewModel : ViewModel() { private val _shouldReloadSettingsList = MutableLiveData(false) val shouldReloadSettingsList: LiveData get() = _shouldReloadSettingsList + private val _isUsingSearch = MutableLiveData(false) + val isUsingSearch: LiveData get() = _isUsingSearch + fun setToolbarTitle(value: String) { _toolbarTitle.value = value } @@ -47,6 +50,10 @@ class SettingsViewModel : ViewModel() { _shouldReloadSettingsList.value = value } + fun setIsUsingSearch(value: Boolean) { + _isUsingSearch.value = value + } + fun clear() { game = null shouldSave = false diff --git a/src/android/app/src/main/res/layout/fragment_settings_search.xml b/src/android/app/src/main/res/layout/fragment_settings_search.xml new file mode 100644 index 000000000..c779ed2fc --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_settings_search.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + +