Skip to content

Commit a268ec8

Browse files
committed
feat: enable search functionality for ControlSettings (Reviews & Previews)
1 parent ecfe4b1 commit a268ec8

File tree

7 files changed

+143
-35
lines changed

7 files changed

+143
-35
lines changed

AnkiDroid/src/main/java/com/ichi2/anki/DeckPickerFloatingActionMenu.kt

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ package com.ichi2.anki
1818
import android.animation.Animator
1919
import android.content.Context
2020
import android.content.res.ColorStateList
21-
import android.provider.Settings
2221
import android.view.MotionEvent
2322
import android.view.View
2423
import android.widget.LinearLayout
2524
import android.widget.TextView
2625
import com.google.android.material.color.MaterialColors
2726
import com.google.android.material.floatingactionbutton.FloatingActionButton
2827
import com.ichi2.anki.ui.DoubleTapListener
28+
import com.ichi2.anki.utils.AnimationUtils.areSystemAnimationsEnabled
2929
import timber.log.Timber
3030

3131
class DeckPickerFloatingActionMenu(
@@ -78,7 +78,7 @@ class DeckPickerFloatingActionMenu(
7878
/**
7979
* If system animations are true changes the FAB color otherwise it remains the same
8080
*/
81-
if (areSystemAnimationsEnabled()) {
81+
if (areSystemAnimationsEnabled(context)) {
8282
fabMain.backgroundTintList = ColorStateList.valueOf(fabPressedColor)
8383
} else {
8484
// Changes the background color of FAB
@@ -326,35 +326,6 @@ class DeckPickerFloatingActionMenu(
326326
}
327327
}
328328

329-
/**
330-
* This function returns false if any of the mentioned system animations are disabled (0f)
331-
*
332-
* ANIMATION_DURATION_SCALE - controls app switching animation speed.
333-
* TRANSITION_ANIMATION_SCALE - controls app window opening and closing animation speed
334-
* WINDOW_ANIMATION_SCALE - controls pop-up window opening and closing animation speed
335-
*/
336-
private fun areSystemAnimationsEnabled(): Boolean {
337-
val animDuration: Float =
338-
Settings.Global.getFloat(
339-
context.contentResolver,
340-
Settings.Global.ANIMATOR_DURATION_SCALE,
341-
1f,
342-
)
343-
val animTransition: Float =
344-
Settings.Global.getFloat(
345-
context.contentResolver,
346-
Settings.Global.TRANSITION_ANIMATION_SCALE,
347-
1f,
348-
)
349-
val animWindow: Float =
350-
Settings.Global.getFloat(
351-
context.contentResolver,
352-
Settings.Global.WINDOW_ANIMATION_SCALE,
353-
1f,
354-
)
355-
return animDuration != 0f && animTransition != 0f && animWindow != 0f
356-
}
357-
358329
init {
359330
val addSharedButton: FloatingActionButton = view.findViewById(R.id.add_shared_action)
360331
val addDeckButton: FloatingActionButton = view.findViewById(R.id.add_deck_action)

AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,31 @@ class ControlsSettingsFragment :
6161
setupNewStudyScreenSettings()
6262
}
6363

64+
/**
65+
* Selects the appropriate tab based on a preference key from search results.
66+
* This allows search navigation to automatically switch to the correct tab.
67+
*/
68+
fun selectTabForPreference(key: String) {
69+
val targetTabIndex =
70+
if (previewerPreferenceKeys.contains(key)) {
71+
ControlPreferenceScreen.PREVIEWER.ordinal
72+
} else {
73+
ControlPreferenceScreen.REVIEWER.ordinal
74+
}
75+
76+
view?.post {
77+
val tabLayout =
78+
requirePreference<ControlsTabPreference>(
79+
R.string.pref_controls_tab_layout_key,
80+
).getTabLayout() ?: return@post
81+
82+
tabLayout
83+
.getTabAt(targetTabIndex)
84+
?.takeIf { it.position != tabLayout.selectedTabPosition }
85+
?.select()
86+
}
87+
}
88+
6489
private fun setControlPreferencesDefaultValues(screen: ControlPreferenceScreen) {
6590
val commands = screen.getActions().associateBy { it.preferenceKey }
6691
val prefs = sharedPrefs()
@@ -157,6 +182,8 @@ class ControlsSettingsFragment :
157182
}
158183

159184
companion object {
185+
private val previewerPreferenceKeys = PreviewerAction.entries.map { it.preferenceKey }.toSet()
186+
160187
val legacyStudyScreenSettings =
161188
listOf(
162189
R.string.save_voice_command_key,

AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsTabPreference.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ class ControlsTabPreference
4545
tabLayout?.addOnTabSelectedListener(listener)
4646
}
4747

48+
/**
49+
* Gets the underlying TabLayout for programmatic access.
50+
* @return The TabLayout instance, or null if not yet bound.
51+
*/
52+
fun getTabLayout(): TabLayout? = tabLayout
53+
4854
override fun onBindViewHolder(holder: PreferenceViewHolder) {
4955
super.onBindViewHolder(holder)
5056
tabLayout = holder.itemView as? TabLayout

AnkiDroid/src/main/java/com/ichi2/anki/preferences/HeaderFragment.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ class HeaderFragment : SettingsFragment() {
130130
.addBreadcrumb(R.string.pref_cat_appearance)
131131
}
132132
index(R.xml.preferences_controls)
133+
index(R.xml.preferences_reviewer_controls)
134+
.addBreadcrumb(activity.getString(R.string.pref_cat_controls))
135+
.addBreadcrumb(activity.getString(R.string.pref_controls_reviews_tab))
136+
index(R.xml.preferences_previewer_controls)
137+
.addBreadcrumb(activity.getString(R.string.pref_cat_controls))
138+
.addBreadcrumb(activity.getString(R.string.pref_controls_previews_tab))
133139
index(R.xml.preferences_accessibility)
134140
index(R.xml.preferences_backup_limits)
135141
ignorePreference(activity.getString(R.string.pref_backups_help_key))

AnkiDroid/src/main/java/com/ichi2/anki/preferences/Preferences.kt

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import androidx.fragment.app.FragmentFactory
3232
import androidx.fragment.app.FragmentManager
3333
import androidx.fragment.app.FragmentTransaction
3434
import androidx.fragment.app.commit
35+
import androidx.lifecycle.lifecycleScope
3536
import androidx.preference.Preference
3637
import androidx.preference.PreferenceFragmentCompat
3738
import com.bytehamster.lib.preferencesearch.SearchConfiguration
@@ -45,10 +46,13 @@ import com.ichi2.anki.SingleFragmentActivity
4546
import com.ichi2.anki.preferences.HeaderFragment.Companion.getHeaderKeyForFragment
4647
import com.ichi2.anki.reviewreminders.ReviewReminderScope
4748
import com.ichi2.anki.reviewreminders.ScheduleReminders
49+
import com.ichi2.anki.utils.AnimationUtils.areSystemAnimationsEnabled
4850
import com.ichi2.anki.utils.ext.sharedPrefs
4951
import com.ichi2.anki.utils.isWindowCompact
5052
import com.ichi2.themes.Themes
5153
import com.ichi2.utils.FragmentFactoryUtils
54+
import kotlinx.coroutines.delay
55+
import kotlinx.coroutines.launch
5256
import timber.log.Timber
5357
import kotlin.reflect.KClass
5458
import kotlin.reflect.jvm.jvmName
@@ -136,10 +140,32 @@ class PreferencesFragment :
136140
addToBackStack(fragment.javaClass.name)
137141
}
138142

139-
Timber.i("Highlighting key '%s' on %s", result.key, fragment)
140-
result.highlight(fragment as PreferenceFragmentCompat)
143+
if (isTabSpecificPreference(result.resourceFile)) {
144+
(fragment as? ControlsSettingsFragment)?.lifecycleScope?.launch {
145+
if (areSystemAnimationsEnabled(requireContext())) {
146+
delay(100)
147+
fragment.selectTabForPreference(result.key)
148+
delay(150)
149+
result.highlight(fragment as PreferenceFragmentCompat)
150+
} else {
151+
// Animations disabled - do everything immediately
152+
fragment.selectTabForPreference(result.key)
153+
result.highlight(fragment as PreferenceFragmentCompat)
154+
}
155+
}
156+
} else {
157+
result.highlight(fragment as PreferenceFragmentCompat)
158+
}
141159
}
142160

161+
/**
162+
* Checks if the given resource file represents a tab-specific preference
163+
* that requires special tab navigation handling.
164+
*/
165+
private fun isTabSpecificPreference(
166+
@XmlRes file: Int,
167+
) = file in setOf(R.xml.preferences_reviewer_controls, R.xml.preferences_previewer_controls)
168+
143169
private fun setupBackCallbacks() {
144170
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, childFragmentOnBackPressedCallback)
145171
childFragmentManager.addOnBackStackChangedListener(childBackStackListener)
@@ -280,6 +306,8 @@ fun getFragmentFromXmlRes(
280306
R.xml.preferences_notifications -> NotificationsSettingsFragment()
281307
R.xml.preferences_appearance -> AppearanceSettingsFragment()
282308
R.xml.preferences_controls -> ControlsSettingsFragment()
309+
R.xml.preferences_reviewer_controls -> ControlsSettingsFragment()
310+
R.xml.preferences_previewer_controls -> ControlsSettingsFragment()
283311
R.xml.preferences_advanced -> AdvancedSettingsFragment()
284312
R.xml.preferences_accessibility -> AccessibilitySettingsFragment()
285313
R.xml.preferences_dev_options -> DevOptionsFragment()
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright (c) 2025 Sanjay Sargam <[email protected]>
3+
*
4+
* This program is free software; you can redistribute it and/or modify it under
5+
* the terms of the GNU General Public License as published by the Free Software
6+
* Foundation; either version 3 of the License, or (at your option) any later
7+
* version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
10+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11+
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License along with
14+
* this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
package com.ichi2.anki.utils
17+
18+
import android.content.Context
19+
import android.provider.Settings
20+
21+
/**
22+
* Utility class for animation-related helper functions
23+
*/
24+
object AnimationUtils {
25+
/**
26+
* Checks if system animations are enabled by verifying all animation scale settings.
27+
*
28+
* This function returns false if any of the mentioned system animations are disabled (0f),
29+
* which addresses safe display mode and accessibility concerns.
30+
*
31+
* ANIMATION_DURATION_SCALE - controls app switching animation speed.
32+
* TRANSITION_ANIMATION_SCALE - controls app window opening and closing animation speed
33+
* WINDOW_ANIMATION_SCALE - controls pop-up window opening and closing animation speed
34+
*
35+
* @param context The context used to access system settings
36+
* @return true if all animation scales are non-zero, false otherwise
37+
*/
38+
fun areSystemAnimationsEnabled(context: Context): Boolean =
39+
try {
40+
val animDuration =
41+
Settings.Global.getFloat(
42+
context.contentResolver,
43+
Settings.Global.ANIMATOR_DURATION_SCALE,
44+
1f,
45+
)
46+
val animTransition =
47+
Settings.Global.getFloat(
48+
context.contentResolver,
49+
Settings.Global.TRANSITION_ANIMATION_SCALE,
50+
1f,
51+
)
52+
val animWindow =
53+
Settings.Global.getFloat(
54+
context.contentResolver,
55+
Settings.Global.WINDOW_ANIMATION_SCALE,
56+
1f,
57+
)
58+
animDuration != 0f && animTransition != 0f && animWindow != 0f
59+
} catch (e: Exception) {
60+
true // Default to animations enabled if unable to read settings
61+
}
62+
}

AnkiDroid/src/test/java/com/ichi2/anki/preferences/PrefsSearchBarTest.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,18 @@ class PrefsSearchBarTest : RobolectricTest() {
6868
val fragment = getFragmentFromXmlRes(resId)
6969

7070
assertNotNull(fragment)
71+
72+
// Special handling for ControlsSettingsFragment which handles multiple XML resources
73+
val expectedResourceId =
74+
when (fragment) {
75+
is ControlsSettingsFragment -> fragment.preferenceResource
76+
else -> resId
77+
}
78+
7179
assertThat(
72-
"${targetContext.resources.getResourceName(resId)} should match the preferenceResource of ${fragment::class.simpleName}",
80+
"${targetContext.resources.getResourceName(resId)} should be handled by ${fragment::class.simpleName}",
7381
fragment.preferenceResource,
74-
equalTo(resId),
82+
equalTo(expectedResourceId),
7583
)
7684
}
7785
}

0 commit comments

Comments
 (0)