Skip to content

Commit

Permalink
fix: highlight header in tablets
Browse files Browse the repository at this point in the history
the previous approach only changes the highlight when tapping a HeaderPreference

It doesn't listen to backstack changes done by other things, like the back button and the search bar.
  • Loading branch information
BrayanDSO committed Nov 27, 2024
1 parent c723f4b commit 4261190
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ package com.ichi2.anki.preferences
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.bytehamster.lib.preferencesearch.SearchConfiguration
Expand All @@ -34,19 +36,14 @@ import com.ichi2.preferences.HeaderPreference
import com.ichi2.utils.AdaptionUtil

class HeaderFragment : PreferenceFragmentCompat(), TitleProvider {
private var selectedHeaderPreference: HeaderPreference? = null
private var selectedHeaderPreferenceKey: String = DEFAULT_SELECTED_HEADER

override val title: CharSequence
get() = getString(R.string.settings)

private var highlightedPreferenceKey: String = ""

override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preference_headers, rootKey)

selectedHeaderPreferenceKey = savedInstanceState?.getString(KEY_SELECTED_HEADER_PREF) ?: DEFAULT_SELECTED_HEADER

highlightHeaderPreference(requirePreference<HeaderPreference>(selectedHeaderPreferenceKey))

requirePreference<HeaderPreference>(R.string.pref_backup_limits_screen_key)
.title = CollectionManager.TR.preferencesBackups()

Expand All @@ -63,33 +60,28 @@ class HeaderFragment : PreferenceFragmentCompat(), TitleProvider {
requireActivity() as AppCompatActivity,
requirePreference<SearchPreference>(R.string.search_preference_key).searchConfiguration
)
}

private fun highlightHeaderPreference(headerPreference: HeaderPreference) {
if (resources.isWindowCompact()) {
return
}
selectedHeaderPreference?.setHighlighted(false)
// highlight the newly selected header
selectedHeaderPreference = headerPreference.apply {
setHighlighted(true)
selectedHeaderPreferenceKey = this.key
}
}
if (!resources.isWindowCompact()) {
parentFragmentManager.findFragmentById(R.id.settings_container)?.let {
val key = getHeaderKeyForFragment(it) ?: return@let
highlightPreference(key)
}

override fun onPreferenceTreeClick(preference: Preference): Boolean {
highlightHeaderPreference(preference as HeaderPreference)
return super.onPreferenceTreeClick(preference)
}
parentFragmentManager.addOnBackStackChangedListener {
val fragment = parentFragmentManager.findFragmentById(R.id.settings_container)
?: return@addOnBackStackChangedListener

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(KEY_SELECTED_HEADER_PREF, selectedHeaderPreferenceKey)
val key = getHeaderKeyForFragment(fragment) ?: return@addOnBackStackChangedListener
highlightPreference(key)
}
}
}

