Skip to content

Commit

Permalink
Merge branch 'ankidroid:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
Tyler-Programs authored Oct 12, 2021
2 parents 53ea691 + 351258d commit 34e24a6
Show file tree
Hide file tree
Showing 12 changed files with 684 additions and 604 deletions.
270 changes: 21 additions & 249 deletions AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.java

Large diffs are not rendered by default.

26 changes: 14 additions & 12 deletions AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,10 @@
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
Expand Down Expand Up @@ -89,6 +87,7 @@
import com.ichi2.anki.noteeditor.CustomToolbarButton;
import com.ichi2.anki.noteeditor.Toolbar;
import com.ichi2.anki.receiver.SdCardReceiver;
import com.ichi2.anki.servicelayer.LanguageHintService;
import com.ichi2.anki.servicelayer.NoteService;
import com.ichi2.anki.ui.NoteTypeSpinnerUtils;
import com.ichi2.anki.widgets.DeckDropDownAdapter;
Expand All @@ -98,6 +97,7 @@
import com.ichi2.compat.CompatHelper;
import com.ichi2.libanki.Card;
import com.ichi2.libanki.Collection;
import com.ichi2.libanki.Consts;
import com.ichi2.libanki.Models;
import com.ichi2.libanki.Model;
import com.ichi2.libanki.Note;
Expand All @@ -112,7 +112,6 @@
import com.ichi2.utils.HashUtil;
import com.ichi2.utils.KeyUtils;
import com.ichi2.utils.MapUtil;
import com.ichi2.utils.NamedJSONComparator;
import com.ichi2.utils.NoteFieldDecorator;
import com.ichi2.utils.TextViewUtil;
import com.ichi2.widget.WidgetStatus;
Expand All @@ -122,7 +121,6 @@

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
Expand Down Expand Up @@ -1677,18 +1675,13 @@ private void initFieldEditText(FieldEditText editText, final int index, boolean
editText.setEnabled(enabled);
}


@Nullable
private Locale getHintLocaleForField(String name) {
JSONObject field = getFieldByName(name);
if (field == null) {
return null;
}
String languageTag = field.optString("ad-hint-locale", null);
if (languageTag == null) {
return null;
}

return Locale.forLanguageTag(languageTag);
return LanguageHintService.getLanguageHintForField(field);
}

@NonNull
Expand Down Expand Up @@ -2081,7 +2074,7 @@ public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
getCol().getDecks().save(currentDeck);
// Update deck
if (!getCol().get_config("addToCur", true)) {
mCurrentDid = model.getLong("did");
mCurrentDid = model.optLong("did", Consts.DEFAULT_DECK_ID);
}

refreshNoteData(FieldChangeType.changeFieldCount(shouldReplaceNewlines()));
Expand Down Expand Up @@ -2309,6 +2302,15 @@ FieldEditText getFieldForTest(int index) {
}


@VisibleForTesting(otherwise = VisibleForTesting.NONE)
void setCurrentlySelectedModel(long mid) {
int position = mAllModelIds.indexOf(mid);
if (position == -1) {
throw new IllegalStateException(mid + " not found");
}
mNoteTypeSpinner.setSelection(position);
}

