# -*- coding: utf-8 -*- # Copyright: Damien Elmes # License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html import time, os, stat, shutil, difflib, simplejson import unicodedata as ucd from PyQt4.QtCore import * from PyQt4.QtGui import * from anki.utils import fmtTimeSpan, stripHTML from anki.hooks import addHook, runHook, runFilter from anki.sound import playFromText, clearAudioQueue from aqt.utils import mungeQA, getBase import aqt class Reviewer(object): "Manage reviews. Maintains a separate state." def __init__(self, mw): self.mw = mw self.web = mw.web self.card = None self.cardQueue = [] self._answeredIds = [] self.state = None self._setupStatus() addHook("leech", self.onLeech) def show(self): self.web.setKeyHandler(self._keyHandler) self.web.setLinkHandler(self._linkHandler) self._getCard() def lastCard(self): if self._answeredIds: if not self.card or self._answeredIds[-1] != self.card.id: return self.mw.deck.getCard(self._answeredIds[-1]) # Fetching a card ########################################################################## def _getCard(self): if self.cardQueue: # a card has been retrieved from undo c = self.cardQueue.pop() else: c = self.mw.deck.sched.getCard() self.card = c clearAudioQueue() if c: self.mw.enableCardMenuItems() self._showStatus() self._maybeEnableSound() #self.updateMarkAction() self.state = "question" self._initWeb() else: self._hideStatus() self.mw.disableCardMenuItems() if self.mw.deck.cardCount(): self._showCongrats() else: self._showEmpty() def _maybeEnableSound(self): print "enable sound fixme" return snd = (hasSound(self.reviewer.card.q()) or (hasSound(self.reviewer.card.a()) and self.state != "getQuestion")) self.form.actionRepeatAudio.setEnabled(snd) # Initializing the webview ########################################################################## _revHtml = """