override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
highlightHeaderPreference(requirePreference<HeaderPreference>(selectedHeaderPreferenceKey))
private fun highlightPreference(@StringRes keyRes: Int) {
val key = getString(keyRes)
findPreference<HeaderPreference>(highlightedPreferenceKey)?.setHighlighted(false)
findPreference<HeaderPreference>(key)?.setHighlighted(true)
highlightedPreferenceKey = key
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Expand All @@ -101,9 +93,6 @@ class HeaderFragment : PreferenceFragmentCompat(), TitleProvider {
}

companion object {
private const val KEY_SELECTED_HEADER_PREF = "selected_header_pref"
private const val DEFAULT_SELECTED_HEADER = "generalScreen"

fun configureSearchBar(activity: AppCompatActivity, searchConfiguration: SearchConfiguration) {
val setDuePreferenceTitle = TR.actionsSetDueDate().toSentenceCase(activity, R.string.sentence_set_due_date)
with(searchConfiguration) {
Expand Down Expand Up @@ -163,5 +152,29 @@ class HeaderFragment : PreferenceFragmentCompat(), TitleProvider {

searchConfiguration.ignorePreference(activity.getString(R.string.user_actions_controls_category_key))
}

/**
* @return the key for the [HeaderPreference] that corresponds to the given [fragment]
* in the Preference tree.
*
* e.g. Sync > Custom sync server settings -> returns the key for the Sync header
*/
@StringRes
fun getHeaderKeyForFragment(fragment: Fragment): Int? {
return when (fragment) {
is GeneralSettingsFragment -> R.string.pref_general_screen_key
is ReviewingSettingsFragment -> R.string.pref_reviewing_screen_key
is SyncSettingsFragment, is CustomSyncServerSettingsFragment -> R.string.pref_sync_screen_key
is NotificationsSettingsFragment -> R.string.pref_notifications_screen_key
is AppearanceSettingsFragment, is CustomButtonsSettingsFragment -> R.string.pref_appearance_screen_key
is ControlsSettingsFragment -> R.string.pref_controls_screen_key
is AccessibilitySettingsFragment -> R.string.pref_accessibility_screen_key
is BackupLimitsSettingsFragment -> R.string.pref_backup_limits_screen_key
is AdvancedSettingsFragment -> R.string.pref_advanced_screen_key
is DevOptionsFragment, is ReviewerOptionsFragment -> R.string.pref_dev_options_screen_key
is AboutFragment -> R.string.about_screen_key
else -> null
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ package com.ichi2.preferences

import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.ThemeUtils
import androidx.core.content.withStyledAttributes
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import com.google.android.material.color.MaterialColors
import com.ichi2.anki.LanguageUtils
import com.ichi2.anki.R

Expand All @@ -35,8 +35,8 @@ constructor(
defStyleAttr: Int = androidx.preference.R.attr.preferenceStyle,
defStyleRes: Int = androidx.preference.R.style.Preference
) : Preference(context, attrs, defStyleAttr, defStyleRes) {

private var isHighlighted = false
private val highlightColor: Int = MaterialColors.getColor(context, R.attr.currentDeckBackgroundColor, 0)

init {
context.withStyledAttributes(attrs, R.styleable.HeaderPreference) {
Expand All @@ -50,7 +50,8 @@ constructor(
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
if (isHighlighted) {
holder.itemView.setBackgroundColor(highlightColor)
val color = ThemeUtils.getThemeAttrColor(context, R.attr.currentDeckBackgroundColor)
holder.itemView.setBackgroundColor(color)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import com.ichi2.utils.getInstanceFromClassName
import org.xmlpull.v1.XmlPullParser

object PreferenceTestUtils {
private fun getAttrFromXml(context: Context, @XmlRes xml: Int, attrName: String, namespace: String = AnkiDroidApp.ANDROID_NAMESPACE): List<String> {
fun getAttrFromXml(context: Context, @XmlRes xml: Int, attrName: String, namespace: String = AnkiDroidApp.ANDROID_NAMESPACE): List<String> {
val occurrences = mutableListOf<String>()

val xrp = context.resources.getXml(xml).apply {
Expand All @@ -44,6 +44,29 @@ object PreferenceTestUtils {
return occurrences.toList()
}

fun getAttrsFromXml(
context: Context,
@XmlRes xml: Int,
attrNames: List<String>,
namespace: String = AnkiDroidApp.ANDROID_NAMESPACE
): List<Map<String, String>> {
val occurrences = mutableListOf<Map<String, String>>()

val xrp = context.resources.getXml(xml).apply {
setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true)
setFeature(XmlPullParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES, true)
}

while (xrp.eventType != XmlPullParser.END_DOCUMENT) {
if (xrp.eventType == XmlPullParser.START_TAG) {
val attrValues = attrNames.associateWith { xrp.getAttributeValue(namespace, it) }
occurrences.add(attrValues)
}
xrp.next()
}
return occurrences.toList()
}

/** @return fragments found on [xml] */
private fun getFragmentsFromXml(context: Context, @XmlRes xml: Int): List<Fragment> {
return getAttrFromXml(context, xml, "fragment").map { getInstanceFromClassName(it) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,27 @@ package com.ichi2.anki.preferences

import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.commitNow
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.ichi2.anki.R
import com.ichi2.anki.RobolectricTest
import com.ichi2.anki.preferences.HeaderFragment.Companion.getHeaderKeyForFragment
import com.ichi2.anki.preferences.PreferenceTestUtils.getAttrFromXml
import com.ichi2.libanki.exception.ConfirmModSchemaException
import com.ichi2.preferences.HeaderPreference
import com.ichi2.testutils.getJavaMethodAsAccessible
import com.ichi2.utils.getInstanceFromClassName
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import kotlin.reflect.jvm.jvmName
import kotlin.test.assertEquals
import kotlin.test.assertTrue

@RunWith(AndroidJUnit4::class)
class PreferencesTest : RobolectricTest() {
Expand Down Expand Up @@ -98,6 +104,36 @@ class PreferencesTest : RobolectricTest() {
}
}

@Test
fun `All preferences fragments highlight the correct header`() {
val headers = PreferenceTestUtils.getAttrsFromXml(
targetContext,
R.xml.preference_headers,
listOf("key", "fragment")
).filter { it["fragment"] != null }

assertTrue(headers.all { it["key"] != null })

fun assertThatFragmentLeadsToHeaderKey(fragmentClass: String, parentFragmentClass: String? = null) {
val fragment = getInstanceFromClassName<Fragment>(fragmentClass)
val headerFragmentClass = parentFragmentClass ?: fragmentClass
val expectedKey = headers.first { it["fragment"] == headerFragmentClass }["key"]!!.removePrefix("@").toInt()
val key = getHeaderKeyForFragment(fragment)
assertEquals(expectedKey, key)

if (fragment is SettingsFragment) {
val subFragments = getAttrFromXml(targetContext, fragment.preferenceResource, "fragment")
for (subFragment in subFragments) {
assertThatFragmentLeadsToHeaderKey(subFragment, headerFragmentClass)
}
}
}

for (header in headers) {
assertThatFragmentLeadsToHeaderKey(header["fragment"]!!)
}
}

@Test
@Config(qualifiers = "ar")
fun buildHeaderSummary_RTL_Test() {
Expand Down

0 comments on commit 4261190

Please sign in to comment.