private class EditFieldTextWatcher implements TextWatcher {
private final int mIndex;

Expand Down
308 changes: 308 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/TypeAnswer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
/*
* Copyright (c) 2021 David Allison <[email protected]>
*
* 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/>.
*/

package com.ichi2.anki.cardviewer

import android.content.SharedPreferences
import android.content.res.Resources
import android.text.TextUtils
import androidx.annotation.VisibleForTesting
import com.ichi2.anki.R
import com.ichi2.libanki.Card
import com.ichi2.libanki.Sound
import com.ichi2.libanki.Utils
import com.ichi2.utils.DiffEngine
import com.ichi2.utils.JSONArray
import timber.log.Timber
import java.util.*
import java.util.regex.Matcher
import java.util.regex.Pattern

class TypeAnswer(
@get:JvmName("useInputTag") val useInputTag: Boolean,
@get:JvmName("doNotUseCodeFormatting") val doNotUseCodeFormatting: Boolean,
/** Preference: Whether the user wants to focus "type in answer" */
val autoFocus: Boolean
) {

/** The correct answer in the compare to field if answer should be given by learner. Null if no answer is expected. */
var correct: String? = null
private set

/** What the learner actually typed (externally mutable) */
var input = ""
/** Font face of the 'compare to' field */
var font = ""
private set
/** The font size of the 'compare to' field */
var size = 0
private set

/**
* Optional warning for when a typed answer can't be displayed
*
* * empty card [R.string.empty_card_warning]
* * unknown field specified [R.string.unknown_type_field_warning]
* */
var warning: String? = null
private set

/**
* @return true If entering input via EditText
* and if the current card has a {{type:field}} on the card template
*/
fun validForEditText(): Boolean {
return !useInputTag && correct != null
}

fun autoFocusEditText(): Boolean {
return validForEditText() && autoFocus
}

/**
* Extract type answer/cloze text and font/size
* @param card The next card to display
*/
fun updateInfo(card: Card, res: Resources) {
correct = null
input = ""
val q = card.q(false)
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.getOrd() + 1
fldTag = fldTag.split(":").toTypedArray()[1]
}
// loop through fields for a match
val flds: JSONArray = card.model().getJSONArray("flds")
for (fld in flds.jsonObjectIterable()) {
val name = fld.getString("name")
if (name == fldTag) {
correct = card.note().getItem(name)
if (clozeIdx != 0) {
// narrow to cloze
correct = contentForCloze(correct!!, clozeIdx)
}
font = fld.getString("font")
size = fld.getInt("size")
break
}
}
when (correct) {
null -> {
warning = if (clozeIdx != 0) {
res.getString(R.string.empty_card_warning)
} else {
res.getString(R.string.unknown_type_field_warning, fldTag)
}
}
"" -> {
correct = null
}
else -> {
warning = null
}
}
}

/**
* Format question field when it contains typeAnswer or clozes. If there was an error during type text extraction, a
* warning is displayed
*
* @param buf The question text
* @return The formatted question text
*/
fun filterQuestion(buf: String): String? {
val m = PATTERN.matcher(buf)
if (warning != null) {
return m.replaceFirst(warning!!)
}
val sb = java.lang.StringBuilder()
if (useInputTag) {
// These functions are defined in the JavaScript file assets/scripts/card.js. We get the text back in
// shouldOverrideUrlLoading() in createWebView() in this file.
sb.append(
"""<center>
<input type="text" name="typed" id="typeans" onfocus="taFocus();" onblur="taBlur(this);" onKeyPress="return taKey(this, event)" autocomplete="off" """
)
// We have to watch out. For the preview we don’t know the font or font size. Skip those there. (Anki
// desktop just doesn’t show the input tag there. Do it with standard values here instead.)
if (!TextUtils.isEmpty(font) && size > 0) {
sb.append("style=\"font-family: '").append(font).append("'; font-size: ")
.append(size).append("px;\" ")
}
sb.append(">\n</center>\n")
} else {
sb.append("<span id=\"typeans\" class=\"typePrompt")
if (useInputTag) {
sb.append(" typeOff")
}
sb.append("\">........</span>")
}
return m.replaceAll(sb.toString())
}

/**
* Fill the placeholder for the type comparison: `[[type:(.+?)]]`
*
* Replaces with the HTML for the correct answer, and the comparison to the correct answer if appropriate.
*
* @param answer The card content on the back of the card
*
* @return The formatted answer text with `[[type:(.+?)]]` replaced with HTML
*/
fun filterAnswer(answer: String): String {
val userAnswer = cleanTypedAnswer(input)
val correctAnswer = cleanCorrectAnswer(correct)
Timber.d("correct answer = %s", correctAnswer)
Timber.d("user answer = %s", userAnswer)
return filterAnswer(answer, userAnswer, correctAnswer)
}

/**
* Fill the placeholder for the type comparison. Show the correct answer, and the comparison if appropriate.
*
* @param answer The answer text
* @param userAnswer Text typed by the user, or empty.
* @param correctAnswer The correct answer, taken from the note.
* @return The formatted answer text
*/
fun filterAnswer(answer: String, userAnswer: String, correctAnswer: String): String {
val m: Matcher = PATTERN.matcher(answer)
val diffEngine = DiffEngine()
val sb = StringBuilder()
sb.append(if (doNotUseCodeFormatting) "<div><span id=\"typeans\">" else "<div><code id=\"typeans\">")

// We have to use Matcher.quoteReplacement because the inputs here might have $ or \.
if (userAnswer.isNotEmpty()) {
// The user did type something.
if (userAnswer == correctAnswer) {
// and it was right.
sb.append(Matcher.quoteReplacement(DiffEngine.wrapGood(correctAnswer)))
sb.append("<span id=\"typecheckmark\">\u2714</span>") // Heavy check mark
} else {
// Answer not correct.
// Only use the complex diff code when needed, that is when we have some typed text that is not
// exactly the same as the correct text.
val diffedStrings = diffEngine.diffedHtmlStrings(correctAnswer, userAnswer)
// We know we get back two strings.
sb.append(Matcher.quoteReplacement(diffedStrings[0]))
sb.append("<br><span id=\"typearrow\">&darr;</span><br>")
sb.append(Matcher.quoteReplacement(diffedStrings[1]))
}
} else {
if (!useInputTag) {
sb.append(Matcher.quoteReplacement(DiffEngine.wrapMissing(correctAnswer)))
} else {
sb.append(Matcher.quoteReplacement(correctAnswer))
}
}
sb.append(if (doNotUseCodeFormatting) "</span></div>" else "</code></div>")
return m.replaceAll(sb.toString())
}

companion object {
@JvmField
/** Regular expression in card data for a 'type answer' after processing has occurred */
val PATTERN: Pattern = Pattern.compile("\\[\\[type:(.+?)]]")

@JvmStatic
fun createInstance(preferences: SharedPreferences): TypeAnswer {
return TypeAnswer(
useInputTag = preferences.getBoolean("useInputTag", false),
doNotUseCodeFormatting = preferences.getBoolean("noCodeFormatting", false),
autoFocus = preferences.getBoolean("autoFocusTypeInAnswer", false)
)
}

/** Regex pattern used in removing tags from text before diff */
private val spanPattern = Pattern.compile("</?span[^>]*>")
private val brPattern = Pattern.compile("<br\\s?/?>")

/**
* Clean up the correct answer text, so it can be used for the comparison with the typed text
*
* @param answer The content of the field the text typed by the user is compared to.
* @return The correct answer text, with actual HTML and media references removed, and HTML entities unescaped.
*/
@JvmStatic
fun cleanCorrectAnswer(answer: String?): String {
if (answer == null || "" == answer) {
return ""
}
var matcher = spanPattern.matcher(Utils.stripHTML(answer.trim { it <= ' ' }))
var answerText = matcher.replaceAll("")
matcher = brPattern.matcher(answerText)
answerText = matcher.replaceAll("\n")
matcher = Sound.SOUND_PATTERN.matcher(answerText)
answerText = matcher.replaceAll("")
return Utils.nfcNormalized(answerText)
}

/**
* Clean up the typed answer text, so it can be used for the comparison with the correct answer
*
* @param answer The answer text typed by the user.
* @return The typed answer text, cleaned up.
*/
@JvmStatic
fun cleanTypedAnswer(answer: String?): String {
return if (answer == null || "" == answer) {
""
} else Utils.nfcNormalized(answer.trim())
}

/**
* Return the correct answer to use for {{type::cloze::NN}} fields.
*
* @param txt The field text with the clozes
* @param idx The index of the cloze to use
* @return If the cloze strings are the same, return a single cloze string, otherwise, return
* a string with a comma-separeted list of strings with the correct index.
*/
@VisibleForTesting
fun contentForCloze(txt: String, idx: Int): String? {
// In Android, } should be escaped
val re = Pattern.compile("\\{\\{c$idx::(.+?)\\}\\}")
val m = re.matcher(txt)
val matches: MutableList<String?> = ArrayList()
var groupOne: String
while (m.find()) {
groupOne = m.group(1)!!
val colonColonIndex = groupOne.indexOf("::")
if (colonColonIndex > -1) {
// Cut out the hint.
groupOne = groupOne.substring(0, colonColonIndex)
}
matches.add(groupOne)
}
val uniqMatches: Set<String?> = HashSet(matches) // Allow to check whether there are distinct strings

// Make it consistent with the Desktop version (see issue #8229)
return if (uniqMatches.size == 1) {
matches[0]
} else {
TextUtils.join(", ", matches)
}
}
}
}
Loading

0 comments on commit 34e24a6

Please sign in to comment.