%(showans)s
""" def _initWeb(self): self.web.stdHtml(self._revHtml % dict( showans=_("Show Answer")), self._styles(), loadCB=lambda x: self._showQuestion()) # Showing the question (and preparing answer) ########################################################################## def _showQuestion(self): self.state = "question" # fixme: timeboxing # fixme: prevent audio from repeating # fixme: include placeholder for type answer result c = self.card # original question with sounds q = c.q() a = c.a() if (#self.state != self.oldState and not nosound self.mw.config['autoplaySounds']): playFromText(q) # render # buf = self.typeAnsResult() esc = self.mw.deck.media.escapeImages q=esc(mungeQA(q)) + self.typeAnsInput() a=esc(mungeQA(a)) self.web.eval("updateQA(%s);" % simplejson.dumps( [q, a, self._answerButtons(), c.cssClass(), c.template()['hideQ']])) runHook('showQuestion') # Showing the answer ########################################################################## def _showAnswer(self): self.state = "answer" c = self.card a = c.a() if self.mw.config['autoplaySounds']: playFromText(a) # render runHook('showAnswer') # Ease buttons ########################################################################## def _defaultEase(self): if self.mw.deck.sched.answerButtons(self.card) == 4: return 3 else: return 2 def _answerButtons(self): if self.mw.deck.sched.answerButtons(self.card) == 4: labels = (_("Again"), _("Hard"), _("Good"), _("Easy")) else: labels = (_("Again"), _("Good"), _("Easy")) times = [] buttons = [] default = self._defaultEase() def but(label, i): if i == default: extra=" id=defease" else: extra = "" return ''' %s''' % (extra, i, label) for i in range(0, len(labels)): l = labels[i] l += "
%s" % self._buttonTime(i, default-1) buttons.append(but(l, i+1)) buf = ("
" + "".join(buttons) + "
") return "
" + buf + "
" return buf def _buttonTime(self, i, green): if self.mw.config['suppressEstimates']: return "" txt = self.mw.deck.sched.nextIvlStr(self.card, i+1, True) if i == 0: txt = '%s' % txt elif i == green: txt = '%s' % txt return txt # Answering a card ############################################################ def _answerCard(self, ease): "Reschedule card and show next." self.mw.deck.sched.answerCard(self.card, ease) self._answeredIds.append(self.card.id) print "fixme: save" self._getCard() # Handlers ############################################################ def _keyHandler(self, evt): if self.state == "question": show = False if evt.key() in (Qt.Key_Enter, Qt.Key_Return): show = True elif evt.key() == Qt.Key_Space and self.typeAns() is None: show = True if show: self._showAnswer() self.web.eval("showans();") return True elif self.state == "answer": if evt.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Space): self.web.eval("space();") else: key = unicode(evt.text()) if key and key >= "1" and key <= "4": key=int(key) if self.card.queue == 2 or key < 4: self._answerCard(key) return True def _linkHandler(self, url): if url == "ans": self._showAnswer() elif url.startswith("ease"): self._answerCard(int(url[4:])) elif url == "add": self.mw.onAddCard() elif url == "dlist": self.mw.close() elif url == "ov": self.mw.moveToState("overview") elif url.startswith("typeans:"): (cmd, arg) = url.split(":") self.processTypedAns(arg) else: QDesktopServices.openUrl(QUrl(url)) # CSS ########################################################################## _css = """ .ansbut { -webkit-box-shadow: 2px 2px 6px rgba(0,0,0,0.6); -webkit-user-drag: none; -webkit-user-select: none; background-color: #ddd; border-radius: 5px; border: 1px solid #aaa; color: #000; display: inline-block; font-size: 80%; margin: 0 5 0 5; padding: 3; text-decoration: none; text-align: center; } .but:focus, .but:hover { background-color: #aaa; } .ansbutbig { bottom: 1em; height: 40px; left: 50%; margin-left: -125px !important; position: fixed; width: 250px; font-size: 100%; } .ansbut:focus { font-weight: bold; } div.ansbuttxt { position: relative; top: 25%; } div#q, div#a { margin: 0px; } #easebuts { bottom: 1em; height: 47px; left: 50%; margin-left: -200px; position: fixed; width: 400px; font-size: 100%; } .easebut { width: 60px; font-size: 100%; } .time { background: #eee; padding: 5px; border-radius: 10px; } div#filler { height: 30px; } .q { margin-bottom: 1em; } .a { margin-top: 1em; } .inv { visibility: hidden; } """ def _styles(self): css = self.mw.sharedCSS css += self.mw.deck.allCSS() css += self._css css = runFilter("addStyles", css) return css # Type in the answer ########################################################################## failedCharColour = "#FF0000" passedCharColour = "#00FF00" def typeAns(self): "None if answer typing disabled." return self.card.template()['typeAns'] def typeAnsInput(self): if self.typeAns() is None: return "" return """
""" % ( self.getFont()) def processTypedAns(self, given): ord = self.typeAns() try: cor = self.mw.deck.media.strip( stripHTML(self.card.fact()._fields[ord])) except IndexError: self.card.template()['typeAns'] = None self.card.model().flush() cor = "" if cor: res = self.correct(cor, given) self.web.eval("proctypeans(%s);" % simplejson.dumps(res)) def getFont(self): f = self.card.model().fields[self.typeAns()] return (f['font'], f['qsize']) def calculateOkBadStyle(self): "Precalculates styles for correct and incorrect part of answer" (fn, sz) = self.getFont() st = "background: %s; color: #000; font-size: %dpx; font-family: %s;" self.styleOk = st % (self.passedCharColour, sz, fn) self.styleBad = st % (self.failedCharColour, sz, fn) def ok(self, a): "returns given sring in style correct (green)" if len(a) == 0: return "" return "%s" % (self.styleOk, a) def bad(self, a): "returns given sring in style incorrect (red)" if len(a) == 0: return "" return "%s" % (self.styleBad, a) def applyStyle(self, testChar, correct, wrong): "Calculates answer fragment depending on testChar's unicode category" ZERO_SIZE = 'Mn' def head(a): return a[:len(a) - 1] def tail(a): return a[len(a) - 1:] if ucd.category(testChar) == ZERO_SIZE: return self.ok(head(correct)) + self.bad(tail(correct) + wrong) return self.ok(correct) + self.bad(wrong) def correct(self, a, b): "Diff-corrects the typed-in answer." if b == "": return ""; self.calculateOkBadStyle() ret = "" lastEqual = "" s = difflib.SequenceMatcher(None, b, a) for tag, i1, i2, j1, j2 in s.get_opcodes(): if tag == "equal": lastEqual = b[i1:i2] elif tag == "replace": ret += self.applyStyle(b[i1], lastEqual, b[i1:i2] + ("-" * ((j2 - j1) - (i2 - i1)))) lastEqual = "" elif tag == "delete": ret += self.applyStyle(b[i1], lastEqual, b[i1:i2]) lastEqual = "" elif tag == "insert": dashNum = (j2 - j1) if ucd.category(a[j1]) != 'Mn' else ((j2 - j1) - 1) ret += self.applyStyle(a[j1], lastEqual, "-" * dashNum) lastEqual = "" return ret + self.ok(lastEqual) # Deck finished case ########################################################################## def _showCongrats(self): self.state = "congrats" self.card = None self.mw.deck.save() buf = """
%s

%s %s

""" % (self.mw.deck.sched.finishedMsg(), self.mw.button(key="o", name=_("Overview"), link="ov", id='ov'), self.mw.button(key="o", name=_("Deck List"), link="dlist")) self.web.stdHtml(buf, css=self.mw.sharedCSS) runHook('deckFinished') def drawDeckFinishedMessage(self): "Tell the user the deck is finished." # Deck empty case ########################################################################## def _showEmpty(self): self.state = "empty" buf = """

%(welcome)s

%(add)s
%(start)s

%(back)s
""" % \ {"welcome":_("Welcome to Anki!"), "add":_("Add Cards"), "start":_("Start adding your own material."), "back":_("Deck List"), } self.web.stdHtml(buf, css=self.mw.sharedCSS) # Status bar ########################################################################## def _setupStatus(self): self._statusWidgets = [] sb = self.mw.form.statusbar def addWgt(w, stretch=0): w.setShown(False) sb.addWidget(w, stretch) self._statusWidgets.append(w) def vertSep(): spacer = QFrame() spacer.setFrameStyle(QFrame.VLine) spacer.setFrameShadow(QFrame.Plain) spacer.setStyleSheet("* { color: #888; }") return spacer # left spacer space = QWidget() addWgt(space, 1) # remaining self.remText = QLabel() addWgt(self.remText, 0) # progress addWgt(vertSep()) class QClickableProgress(QProgressBar): url = "http://ichi2.net/anki/wiki/ProgressBars" def mouseReleaseEvent(self, evt): QDesktopServices.openUrl(QUrl(self.url)) progressBarSize = (50, 14) self.progressBar = QClickableProgress() self.progressBar.setFixedSize(*progressBarSize) self.progressBar.setMaximum(100) self.progressBar.setTextVisible(False) if QApplication.instance().style().objectName() != "plastique": self.plastiqueStyle = QStyleFactory.create("plastique") self.progressBar.setStyle(self.plastiqueStyle) addWgt(self.progressBar, 0) def _showStatus(self): self._showStatusWidgets(True) self._updateRemaining() self._updateProgress() def _hideStatus(self): self._showStatusWidgets(False) def _showStatusWidgets(self, shown=True): for w in self._statusWidgets: w.setShown(shown) self.mw.form.statusbar.hideOrShow() # fixme: only show progress for reviews, and only when revs due? def _updateRemaining(self): counts = list(self.mw.deck.sched.counts()) idx = self.mw.deck.sched.countIdx(self.card) counts[idx] = "%s" % (counts[idx]+1) space = " " * 2 ctxt = '%s' % counts[0] ctxt += space + '%s' % counts[1] ctxt += space + '%s' % counts[2] buf = _("Remaining: %s") % ctxt self.remText.setText(buf) def _updateProgress(self): p = QPalette() p.setColor(QPalette.Base, QColor("black")) p.setColor(QPalette.Button, QColor("black")) perc = 50 if perc == 0: p.setColor(QPalette.Highlight, QColor("black")) elif perc < 50: p.setColor(QPalette.Highlight, QColor("#ee0000")) elif perc < 65: p.setColor(QPalette.Highlight, QColor("#ee7700")) elif perc < 75: p.setColor(QPalette.Highlight, QColor("#eeee00")) else: p.setColor(QPalette.Highlight, QColor("#00ee00")) self.progressBar.setPalette(p) self.progressBar.setValue(perc) # Leeches ########################################################################## # fixme: update; clear on card transition def onLeech(self, card): print "leech" return txt = (_("""\ %s... is a leech.""") % stripHTML(stripSounds(self.currentCard.question)).\ replace("\n", " ")[0:30]) if isLeech and self.deck.db.scalar( "select 1 from cards where id = :id and type < 0", id=cardId): txt += _(" It has been suspended.") self.setNotice(txt)