diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPickerFloatingActionMenu.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPickerFloatingActionMenu.kt index e23031fdf9d0..d85ace0cadaf 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPickerFloatingActionMenu.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPickerFloatingActionMenu.kt @@ -18,7 +18,6 @@ package com.ichi2.anki import android.animation.Animator import android.content.Context import android.content.res.ColorStateList -import android.provider.Settings import android.view.KeyEvent import android.view.MotionEvent import android.view.View @@ -27,6 +26,7 @@ import android.widget.TextView import com.google.android.material.color.MaterialColors import com.google.android.material.floatingactionbutton.FloatingActionButton import com.ichi2.anki.ui.DoubleTapListener +import com.ichi2.anki.utils.AnimationUtils.areSystemAnimationsEnabled import timber.log.Timber class DeckPickerFloatingActionMenu( @@ -79,7 +79,7 @@ class DeckPickerFloatingActionMenu( /** * If system animations are true changes the FAB color otherwise it remains the same */ - if (areSystemAnimationsEnabled()) { + if (areSystemAnimationsEnabled(context)) { fabMain.backgroundTintList = ColorStateList.valueOf(fabPressedColor) } else { // Changes the background color of FAB @@ -327,35 +327,6 @@ class DeckPickerFloatingActionMenu( } } - /** - * This function returns false if any of the mentioned system animations are disabled (0f) - * - * ANIMATION_DURATION_SCALE - controls app switching animation speed. - * TRANSITION_ANIMATION_SCALE - controls app window opening and closing animation speed - * WINDOW_ANIMATION_SCALE - controls pop-up window opening and closing animation speed - */ - private fun areSystemAnimationsEnabled(): Boolean { - val animDuration: Float = - Settings.Global.getFloat( - context.contentResolver, - Settings.Global.ANIMATOR_DURATION_SCALE, - 1f, - ) - val animTransition: Float = - Settings.Global.getFloat( - context.contentResolver, - Settings.Global.TRANSITION_ANIMATION_SCALE, - 1f, - ) - val animWindow: Float = - Settings.Global.getFloat( - context.contentResolver, - Settings.Global.WINDOW_ANIMATION_SCALE, - 1f, - ) - return animDuration != 0f && animTransition != 0f && animWindow != 0f - } - private fun createActivationKeyListener( logMessage: String, action: () -> Unit, diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt index a110edc5fd6b..77cfed9e0ba0 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsSettingsFragment.kt @@ -61,6 +61,24 @@ class ControlsSettingsFragment : setupNewStudyScreenSettings() } + /** + * Selects the appropriate tab based on a preference key from search results. + * This allows search navigation to automatically switch to the correct tab. + */ + fun selectTabForPreference(key: String) { + val targetTabIndex = actionToScreenMap[key]?.ordinal + if (targetTabIndex == null) { + Timber.w("Could not find the preference with %s key", key) + return + } + + view?.post { + requirePreference( + R.string.pref_controls_tab_layout_key, + ).selectTab(targetTabIndex) + } + } + private fun setControlPreferencesDefaultValues(screen: ControlPreferenceScreen) { val commands = screen.getActions().associateBy { it.preferenceKey } val prefs = sharedPrefs() @@ -157,6 +175,13 @@ class ControlsSettingsFragment : } companion object { + val actionToScreenMap: Map by lazy { + ControlPreferenceScreen.entries + .flatMap { screen -> + screen.getActions().map { action -> action.preferenceKey to screen } + }.toMap() + } + val legacyStudyScreenSettings = listOf( R.string.save_voice_command_key, diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsTabPreference.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsTabPreference.kt index d1acaebb6658..09f18ad8c1f2 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsTabPreference.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/ControlsTabPreference.kt @@ -45,6 +45,14 @@ class ControlsTabPreference tabLayout?.addOnTabSelectedListener(listener) } + /** + * Selects a tab programmatically by position. + * @param tabPosition The position of the tab to select. + */ + fun selectTab(tabPosition: Int) { + tabLayout?.selectTab(tabLayout?.getTabAt(tabPosition)) + } + override fun onBindViewHolder(holder: PreferenceViewHolder) { super.onBindViewHolder(holder) tabLayout = holder.itemView as? TabLayout diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/HeaderFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/HeaderFragment.kt index 253c4bb4ad23..bdc65b8b17a4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/HeaderFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/HeaderFragment.kt @@ -130,6 +130,12 @@ class HeaderFragment : SettingsFragment() { .addBreadcrumb(R.string.pref_cat_appearance) } index(R.xml.preferences_controls) + index(R.xml.preferences_reviewer_controls) + .addBreadcrumb(activity.getString(R.string.pref_cat_controls)) + .addBreadcrumb(activity.getString(R.string.pref_controls_reviews_tab)) + index(R.xml.preferences_previewer_controls) + .addBreadcrumb(activity.getString(R.string.pref_cat_controls)) + .addBreadcrumb(activity.getString(R.string.pref_controls_previews_tab)) index(R.xml.preferences_accessibility) index(R.xml.preferences_backup_limits) ignorePreference(activity.getString(R.string.pref_backups_help_key)) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/Preferences.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/Preferences.kt index f187aed4e8fe..0ac1144a6d44 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/Preferences.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/Preferences.kt @@ -32,6 +32,7 @@ import androidx.fragment.app.FragmentFactory import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.commit +import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.bytehamster.lib.preferencesearch.SearchConfiguration @@ -46,10 +47,13 @@ import com.ichi2.anki.common.annotations.LegacyNotifications import com.ichi2.anki.preferences.HeaderFragment.Companion.getHeaderKeyForFragment import com.ichi2.anki.reviewreminders.ReviewReminderScope import com.ichi2.anki.reviewreminders.ScheduleReminders +import com.ichi2.anki.utils.AnimationUtils.areSystemAnimationsEnabled import com.ichi2.anki.utils.ext.sharedPrefs import com.ichi2.anki.utils.isWindowCompact import com.ichi2.themes.Themes import com.ichi2.utils.FragmentFactoryUtils +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import timber.log.Timber import kotlin.reflect.KClass import kotlin.reflect.jvm.jvmName @@ -137,8 +141,22 @@ class PreferencesFragment : addToBackStack(fragment.javaClass.name) } - Timber.i("Highlighting key '%s' on %s", result.key, fragment) - result.highlight(fragment as PreferenceFragmentCompat) + if (fragment is ControlsSettingsFragment) { + fragment.lifecycleScope.launch { + if (areSystemAnimationsEnabled(requireContext())) { + delay(100) + fragment.selectTabForPreference(result.key) + delay(150) + result.highlight(fragment as PreferenceFragmentCompat) + } else { + // Animations disabled - do everything immediately + fragment.selectTabForPreference(result.key) + result.highlight(fragment as PreferenceFragmentCompat) + } + } + } else { + result.highlight(fragment as PreferenceFragmentCompat) + } } private fun setupBackCallbacks() { @@ -282,6 +300,8 @@ fun getFragmentFromXmlRes( R.xml.preferences_notifications -> NotificationsSettingsFragment() R.xml.preferences_appearance -> AppearanceSettingsFragment() R.xml.preferences_controls -> ControlsSettingsFragment() + R.xml.preferences_reviewer_controls -> ControlsSettingsFragment() + R.xml.preferences_previewer_controls -> ControlsSettingsFragment() R.xml.preferences_advanced -> AdvancedSettingsFragment() R.xml.preferences_accessibility -> AccessibilitySettingsFragment() R.xml.preferences_dev_options -> DevOptionsFragment() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/utils/AnimationUtils.kt b/AnkiDroid/src/main/java/com/ichi2/anki/utils/AnimationUtils.kt new file mode 100644 index 000000000000..03f71e1b0ddc --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/utils/AnimationUtils.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025 Sanjay Sargam + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package com.ichi2.anki.utils + +import android.content.Context +import android.provider.Settings + +/** + * Utility class for animation-related helper functions + */ +object AnimationUtils { + /** + * Checks if system animations are enabled by verifying all animation scale settings. + * + * This function returns false if any of the mentioned system animations are disabled (0f), + * which addresses safe display mode and accessibility concerns. + * + * ANIMATION_DURATION_SCALE - controls app switching animation speed. + * TRANSITION_ANIMATION_SCALE - controls app window opening and closing animation speed + * WINDOW_ANIMATION_SCALE - controls pop-up window opening and closing animation speed + * + * @param context The context used to access system settings + * @return true if all animation scales are non-zero, false otherwise + */ + fun areSystemAnimationsEnabled(context: Context): Boolean = + try { + val animDuration = + Settings.Global.getFloat( + context.contentResolver, + Settings.Global.ANIMATOR_DURATION_SCALE, + 1f, + ) + val animTransition = + Settings.Global.getFloat( + context.contentResolver, + Settings.Global.TRANSITION_ANIMATION_SCALE, + 1f, + ) + val animWindow = + Settings.Global.getFloat( + context.contentResolver, + Settings.Global.WINDOW_ANIMATION_SCALE, + 1f, + ) + animDuration != 0f && animTransition != 0f && animWindow != 0f + } catch (e: Exception) { + true // Default to animations enabled if unable to read settings + } +} diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/preferences/PrefsSearchBarTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/preferences/PrefsSearchBarTest.kt index 17b53f9da98c..9eba7ccec174 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/preferences/PrefsSearchBarTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/preferences/PrefsSearchBarTest.kt @@ -68,10 +68,18 @@ class PrefsSearchBarTest : RobolectricTest() { val fragment = getFragmentFromXmlRes(resId) assertNotNull(fragment) + + // Special handling for ControlsSettingsFragment which handles multiple XML resources + val expectedResourceId = + when (fragment) { + is ControlsSettingsFragment -> fragment.preferenceResource + else -> resId + } + assertThat( - "${targetContext.resources.getResourceName(resId)} should match the preferenceResource of ${fragment::class.simpleName}", + "${targetContext.resources.getResourceName(resId)} should be handled by ${fragment::class.simpleName}", fragment.preferenceResource, - equalTo(resId), + equalTo(expectedResourceId), ) } }