# -*- 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.consts import NEW_CARDS_RANDOM from anki.utils import fmtTimeSpan, stripHTML from anki.hooks import addHook, runHook, runFilter from anki.sound import playFromText from aqt.utils import mungeQA, getBase import aqt failedCharColour = "#FF0000" passedCharColour = "#00FF00" futureWarningColour = "#FF0000" class Reviewer(object): "Manage reviews. Maintains a separate state." def __init__(self, mw): self.mw = mw self.web = mw.web self._state = None # self.main.connect(self.body, SIGNAL("loadFinished(bool)"), # self.onLoadFinished) def show(self): self._setupToolbar() self._reset() # State control ########################################################################## def _reset(self): pass def setState(self, state): "Change to STATE, and update the display." self.oldState = getattr(self, 'state', None) self.state = state if self.state == "initial": return elif self.state == "deckBrowser": self.clearWindow() self.drawWelcomeMessage() self.flush() return self.redisplay() def redisplay(self): "Idempotently display the current state (prompt for question, etc)" if self.state == "deckBrowser" or self.state == "studyScreen": return self.buffer = "" self.haveTop = self.needFutureWarning() self.drawRule = (self.main.config['qaDivider'] and self.main.currentCard and not self.main.currentCard.cardModel.questionInAnswer) if not self.main.deck.isEmpty(): if self.haveTop: self.drawTopSection() if self.state == "showQuestion": self.setBackground() self.drawQuestion() if self.drawRule: self.write("
") elif self.state == "showAnswer": self.setBackground() if not self.main.currentCard.cardModel.questionInAnswer: self.drawQuestion(nosound=True) if self.drawRule: self.write("
") self.drawAnswer() elif self.state == "deckEmpty": self.drawWelcomeMessage() elif self.state == "deckFinished": self.drawDeckFinishedMessage() self.flush() def addStyles(self): # card styles s = "" return s def clearWindow(self): self.body.setHtml("") self.buffer = "" def setBackground(self): col = self.main.currentCard.cardModel.lastFontColour self.write("" % col) def _getQuestionState(self, oldState): # stop anything playing clearAudioQueue() if self.deck.isEmpty(): return self.moveToState("deckEmpty") else: # timeboxing only supported using the standard scheduler if not self.deck.finishScheduler: if self.config['showStudyScreen']: if not self.deck.timeboxStarted(): return self.moveToState("studyScreen") elif self.deck.timeboxReached(): self.showToolTip(_("Session limit reached.")) self.moveToState("studyScreen") # switch to timeboxing screen self.form.tabWidget.setCurrentIndex(2) return if not self.currentCard: self.currentCard = self.deck.getCard() if self.currentCard: if self.lastCard: if self.lastCard.id == self.currentCard.id: pass # if self.currentCard.combinedDue > time.time(): # # if the same card is being shown and it's not # # due yet, give up # return self.moveToState("deckFinished") self.enableCardMenuItems() return self.moveToState("showQuestion") else: return self.moveToState("deckFinished") def _deckEmptyState(self, oldState): self.switchToWelcomeScreen() self.disableCardMenuItems() def _deckFinishedState(self, oldState): self.currentCard = None self.deck.db.flush() self.hideButtons() self.disableCardMenuItems() self.switchToCongratsScreen() self.form.learnMoreButton.setEnabled( not not self.deck.newAvail) self.startRefreshTimer() self.bodyView.setState(state) # focus finish button self.form.finishButton.setFocus() runHook('deckFinished') def _showQuestionState(self, oldState): # ensure cwd set to media dir self.deck.mediaDir() self.showAnswerButton() self.updateMarkAction() runHook('showQuestion') def _showAnswerState(self, oldState): self.showEaseButtons() # Font properties & output ########################################################################## def flush(self): "Write the current HTML buffer to the screen." self.buffer = self.addStyles() + self.buffer # hook for user css runHook("preFlushHook") self.buffer = '''%s%s''' % ( getBase(self.main.deck, self.main.currentCard), self.buffer) #print self.buffer.encode("utf-8") b = self.buffer # Feeding webkit unicode can result in it not finding images, so on # linux/osx we percent escape the image paths as utf8. On Windows the # problem is more complicated - if we percent-escape as utf8 it fixes # some images but breaks others. When filenames are normalized by # dropbox they become unreadable if we escape them. if not sys.platform.startswith("win32") and self.main.deck: # and self.main.config['mediaLocation'] == "dropbox"): b = self.main.deck.media.escapeImages(b) self.body.setHtml(b) def write(self, text): if type(text) != types.UnicodeType: text = unicode(text, "utf-8") self.buffer += text # Question and answer ########################################################################## def center(self, str, height=40): if not self.main.config['splitQA']: return "
" + str + "
" return '''\
\
\
%s
''' % (height, str) def drawQuestion(self, nosound=False): "Show the question." if not self.main.config['splitQA']: self.write("
") q = self.main.currentCard.htmlQuestion() if self.haveTop: height = 35 elif self.main.currentCard.cardModel.questionInAnswer: height = 40 else: height = 45 q = runFilter("drawQuestion", q, self.main.currentCard) self.write(self.center(self.mungeQA(self.main.deck, q), height)) if (self.state != self.oldState and not nosound and self.main.config['autoplaySounds']): playFromText(q) if self.main.currentCard.cardModel.typeAnswer: self.adjustInputFont() def getFont(self): sz = 20 fn = u"Arial" for fm in self.main.currentCard.fact.model.fieldModels: if fm.name == self.main.currentCard.cardModel.typeAnswer: sz = fm.quizFontSize or sz fn = fm.quizFontFamily or fn break return (fn, sz) def adjustInputFont(self): (fn, sz) = self.getFont() f = QFont() f.setFamily(fn) f.setPixelSize(sz) self.main.typeAnswerField.setFont(f) # add some extra space as layout is wrong on osx self.main.typeAnswerField.setFixedHeight( self.main.typeAnswerField.sizeHint().height() + 10) 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 % (passedCharColour, sz, fn) self.styleBad = st % (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 head(self, a): return a[:len(a) - 1] def tail(self, a): return a[len(a) - 1:] def applyStyle(self, testChar, correct, wrong): "Calculates answer fragment depending on testChar's unicode category" ZERO_SIZE = 'Mn' if ucd.category(testChar) == ZERO_SIZE: return self.ok(self.head(correct)) + self.bad(self.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) def drawAnswer(self): "Show the answer." a = self.main.currentCard.htmlAnswer() a = runFilter("drawAnswer", a, self.main.currentCard) if self.main.currentCard.cardModel.typeAnswer: try: cor = stripMedia(stripHTML(self.main.currentCard.fact[ self.main.currentCard.cardModel.typeAnswer])) except KeyError: self.main.currentCard.cardModel.typeAnswer = "" cor = "" if cor: given = unicode(self.main.typeAnswerField.text()) res = self.correct(cor, given) a = res + "
" + a self.write(self.center('' + self.mungeQA(self.main.deck, a))) if self.state != self.oldState and self.main.config['autoplaySounds']: playFromText(a) def mungeQA(self, deck, txt): txt = mungeQA(deck, txt) return txt def onLoadFinished(self, bool): if self.state == "showAnswer": if self.main.config['scrollToAnswer']: mf = self.body.page().mainFrame() mf.evaluateJavaScript("location.hash = 'answer'") # Top section ########################################################################## def drawTopSection(self): "Show previous card, next scheduled time, and stats." self.buffer += "
" self.drawFutureWarning() self.buffer += "
" def needFutureWarning(self): if not self.main.currentCard: return if self.main.currentCard.due <= self.main.deck.dueCutoff: return if self.main.currentCard.due - time.time() <= self.main.deck.delay0: return if self.main.deck.scheduler == "cram": return return True def drawFutureWarning(self): if not self.needFutureWarning(): return self.write("" % futureWarningColour + _("This card was due in %s.") % fmtTimeSpan( self.main.currentCard.due - time.time(), after=True) + "") # Welcome/empty/finished deck messages ########################################################################## def drawDeckFinishedMessage(self): "Tell the user the deck is finished." self.main.mainWin.congratsLabel.setText( self.main.deck.deckFinishedMsg()) # Toolbar ########################################################################## def _setupToolbar(self): if not self.mw.config['showToolbar']: return self.mw.form.toolBar.show()