Monkeytype qt/aqt/reviewer.py
This commit is contained in:
parent
96ca469d12
commit
6c2dda6c9c
@ -657,6 +657,7 @@ where c.nid = n.id and c.id in %s group by nid"""
|
||||
self._startTime = time.time()
|
||||
self._startReps = self.sched.reps
|
||||
|
||||
# FIXME: Use Literal[False] when on Python 3.8
|
||||
def timeboxReached(self) -> Union[bool, Tuple[Any, int]]:
|
||||
"Return (elapsedTime, reps) if timebox reached, or False."
|
||||
if not self.conf["timeLim"]:
|
||||
|
@ -6,11 +6,12 @@ from __future__ import annotations
|
||||
|
||||
import difflib
|
||||
import html
|
||||
import html.parser
|
||||
import json
|
||||
import re
|
||||
import unicodedata as ucd
|
||||
from typing import List, Optional
|
||||
from typing import Callable, List, Optional, Sequence, Tuple, Union
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from anki import hooks
|
||||
from anki.cards import Card
|
||||
@ -25,7 +26,7 @@ from aqt.utils import askUserDialog, downArrow, qtMenuShortcutWorkaround, toolti
|
||||
|
||||
|
||||
class ReviewerBottomBar:
|
||||
def __init__(self, reviewer: Reviewer):
|
||||
def __init__(self, reviewer: Reviewer) -> None:
|
||||
self.reviewer = reviewer
|
||||
|
||||
|
||||
@ -39,28 +40,29 @@ class Reviewer:
|
||||
self.cardQueue: List[Card] = []
|
||||
self.hadCardQueue = False
|
||||
self._answeredIds: List[int] = []
|
||||
self._recordedAudio = None
|
||||
self.typeCorrect = None # web init happens before this is set
|
||||
self._recordedAudio: Optional[str] = None
|
||||
self.typeCorrect: str = None # web init happens before this is set
|
||||
self.state: Optional[str] = None
|
||||
self.bottom = BottomBar(mw, mw.bottomWeb)
|
||||
hooks.card_did_leech.append(self.onLeech)
|
||||
|
||||
def show(self):
|
||||
def show(self) -> None:
|
||||
self.mw.col.reset()
|
||||
self.mw.setStateShortcuts(self._shortcutKeys())
|
||||
self.mw.setStateShortcuts(self._shortcutKeys()) # type: ignore
|
||||
self.web.set_bridge_command(self._linkHandler, self)
|
||||
self.bottom.web.set_bridge_command(self._linkHandler, ReviewerBottomBar(self))
|
||||
self._reps = None
|
||||
self._reps: int = None
|
||||
self.nextCard()
|
||||
|
||||
def lastCard(self):
|
||||
def lastCard(self) -> Optional[Card]:
|
||||
if self._answeredIds:
|
||||
if not self.card or self._answeredIds[-1] != self.card.id:
|
||||
try:
|
||||
return self.mw.col.getCard(self._answeredIds[-1])
|
||||
except TypeError:
|
||||
# id was deleted
|
||||
return
|
||||
return None
|
||||
return None
|
||||
|
||||
def cleanup(self) -> None:
|
||||
gui_hooks.reviewer_will_end()
|
||||
@ -68,9 +70,10 @@ class Reviewer:
|
||||
# Fetching a card
|
||||
##########################################################################
|
||||
|
||||
def nextCard(self):
|
||||
def nextCard(self) -> None:
|
||||
elapsed = self.mw.col.timeboxReached()
|
||||
if elapsed:
|
||||
assert not isinstance(elapsed, bool)
|
||||
part1 = (
|
||||
ngettext("%d card studied in", "%d cards studied in", elapsed[1])
|
||||
% elapsed[1]
|
||||
@ -125,7 +128,7 @@ class Reviewer:
|
||||
# Initializing the webview
|
||||
##########################################################################
|
||||
|
||||
def revHtml(self):
|
||||
def revHtml(self) -> str:
|
||||
extra = self.mw.col.conf.get("reviewExtra", "")
|
||||
fade = ""
|
||||
if self.mw.pm.glMode() == "software":
|
||||
@ -140,7 +143,7 @@ class Reviewer:
|
||||
fade, extra
|
||||
)
|
||||
|
||||
def _initWeb(self):
|
||||
def _initWeb(self) -> None:
|
||||
self._reps = 0
|
||||
# main window
|
||||
self.web.stdHtml(
|
||||
@ -167,13 +170,13 @@ class Reviewer:
|
||||
# Showing the question
|
||||
##########################################################################
|
||||
|
||||
def _mungeQA(self, buf):
|
||||
def _mungeQA(self, buf: str) -> str:
|
||||
return self.typeAnsFilter(self.mw.prepare_card_text_for_display(buf))
|
||||
|
||||
def _showQuestion(self) -> None:
|
||||
self._reps += 1
|
||||
self.state = "question"
|
||||
self.typedAnswer = None
|
||||
self.typedAnswer: str = None
|
||||
c = self.card
|
||||
# grab the question and play audio
|
||||
if c.isEmpty():
|
||||
@ -206,17 +209,17 @@ The front of this card is empty. Please run Tools>Empty Cards."""
|
||||
# user hook
|
||||
gui_hooks.reviewer_did_show_question(c)
|
||||
|
||||
def autoplay(self, card):
|
||||
def autoplay(self, card: Card) -> bool:
|
||||
return self.mw.col.decks.confForDid(card.odid or card.did)["autoplay"]
|
||||
|
||||
def _replayq(self, card, previewer=None):
|
||||
s = previewer if previewer else self
|
||||
return s.mw.col.decks.confForDid(s.card.odid or s.card.did).get("replayq", True)
|
||||
|
||||
def _drawFlag(self):
|
||||
def _drawFlag(self) -> None:
|
||||
self.web.eval("_drawFlag(%s);" % self.card.userFlag())
|
||||
|
||||
def _drawMark(self):
|
||||
def _drawMark(self) -> None:
|
||||
self.web.eval("_drawMark(%s);" % json.dumps(self.card.note().hasTag("marked")))
|
||||
|
||||
# Showing the answer
|
||||
@ -246,7 +249,7 @@ The front of this card is empty. Please run Tools>Empty Cards."""
|
||||
# Answering a card
|
||||
############################################################
|
||||
|
||||
def _answerCard(self, ease):
|
||||
def _answerCard(self, ease: int) -> None:
|
||||
"Reschedule card and show next."
|
||||
if self.mw.state != "review":
|
||||
# showing resetRequired screen; ignore key
|
||||
@ -269,7 +272,9 @@ The front of this card is empty. Please run Tools>Empty Cards."""
|
||||
# Handlers
|
||||
############################################################
|
||||
|
||||
def _shortcutKeys(self):
|
||||
def _shortcutKeys(
|
||||
self,
|
||||
) -> List[Union[Tuple[str, Callable], Tuple[Qt.Key, Callable]]]:
|
||||
return [
|
||||
("e", self.mw.onEditCurrent),
|
||||
(" ", self.onEnterKey),
|
||||
@ -299,18 +304,18 @@ The front of this card is empty. Please run Tools>Empty Cards."""
|
||||
("7", self.on_seek_forward),
|
||||
]
|
||||
|
||||
def on_pause_audio(self):
|
||||
def on_pause_audio(self) -> None:
|
||||
av_player.toggle_pause()
|
||||
|
||||
seek_secs = 5
|
||||
|
||||
def on_seek_backward(self):
|
||||
def on_seek_backward(self) -> None:
|
||||
av_player.seek_relative(-self.seek_secs)
|
||||
|
||||
def on_seek_forward(self):
|
||||
def on_seek_forward(self) -> None:
|
||||
av_player.seek_relative(self.seek_secs)
|
||||
|
||||
def onEnterKey(self):
|
||||
def onEnterKey(self) -> None:
|
||||
if self.state == "question":
|
||||
self._getTypedAnswer()
|
||||
elif self.state == "answer":
|
||||
@ -318,14 +323,14 @@ The front of this card is empty. Please run Tools>Empty Cards."""
|
||||
"selectedAnswerButton()", self._onAnswerButton
|
||||
)
|
||||
|
||||
def _onAnswerButton(self, val):
|
||||
def _onAnswerButton(self, val: str) -> None:
|
||||
# button selected?
|
||||
if val and val in "1234":
|
||||
self._answerCard(int(val))
|
||||
else:
|
||||
self._answerCard(self._defaultEase())
|
||||
|
||||
def _linkHandler(self, url):
|
||||
def _linkHandler(self, url: str) -> None:
|
||||
if url == "ans":
|
||||
self._getTypedAnswer()
|
||||
elif url.startswith("ease"):
|
||||
@ -344,13 +349,13 @@ The front of this card is empty. Please run Tools>Empty Cards."""
|
||||
|
||||
typeAnsPat = r"\[\[type:(.+?)\]\]"
|
||||
|
||||
def typeAnsFilter(self, buf):
|
||||
def typeAnsFilter(self, buf: str) -> str:
|
||||
if self.state == "question":
|
||||
return self.typeAnsQuestionFilter(buf)
|
||||
else:
|
||||
return self.typeAnsAnswerFilter(buf)
|
||||
|
||||
def typeAnsQuestionFilter(self, buf):
|
||||
def typeAnsQuestionFilter(self, buf: str) -> str:
|
||||
self.typeCorrect = None
|
||||
clozeIdx = None
|
||||
m = re.search(self.typeAnsPat, buf)
|
||||
@ -397,20 +402,19 @@ Please run Tools>Empty Cards"""
|
||||
buf,
|
||||
)
|
||||
|
||||
def typeAnsAnswerFilter(self, buf):
|
||||
def typeAnsAnswerFilter(self, buf: str) -> str:
|
||||
if not self.typeCorrect:
|
||||
return re.sub(self.typeAnsPat, "", buf)
|
||||
origSize = len(buf)
|
||||
buf = buf.replace("<hr id=answer>", "")
|
||||
hadHR = len(buf) != origSize
|
||||
# munge correct value
|
||||
parser = html.parser.HTMLParser()
|
||||
cor = self.mw.col.media.strip(self.typeCorrect)
|
||||
cor = re.sub("(\n|<br ?/?>|</?div>)+", " ", cor)
|
||||
cor = stripHTML(cor)
|
||||
# ensure we don't chomp multiple whitespace
|
||||
cor = cor.replace(" ", " ")
|
||||
cor = parser.unescape(cor)
|
||||
cor = html.unescape(cor)
|
||||
cor = cor.replace("\xa0", " ")
|
||||
cor = cor.strip()
|
||||
given = self.typedAnswer
|
||||
@ -434,7 +438,7 @@ Please run Tools>Empty Cards"""
|
||||
|
||||
return re.sub(self.typeAnsPat, repl, buf)
|
||||
|
||||
def _contentForCloze(self, txt, idx):
|
||||
def _contentForCloze(self, txt: str, idx) -> str:
|
||||
matches = re.findall(r"\{\{c%s::(.+?)\}\}" % idx, txt, re.DOTALL)
|
||||
if not matches:
|
||||
return None
|
||||
@ -452,24 +456,28 @@ Please run Tools>Empty Cards"""
|
||||
txt = ", ".join(matches)
|
||||
return txt
|
||||
|
||||
def tokenizeComparison(self, given, correct):
|
||||
def tokenizeComparison(
|
||||
self, given: str, correct: str
|
||||
) -> Tuple[List[Tuple[bool, str]], List[Tuple[bool, str]]]:
|
||||
# compare in NFC form so accents appear correct
|
||||
given = ucd.normalize("NFC", given)
|
||||
correct = ucd.normalize("NFC", correct)
|
||||
s = difflib.SequenceMatcher(None, given, correct, autojunk=False)
|
||||
givenElems = []
|
||||
correctElems = []
|
||||
givenElems: List[Tuple[bool, str]] = []
|
||||
correctElems: List[Tuple[bool, str]] = []
|
||||
givenPoint = 0
|
||||
correctPoint = 0
|
||||
offby = 0
|
||||
|
||||
def logBad(old, new, str, array):
|
||||
def logBad(old: int, new: int, s: str, array: List[Tuple[bool, str]]) -> None:
|
||||
if old != new:
|
||||
array.append((False, str[old:new]))
|
||||
array.append((False, s[old:new]))
|
||||
|
||||
def logGood(start, cnt, str, array):
|
||||
def logGood(
|
||||
start: int, cnt: int, s: str, array: List[Tuple[bool, str]]
|
||||
) -> None:
|
||||
if cnt:
|
||||
array.append((True, str[start : start + cnt]))
|
||||
array.append((True, s[start : start + cnt]))
|
||||
|
||||
for x, y, cnt in s.get_matching_blocks():
|
||||
# if anything was missed in correct, pad given
|
||||
@ -486,17 +494,17 @@ Please run Tools>Empty Cards"""
|
||||
logGood(y, cnt, correct, correctElems)
|
||||
return givenElems, correctElems
|
||||
|
||||
def correct(self, given, correct, showBad=True):
|
||||
def correct(self, given: str, correct: str, showBad: bool = True) -> str:
|
||||
"Diff-corrects the typed-in answer."
|
||||
givenElems, correctElems = self.tokenizeComparison(given, correct)
|
||||
|
||||
def good(s):
|
||||
def good(s: str) -> str:
|
||||
return "<span class=typeGood>" + html.escape(s) + "</span>"
|
||||
|
||||
def bad(s):
|
||||
def bad(s: str) -> str:
|
||||
return "<span class=typeBad>" + html.escape(s) + "</span>"
|
||||
|
||||
def missed(s):
|
||||
def missed(s: str) -> str:
|
||||
return "<span class=typeMissed>" + html.escape(s) + "</span>"
|
||||
|
||||
if given == correct:
|
||||
@ -519,24 +527,24 @@ Please run Tools>Empty Cards"""
|
||||
res = "<div><code id=typeans>" + res + "</code></div>"
|
||||
return res
|
||||
|
||||
def _noLoneMarks(self, s):
|
||||
def _noLoneMarks(self, s: str) -> str:
|
||||
# ensure a combining character at the start does not join to
|
||||
# previous text
|
||||
if s and ucd.category(s[0]).startswith("M"):
|
||||
return "\xa0" + s
|
||||
return s
|
||||
|
||||
def _getTypedAnswer(self):
|
||||
def _getTypedAnswer(self) -> None:
|
||||
self.web.evalWithCallback("typeans ? typeans.value : null", self._onTypedAnswer)
|
||||
|
||||
def _onTypedAnswer(self, val):
|
||||
def _onTypedAnswer(self, val: None) -> None:
|
||||
self.typedAnswer = val or ""
|
||||
self._showAnswer()
|
||||
|
||||
# Bottom bar
|
||||
##########################################################################
|
||||
|
||||
def _bottomHTML(self):
|
||||
def _bottomHTML(self) -> str:
|
||||
return """
|
||||
<center id=outer>
|
||||
<table id=innertable width=100%% cellspacing=0 cellpadding=0>
|
||||
@ -565,7 +573,7 @@ time = %(time)d;
|
||||
time=self.card.timeTaken() // 1000,
|
||||
)
|
||||
|
||||
def _showAnswerButton(self):
|
||||
def _showAnswerButton(self) -> None:
|
||||
if not self.typeCorrect:
|
||||
self.bottom.web.setFocus()
|
||||
middle = """
|
||||
@ -587,17 +595,17 @@ time = %(time)d;
|
||||
self.bottom.web.eval("showQuestion(%s,%d);" % (json.dumps(middle), maxTime))
|
||||
self.bottom.web.adjustHeightToFit()
|
||||
|
||||
def _showEaseButtons(self):
|
||||
def _showEaseButtons(self) -> None:
|
||||
self.bottom.web.setFocus()
|
||||
middle = self._answerButtons()
|
||||
self.bottom.web.eval("showAnswer(%s);" % json.dumps(middle))
|
||||
|
||||
def _remaining(self):
|
||||
def _remaining(self) -> str:
|
||||
if not self.mw.col.conf["dueCounts"]:
|
||||
return ""
|
||||
if self.hadCardQueue:
|
||||
# if it's come from the undo queue, don't count it separately
|
||||
counts = list(self.mw.col.sched.counts())
|
||||
counts: List[Union[int, str]] = list(self.mw.col.sched.counts())
|
||||
else:
|
||||
counts = list(self.mw.col.sched.counts(self.card))
|
||||
idx = self.mw.col.sched.countIdx(self.card)
|
||||
@ -608,13 +616,13 @@ time = %(time)d;
|
||||
ctxt += space + "<span class=review-count>%s</span>" % counts[2]
|
||||
return ctxt
|
||||
|
||||
def _defaultEase(self):
|
||||
def _defaultEase(self) -> int:
|
||||
if self.mw.col.sched.answerButtons(self.card) == 4:
|
||||
return 3
|
||||
else:
|
||||
return 2
|
||||
|
||||
def _answerButtonList(self):
|
||||
def _answerButtonList(self) -> Sequence[Tuple[int, str]]:
|
||||
l = ((1, _("Again")),)
|
||||
cnt = self.mw.col.sched.answerButtons(self.card)
|
||||
if cnt == 2:
|
||||
@ -624,7 +632,7 @@ time = %(time)d;
|
||||
else:
|
||||
return l + ((2, _("Hard")), (3, _("Good")), (4, _("Easy")))
|
||||
|
||||
def _answerButtons(self):
|
||||
def _answerButtons(self) -> str:
|
||||
default = self._defaultEase()
|
||||
|
||||
def but(i, label):
|
||||
@ -652,7 +660,7 @@ time = %(time)d;
|
||||
<script>$(function () { $("#defease").focus(); });</script>"""
|
||||
return buf + script
|
||||
|
||||
def _buttonTime(self, i):
|
||||
def _buttonTime(self, i: int) -> str:
|
||||
if not self.mw.col.conf["estTimes"]:
|
||||
return "<div class=spacer></div>"
|
||||
txt = self.mw.col.sched.nextIvlStr(self.card, i, True) or " "
|
||||
@ -661,7 +669,7 @@ time = %(time)d;
|
||||
# Leeches
|
||||
##########################################################################
|
||||
|
||||
def onLeech(self, card):
|
||||
def onLeech(self, card: Card) -> None:
|
||||
# for now
|
||||
s = _("Card was a leech.")
|
||||
if card.queue < 0:
|
||||
@ -730,7 +738,7 @@ time = %(time)d;
|
||||
qtMenuShortcutWorkaround(m)
|
||||
m.exec_(QCursor.pos())
|
||||
|
||||
def _addMenuItems(self, m, rows):
|
||||
def _addMenuItems(self, m, rows) -> None:
|
||||
for row in rows:
|
||||
if not row:
|
||||
m.addSeparator()
|
||||
@ -753,10 +761,10 @@ time = %(time)d;
|
||||
a.setChecked(True)
|
||||
a.triggered.connect(func)
|
||||
|
||||
def onOptions(self):
|
||||
def onOptions(self) -> None:
|
||||
self.mw.onDeckConf(self.mw.col.decks.get(self.card.odid or self.card.did))
|
||||
|
||||
def setFlag(self, flag):
|
||||
def setFlag(self, flag: int) -> None:
|
||||
# need to toggle off?
|
||||
if self.card.userFlag() == flag:
|
||||
flag = 0
|
||||
@ -764,7 +772,7 @@ time = %(time)d;
|
||||
self.card.flush()
|
||||
self._drawFlag()
|
||||
|
||||
def onMark(self):
|
||||
def onMark(self) -> None:
|
||||
f = self.card.note()
|
||||
if f.hasTag("marked"):
|
||||
f.delTag("marked")
|
||||
@ -773,19 +781,19 @@ time = %(time)d;
|
||||
f.flush()
|
||||
self._drawMark()
|
||||
|
||||
def onSuspend(self):
|
||||
def onSuspend(self) -> None:
|
||||
self.mw.checkpoint(_("Suspend"))
|
||||
self.mw.col.sched.suspendCards([c.id for c in self.card.note().cards()])
|
||||
tooltip(_("Note suspended."))
|
||||
self.mw.reset()
|
||||
|
||||
def onSuspendCard(self):
|
||||
def onSuspendCard(self) -> None:
|
||||
self.mw.checkpoint(_("Suspend"))
|
||||
self.mw.col.sched.suspendCards([self.card.id])
|
||||
tooltip(_("Card suspended."))
|
||||
self.mw.reset()
|
||||
|
||||
def onDelete(self):
|
||||
def onDelete(self) -> None:
|
||||
# need to check state because the shortcut is global to the main
|
||||
# window
|
||||
if self.mw.state != "review" or not self.card:
|
||||
@ -801,23 +809,24 @@ time = %(time)d;
|
||||
% cnt
|
||||
)
|
||||
|
||||
def onBuryCard(self):
|
||||
def onBuryCard(self) -> None:
|
||||
self.mw.checkpoint(_("Bury"))
|
||||
self.mw.col.sched.buryCards([self.card.id])
|
||||
self.mw.reset()
|
||||
tooltip(_("Card buried."))
|
||||
|
||||
def onBuryNote(self):
|
||||
def onBuryNote(self) -> None:
|
||||
self.mw.checkpoint(_("Bury"))
|
||||
self.mw.col.sched.buryNote(self.card.nid)
|
||||
self.mw.reset()
|
||||
tooltip(_("Note buried."))
|
||||
|
||||
def onRecordVoice(self):
|
||||
def onRecordVoice(self) -> None:
|
||||
self._recordedAudio = getAudio(self.mw, encode=False)
|
||||
self.onReplayRecorded()
|
||||
|
||||
def onReplayRecorded(self):
|
||||
def onReplayRecorded(self) -> None:
|
||||
if not self._recordedAudio:
|
||||
return tooltip(_("You haven't recorded your voice yet."))
|
||||
tooltip(_("You haven't recorded your voice yet."))
|
||||
return
|
||||
av_player.play_file(self._recordedAudio)
|
||||
|
Loading…
Reference in New Issue
Block a user