Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .idea/dictionaries/project.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>nosuggest</w>
</words>
</dictionary>
</component>
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.text.InputType
import android.view.GestureDetector
import android.view.GestureDetector.SimpleOnGestureListener
import android.view.KeyEvent
Expand Down Expand Up @@ -71,6 +72,7 @@ import androidx.annotation.IdRes
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import androidx.core.content.getSystemService
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.core.view.WindowInsetsCompat
Expand Down Expand Up @@ -132,6 +134,7 @@ import com.ichi2.anki.libanki.SoundOrVideoTag
import com.ichi2.anki.libanki.TTSTag
import com.ichi2.anki.libanki.TtsPlayer
import com.ichi2.anki.model.CardStateFilter
import com.ichi2.anki.model.FieldFilters.NoSuggestFilter
import com.ichi2.anki.multimedia.getAvTag
import com.ichi2.anki.noteeditor.NoteEditorLauncher
import com.ichi2.anki.observability.ChangeManager
Expand Down Expand Up @@ -246,6 +249,10 @@ abstract class AbstractFlashcardViewer :
private var cardFrame: FrameLayout? = null
private var touchLayer: FrameLayout? = null
protected var answerField: FixedEditText? = null

/** Layout-provided default `inputType` for [answerField], captured once and used to restore
* state when moving off a card that used `{{nosuggest:type:}}`. See issue #10352. */
private var defaultAnswerFieldInputType: Int? = null
protected var flipCardLayout: FrameLayout? = null
private var easeButtonsLayout: LinearLayout? = null

Expand Down Expand Up @@ -570,6 +577,7 @@ abstract class AbstractFlashcardViewer :
shortAnimDuration = resources.getInteger(android.R.integer.config_shortAnimTime)
gestureDetectorImpl = LinkDetectingGestureDetector()
TtsVoicesFieldFilter.ensureApplied()
NoSuggestFilter.ensureApplied()
}

override fun setupBackPressedCallbacks() {
Expand Down Expand Up @@ -738,6 +746,20 @@ abstract class AbstractFlashcardViewer :

protected open fun answerFieldIsFocused(): Boolean = answerField != null && answerField!!.isFocused

/**
* Apply or restore the `{{nosuggest:type:}}` flag set on [answerField].
*/
private fun applyTypeAnswerSuggestionFlags(noSuggest: Boolean) {
val field = answerField ?: return
// see ReviewerFragment for why `TYPE_NULL` was selected
val targetInputType =
if (noSuggest) InputType.TYPE_NULL else (defaultAnswerFieldInputType ?: field.inputType)
if (field.inputType != targetInputType) {
field.inputType = targetInputType
getSystemService<InputMethodManager>()?.restartInput(field)
}
}

val deckOptionsLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ ->
Timber.i("Returned from deck options -> Restarting activity")
Expand Down Expand Up @@ -979,7 +1001,10 @@ abstract class AbstractFlashcardViewer :
val params = flipCardLayout!!.layoutParams
params.height = initialFlipCardHeight * 2
}
answerField = findViewById(R.id.answer_field)
answerField =
findViewById<FixedEditText>(R.id.answer_field).also { answerField ->
defaultAnswerFieldInputType = answerField.inputType
}
initControls()

// Position answer buttons
Expand Down Expand Up @@ -1341,6 +1366,7 @@ abstract class AbstractFlashcardViewer :
// Show text entry based on if the user wants to write the answer
answerField?.visibility = View.VISIBLE
answerField?.applyLanguageHint(typeAnswer?.languageHint)
applyTypeAnswerSuggestionFlags(typeAnswer?.noSuggest == true)
} else {
answerField?.visibility = View.GONE
}
Expand Down Expand Up @@ -1985,6 +2011,7 @@ abstract class AbstractFlashcardViewer :
// Show text entry based on if the user wants to write the answer
answerField?.visibility = View.VISIBLE
answerField?.applyLanguageHint(typeAnswer?.languageHint)
applyTypeAnswerSuggestionFlags(typeAnswer?.noSuggest == true)
} else {
answerField?.visibility = View.GONE
}
Expand Down
13 changes: 13 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import com.ichi2.anki.logging.FragmentLifecycleLogger
import com.ichi2.anki.logging.LogType
import com.ichi2.anki.logging.ProductionCrashReportingTree
import com.ichi2.anki.logging.RobolectricDebugTree
import com.ichi2.anki.model.FieldFilters.NoSuggestFilter
import com.ichi2.anki.navigation.initializeNavigator
import com.ichi2.anki.observability.ChangeManager
import com.ichi2.anki.preferences.SharedPreferencesProvider
Expand Down Expand Up @@ -215,8 +216,20 @@ open class AnkiDroidApp :
setupLifecycleLogging()
activityAgnosticDialogs = ActivityAgnosticDialogs.register(this)
TtsVoices.launchBuildLocalesJob()
applyCustomFieldFilters()
}

