# -*- coding: utf-8 -*- # Copyright: Damien Elmes # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import collections import re from aqt.qt import * from anki.consts import * import aqt from anki.sound import playFromText, clearAudioQueue from aqt.utils import saveGeom, restoreGeom, mungeQA,\ showInfo, askUser, getOnlyText, \ showWarning, openHelp, downArrow from anki.utils import isMac, isWin, joinFields from aqt.webview import AnkiWebView import json class CardLayout(QDialog): def __init__(self, mw, note, ord=0, parent=None, addMode=False): QDialog.__init__(self, parent or mw, Qt.Window) mw.setupDialogGC(self) self.mw = aqt.mw self.parent = parent or mw self.note = note self.ord = ord self.col = self.mw.col self.mm = self.mw.col.models self.model = note.model() self.mw.checkpoint(_("Card Types")) self.addMode = addMode if addMode: # save it to DB temporarily self.emptyFields = [] for name, val in list(note.items()): if val.strip(): continue self.emptyFields.append(name) note[name] = "(%s)" % name note.flush() self.setupTopArea() self.setupMainArea() self.setupButtons() self.setWindowTitle(_("Card Types for %s") % self.model['name']) v1 = QVBoxLayout() v1.addWidget(self.topArea) v1.addWidget(self.mainArea) v1.addLayout(self.buttons) v1.setContentsMargins(12,12,12,12) self.setLayout(v1) self.redraw() restoreGeom(self, "CardLayout") self.setWindowModality(Qt.ApplicationModal) self.show() # take the focus away from the first input area when starting up, # as users tend to accidentally type into the template self.setFocus() def redraw(self): self.cards = self.col.previewCards(self.note, 2) idx = self.ord if idx >= len(self.cards): self.ord = len(self.cards) - 1 self.redrawing = True self.updateTopArea() self.updateMainArea() self.redrawing = False self.onCardSelected(self.ord) def setupTopArea(self): self.topArea = QWidget() self.topAreaForm = aqt.forms.clayout_top.Ui_Form() self.topAreaForm.setupUi(self.topArea) self.topAreaForm.templateOptions.setText(_("Options") + " "+downArrow()) self.topAreaForm.templateOptions.clicked.connect(self.onMore) self.topAreaForm.templatesBox.currentIndexChanged.connect(self.onCardSelected) def updateTopArea(self): cnt = self.mw.col.models.useCount(self.model) self.topAreaForm.changesLabel.setText(ngettext( "Changes below will affect the %(cnt)d note that uses this card type.", "Changes below will affect the %(cnt)d notes that use this card type.", cnt) % dict(cnt=cnt)) self.updateCardNames() def updateCardNames(self): self.redrawing = True combo = self.topAreaForm.templatesBox combo.clear() combo.addItems(self._templateNameIncludingOrdinal(t) for t in self.model['tmpls']) combo.setCurrentIndex(self.ord) combo.setVisible(not self._isCloze()) self.redrawing = False def _templateNameIncludingOrdinal(self, tmpl): return _("Card Type %(n)d of %(total)d: %(name)s") % dict( n=tmpl['ord']+1, total=len(self.model['tmpls']), name=self._summarizedName(tmpl), ) def _summarizedName(self, tmpl): return "{}: {} -> {}".format( tmpl['name'], self._fieldsOnTemplate(tmpl['qfmt']), self._fieldsOnTemplate(tmpl['afmt'])) def _fieldsOnTemplate(self, fmt): matches = re.findall("{{[^#/}]+?}}", fmt) result = collections.OrderedDict() for m in matches: # strip off mustache m = re.sub(r"[{}]", "", m) # strip off modifiers m = m.split(":")[-1] # don't show 'FrontSide' if m == "FrontSide": continue result[m] = True return "+".join(result.keys()) def _isCloze(self): return self.model['type'] == MODEL_CLOZE def setupMainArea(self): w = self.mainArea = QWidget() l = QHBoxLayout() l.setContentsMargins(0,0,0,0) l.setSpacing(3) left = QWidget() # template area tform = self.tform = aqt.forms.template.Ui_Form() tform.setupUi(left) tform.label1.setText(" →") tform.label2.setText(" →") tform.labelc1.setText(" ↗") tform.labelc2.setText(" ↘") if self.style().objectName() == "gtk+": # gtk+ requires margins in inner layout tform.tlayout1.setContentsMargins(0, 11, 0, 0) tform.tlayout2.setContentsMargins(0, 11, 0, 0) tform.tlayout3.setContentsMargins(0, 11, 0, 0) tform.groupBox_3.setTitle(_( "Styling (shared between cards)")) tform.front.textChanged.connect(self.saveCard) tform.css.textChanged.connect(self.saveCard) tform.back.textChanged.connect(self.saveCard) l.addWidget(left, 5) # preview area right = QWidget() pform = self.pform = aqt.forms.preview.Ui_Form() pform.setupUi(right) if self.style().objectName() == "gtk+": # gtk+ requires margins in inner layout pform.frontPrevBox.setContentsMargins(0, 11, 0, 0) pform.backPrevBox.setContentsMargins(0, 11, 0, 0) self.setupWebviews() l.addWidget(right, 5) w.setLayout(l) def setupWebviews(self): pform = self.pform pform.frontWeb = AnkiWebView() pform.frontPrevBox.addWidget(pform.frontWeb) pform.backWeb = AnkiWebView() pform.backPrevBox.addWidget(pform.backWeb) jsinc = ["jquery.js","browsersel.js", "mathjax/conf.js", "mathjax/MathJax.js", "reviewer.js"] pform.frontWeb.stdHtml(self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc) pform.backWeb.stdHtml(self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc) def updateMainArea(self): if self._isCloze(): cnt = len(self.mm.availOrds( self.model, joinFields(self.note.fields))) for g in self.pform.groupBox, self.pform.groupBox_2: g.setTitle(g.title() + _(" (1 of %d)") % max(cnt, 1)) def onRemove(self): if len(self.model['tmpls']) < 2: return showInfo(_("At least one card type is required.")) idx = self.ord cards = self.mm.tmplUseCount(self.model, idx) cards = ngettext("%d card", "%d cards", cards) % cards msg = (_("Delete the '%(a)s' card type, and its %(b)s?") % dict(a=self.model['tmpls'][idx]['name'], b=cards)) if not askUser(msg): return if not self.mm.remTemplate(self.model, self.cards[idx].template()): return showWarning(_("""\ Removing this card type would cause one or more notes to be deleted. \ Please create a new card type first.""")) self.redraw() # Buttons ########################################################################## def setupButtons(self): l = self.buttons = QHBoxLayout() help = QPushButton(_("Help")) help.setAutoDefault(False) l.addWidget(help) help.clicked.connect(self.onHelp) l.addStretch() addField = QPushButton(_("Add Field")) addField.setAutoDefault(False) l.addWidget(addField) addField.clicked.connect(self.onAddField) if not self._isCloze(): flip = QPushButton(_("Flip")) flip.setAutoDefault(False) l.addWidget(flip) flip.clicked.connect(self.onFlip) l.addStretch() close = QPushButton(_("Close")) close.setAutoDefault(False) l.addWidget(close) close.clicked.connect(self.accept) # Cards ########################################################################## def onCardSelected(self, idx): if self.redrawing: return self.card = self.cards[idx] self.ord = idx self.playedAudio = {} self.readCard() self.renderPreview() def readCard(self): t = self.card.template() self.redrawing = True self.tform.front.setPlainText(t['qfmt']) self.tform.css.setPlainText(self.model['css']) self.tform.back.setPlainText(t['afmt']) self.tform.front.setAcceptRichText(False) self.tform.css.setAcceptRichText(False) self.tform.back.setAcceptRichText(False) self.tform.front.setTabStopWidth(30) self.tform.css.setTabStopWidth(30) self.tform.back.setTabStopWidth(30) self.redrawing = False def saveCard(self): if self.redrawing: return text = self.tform.front.toPlainText() self.card.template()['qfmt'] = text text = self.tform.css.toPlainText() self.card.model()['css'] = text text = self.tform.back.toPlainText() self.card.template()['afmt'] = text self.renderPreview() # Preview ########################################################################## _previewTimer = None def renderPreview(self): # schedule a preview when timing stops self.cancelPreviewTimer() self._previewTimer = self.mw.progress.timer(500, self._renderPreview, False) def cancelPreviewTimer(self): if self._previewTimer: self._previewTimer.stop() self._previewTimer = None def _renderPreview(self): self.cancelPreviewTimer() c = self.card ti = self.maybeTextInput bodyclass="card card%d" % (c.ord+1) q = ti(mungeQA(self.mw.col, c.q(reload=True))) a = ti(mungeQA(self.mw.col, c.a()), type='a') # use _showAnswer to avoid the longer delay self.pform.frontWeb.eval("_showAnswer(%s,'%s');" % (json.dumps(q), bodyclass)) self.pform.backWeb.eval("_showAnswer(%s, '%s');" % (json.dumps(a), bodyclass)) clearAudioQueue() if c.id not in self.playedAudio: playFromText(c.q()) playFromText(c.a()) self.playedAudio[c.id] = True self.updateCardNames() def maybeTextInput(self, txt, type='q'): if "[[type:" not in txt: return txt origLen = len(txt) txt = txt.replace("
", "") hadHR = origLen != len(txt) def answerRepl(match): res = self.mw.reviewer.correct("exomple", "an example") if hadHR: res = "
" + res return res if type == 'q': repl = "" repl = "
%s
" % repl else: repl = answerRepl return re.sub("\[\[type:.+?\]\]", repl, txt) # Card operations ###################################################################### def onRename(self): name = getOnlyText(_("New name:"), default=self.card.template()['name']) if not name: return if name in [c.template()['name'] for c in self.cards if c.template()['ord'] != self.ord]: return showWarning(_("That name is already used.")) self.card.template()['name'] = name self.redraw() def onReorder(self): n = len(self.cards) cur = self.card.template()['ord']+1 pos = getOnlyText( _("Enter new card position (1...%s):") % n, default=str(cur)) if not pos: return try: pos = int(pos) except ValueError: return if pos < 1 or pos > n: return if pos == cur: return pos -= 1 self.mm.moveTemplate(self.model, self.card.template(), pos) self.ord = pos self.redraw() def _newCardName(self): n = len(self.cards) + 1 while 1: name = _("Card %d") % n if name not in [c.template()['name'] for c in self.cards]: break n += 1 return name def onAddCard(self): cnt = self.mw.col.models.useCount(self.model) txt = ngettext("This will create %d card. Proceed?", "This will create %d cards. Proceed?", cnt) % cnt if not askUser(txt): return name = self._newCardName() t = self.mm.newTemplate(name) old = self.card.template() t['qfmt'] = old['qfmt'] t['afmt'] = old['afmt'] self.mm.addTemplate(self.model, t) self.ord = len(self.cards) self.redraw() def onFlip(self): old = self.card.template() self._flipQA(old, old) self.redraw() def _flipQA(self, src, dst): m = re.match("(?s)(.+)
(.+)", src['afmt']) if not m: showInfo(_("""\ Anki couldn't find the line between the question and answer. Please \ adjust the template manually to switch the question and answer.""")) return dst['afmt'] = "{{FrontSide}}\n\n
\n\n%s" % src['qfmt'] dst['qfmt'] = m.group(2).strip() return True def onMore(self): m = QMenu(self) if not self._isCloze(): a = m.addAction(_("Add Card Type...")) a.triggered.connect(self.onAddCard) a = m.addAction(_("Remove Card Type...")) a.triggered.connect(self.onRemove) a = m.addAction(_("Rename Card Type...")) a.triggered.connect(self.onRename) a = m.addAction(_("Reposition Card Type...")) a.triggered.connect(self.onReorder) m.addSeparator() t = self.card.template() if t['did']: s = _(" (on)") else: s = _(" (off)") a = m.addAction(_("Deck Override...") + s) a.triggered.connect(self.onTargetDeck) a = m.addAction(_("Browser Appearance...")) a.triggered.connect(self.onBrowserDisplay) m.exec_(self.topAreaForm.templateOptions.mapToGlobal(QPoint(0,0))) def onBrowserDisplay(self): d = QDialog() f = aqt.forms.browserdisp.Ui_Dialog() f.setupUi(d) t = self.card.template() f.qfmt.setText(t.get('bqfmt', "")) f.afmt.setText(t.get('bafmt', "")) if t.get("bfont"): f.overrideFont.setChecked(True) f.font.setCurrentFont(QFont(t.get('bfont', "Arial"))) f.fontSize.setValue(t.get('bsize', 12)) f.buttonBox.accepted.connect(lambda: self.onBrowserDisplayOk(f)) d.exec_() def onBrowserDisplayOk(self, f): t = self.card.template() t['bqfmt'] = f.qfmt.text().strip() t['bafmt'] = f.afmt.text().strip() if f.overrideFont.isChecked(): t['bfont'] = f.font.currentFont().family() t['bsize'] = f.fontSize.value() else: del t['bfont'] del t['bsize'] def onTargetDeck(self): from aqt.tagedit import TagEdit t = self.card.template() d = QDialog(self) d.setWindowTitle("Anki") d.setMinimumWidth(400) l = QVBoxLayout() lab = QLabel(_("""\ Enter deck to place new %s cards in, or leave blank:""") % self.card.template()['name']) lab.setWordWrap(True) l.addWidget(lab) te = TagEdit(d, type=1) te.setCol(self.col) l.addWidget(te) if t['did']: te.setText(self.col.decks.get(t['did'])['name']) te.selectAll() bb = QDialogButtonBox(QDialogButtonBox.Close) bb.rejected.connect(d.close) l.addWidget(bb) d.setLayout(l) d.exec_() if not te.text().strip(): t['did'] = None else: t['did'] = self.col.decks.id(te.text()) def onAddField(self): diag = QDialog(self) form = aqt.forms.addfield.Ui_Dialog() form.setupUi(diag) fields = [f['name'] for f in self.model['flds']] form.fields.addItems(fields) form.font.setCurrentFont(QFont("Arial")) form.size.setValue(20) diag.show() # Work around a Qt bug, # https://bugreports.qt-project.org/browse/QTBUG-1894 if isMac or isWin: # No problems on Macs or Windows. form.fields.showPopup() else: # Delay showing the pop-up. self.mw.progress.timer(200, form.fields.showPopup, False) if not diag.exec_(): return if form.radioQ.isChecked(): obj = self.tform.front else: obj = self.tform.back self._addField(obj, fields[form.fields.currentIndex()], form.font.currentFont().family(), form.size.value()) def _addField(self, widg, field, font, size): t = widg.toPlainText() t +="\n
{{%s}}
\n" % ( font, size, field) widg.setPlainText(t) self.saveCard() # Closing & Help ###################################################################### def accept(self): self.reject() def reject(self): self.cancelPreviewTimer() clearAudioQueue() if self.addMode: # remove the filler fields we added for name in self.emptyFields: self.note[name] = "" self.mw.col.db.execute("delete from notes where id = ?", self.note.id) self.mm.save(self.model, templates=True) self.mw.reset() saveGeom(self, "CardLayout") return QDialog.reject(self) def onHelp(self): openHelp("templates")