/**
* Applies AnkiDroid-specific implementations of field filters
*
* @see com.ichi2.anki.model.FieldFilter
* @see com.ichi2.anki.model.FieldFilters
*/
private fun applyCustomFieldFilters() {
// enable {{tts-voices:}} field filter
TtsVoicesFieldFilter.ensureApplied()
// enable {{nosuggest:type:}} field filter (issue #10352)
NoSuggestFilter.ensureApplied()
}

/**
Expand Down
4 changes: 3 additions & 1 deletion AnkiDroid/src/main/java/com/ichi2/anki/TtsVoices.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ package com.ichi2.anki

import android.speech.tts.TextToSpeech
import android.speech.tts.Voice
import com.ichi2.anki.TtsVoices.availableLocaleData
import com.ichi2.anki.TtsVoices.availableLocales
import com.ichi2.anki.common.android.appContext
import com.ichi2.anki.common.coroutines.applicationScope
import com.ichi2.anki.i18n.normalize
Expand Down Expand Up @@ -220,7 +222,7 @@ object TtsVoices {
/**
* `{{tts-voices:}}` A filter which lists all available TTS Voices for the current engine
*/
class TtsVoicesFieldFilter : TemplateManager.FieldFilter() {
class TtsVoicesFieldFilter : TemplateManager.FieldFilter {
// modified from libAnki: tts.py: on_tts_voices
override fun apply(
fieldText: String,
Expand Down
44 changes: 17 additions & 27 deletions AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/TypeAnswer.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,4 @@
/*
* Copyright (c) 2021 David Allison <davidallisongithub@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0-or-later

package com.ichi2.anki.cardviewer

Expand All @@ -31,6 +17,8 @@ import java.util.regex.Matcher
import java.util.regex.Pattern

/**
* Handles 'Type the answer' for the old study screen
*
* @param useInputTag use an `<input>` tag to allow for HTML styling
* @param autoFocus Whether the user wants to focus "type in answer"
*/
Expand All @@ -45,6 +33,14 @@ class TypeAnswer(
var combining: Boolean = true
private set

/**
* Whether keyboard suggestions, swiping and autocorrect should be disabled (#10352).
*
* @see com.ichi2.anki.model.FieldFilters.NoSuggestFilter
*/
var noSuggest: Boolean = false
private set

/** What the learner actually typed (externally mutable) */
var input = ""

Expand Down Expand Up @@ -86,24 +82,18 @@ class TypeAnswer(
res: Resources,
) {
combining = true
noSuggest = false
correct = null
val q = card.question(col)
val m = PATTERN.matcher(q)
var clozeIdx = 0
if (!m.find()) {
return
}
var fldTag = m.group(1)!!
// if it's a cloze, extract data
if (fldTag.startsWith("cloze:")) {
// get field and cloze position
clozeIdx = card.ord + 1
fldTag = fldTag.split(":").toTypedArray()[1]
}
if (fldTag.startsWith("nc:")) {
combining = false
fldTag = fldTag.split(":").toTypedArray()[1]
}
val parsed = TypeAnswerModifiers.parse(m.group(1)!!)
combining = parsed.combining
noSuggest = parsed.noSuggest
val fldTag = parsed.fieldName
val clozeIdx = if (parsed.cloze) card.ord + 1 else 0
// loop through fields for a match
for (fld in card.noteType(col).fields) {
val name = fld.name
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package com.ichi2.anki.cardviewer

/**
* Represents modifiers for `[[type:...]]`
*
* Examples:
* - `[[type:nosuggest:nc:Field]]`
* - `[[type:cloze:Field]]`
*/
internal data class TypeAnswerModifiers(
val fieldName: String,
val combining: Boolean,
val cloze: Boolean,
val noSuggest: Boolean,
) {
companion object {
/**
* Represents the known modifiers in a [[type: ]] declaration, and remaining data
*/
fun parse(rawField: String): TypeAnswerModifiers {
var remaining = rawField
var combining = true
var cloze = false
var noSuggest = false
while (true) {
when {
remaining.startsWith("nosuggest:") -> {
noSuggest = true
remaining = remaining.removePrefix("nosuggest:")
}
remaining.startsWith("cloze:") -> {
cloze = true
remaining = remaining.removePrefix("cloze:")
}
remaining.startsWith("nc:") -> {
combining = false
remaining = remaining.removePrefix("nc:")
}
else -> return TypeAnswerModifiers(
fieldName = remaining,
combining = combining,
cloze = cloze,
noSuggest = noSuggest,
)
}
}
}
}
}
70 changes: 52 additions & 18 deletions AnkiDroid/src/main/java/com/ichi2/anki/model/FieldFilters.kt
Original file line number Diff line number Diff line change
@@ -1,23 +1,10 @@
/*
* Copyright (c) 2026 David Allison <davidallisongithub@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0-or-later

package com.ichi2.anki.model

import androidx.annotation.CheckResult
import com.ichi2.anki.libanki.NotetypeJson
import com.ichi2.anki.libanki.TemplateManager

// Classes relating to a chain of field filters: {{type:cloze:Front}}

Expand All @@ -34,7 +21,7 @@ import com.ichi2.anki.libanki.NotetypeJson
* only be used to validate inserting new filters in the AnkiDroid editor.
*
* @see FieldFilter
* @see com.ichi2.anki.libanki.TemplateManager.FieldFilter
* @see TemplateManager.FieldFilter
*/
data class FilterContext(
/**
Expand Down Expand Up @@ -69,6 +56,10 @@ data class FilterContext(
* Applying more filters to these outputs would corrupt them.
*/
val isTerminal: Boolean = false,
/**
* Whether the chain produces a `[[type:...]]` marker (via the `type:` field filter)
*/
val producesTypeMarker: Boolean = false,
)

/**
Expand Down Expand Up @@ -133,6 +124,7 @@ object FieldFilters {
HintFilter,
TypeTheAnswerFilter,
TypeTheAnswerNonCombiningFilter,
NoSuggestFilter,
TextToSpeechFilter(),
FuriganaFilter,
KanaFilter,
Expand Down Expand Up @@ -240,7 +232,7 @@ object FieldFilters {
object TypeTheAnswerFilter : FieldFilter {
override val name: String = "type"

override fun updateContext(input: FilterContext) = input.copy(isTerminal = true)
override fun updateContext(input: FilterContext) = input.copy(isTerminal = true, producesTypeMarker = true)
}

/**
Expand All @@ -263,7 +255,49 @@ object FieldFilters {
object TypeTheAnswerNonCombiningFilter : FieldFilter {
override val name: String = "type:nc"

override fun updateContext(input: FilterContext) = input.copy(isTerminal = true)
override fun updateContext(input: FilterContext) = input.copy(isTerminal = true, producesTypeMarker = true)
}

/**
* `{{nosuggest:type:Field}}`: disables keyboard suggestions, swiping and autocorrect
*
* AnkiDroid-only.
*
* Converts the rendered `[[type:abc]]` to `[[type:nosuggest:abc]]`, which is then consumed
* by the `TypeAnswer` parser.
*
* See [#10352](https://github.com/ankidroid/Anki-Android/issues/10352).
* @see com.ichi2.anki.previewer.TypeAnswer.noSuggest
*/
object NoSuggestFilter :
FieldFilter,
TemplateManager.FieldFilter {
override val name: String = "nosuggest"

/**
* May only be applied to `type:` or `type:nc` filter which are both terminal chains,
* therefore, do not check 'super.canApplyTo'
*/
override fun canApplyTo(context: FilterContext): Boolean = context.producesTypeMarker

override fun updateContext(input: FilterContext): FilterContext = input

override fun apply(
fieldText: String,
fieldName: String,
filterName: String,
ctx: TemplateManager.TemplateRenderContext,
): String {
if (filterName != name) return fieldText
if (!fieldText.contains("[[type:")) return fieldText
if (fieldText.contains("[[type:nosuggest:")) return fieldText
return fieldText.replace("[[type:", "[[type:nosuggest:")
}

/** Registers this filter as a runtime transformer with the [TemplateManager]. */
fun ensureApplied() {
TemplateManager.fieldFilters.putIfAbsent(name, this)
}
}

/**
Expand Down
Loading
Loading