# -*- coding: utf-8 -*- # Copyright: Damien Elmes # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 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 anki.js 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.setupTabs() self.setupButtons() self.setWindowTitle(_("Card Types for %s") % self.model['name']) v1 = QVBoxLayout() v1.addWidget(self.tabs) v1.addLayout(self.buttons) self.setLayout(v1) self.redraw() restoreGeom(self, "CardLayout") self.setWindowModality(Qt.ApplicationModal) self.show() def redraw(self): self.cards = self.col.previewCards(self.note, 2) self.redrawing = True self.updateTabs() self.redrawing = False idx = self.ord if idx >= len(self.cards): idx = len(self.cards) - 1 self.selectCard(idx) def setupTabs(self): cloze = self.model['type'] == MODEL_CLOZE self.tabs = QTabWidget() self.tabs.setTabsClosable(not cloze) self.tabs.setUsesScrollButtons(True) if not cloze: add = QPushButton("+") add.setFixedWidth(30) add.setToolTip(_("Add new card")) add.clicked.connect(self.onAddCard) self.tabs.setCornerWidget(add) self.tabs.currentChanged.connect(self.onCardSelected) self.tabs.tabCloseRequested.connect(self.onRemoveTab) def updateTabs(self): self.forms = [] self.tabs.clear() for t in self.model['tmpls']: self.addTab(t) def addTab(self, t): w = QWidget() l = QHBoxLayout() l.setContentsMargins(0,0,0,0) l.setSpacing(3) left = QWidget() # template area 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) if len(self.cards) > 1: 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 = 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) # for cloze notes, show that it's one of n cards if self.model['type'] == MODEL_CLOZE: cnt = len(self.mm.availOrds( self.model, joinFields(self.note.fields))) for g in pform.groupBox, pform.groupBox_2: g.setTitle(g.title() + _(" (1 of %d)") % max(cnt, 1)) pform.frontWeb = AnkiWebView() pform.frontPrevBox.addWidget(pform.frontWeb) pform.backWeb = AnkiWebView() pform.backPrevBox.addWidget(pform.backWeb) l.addWidget(right, 5) w.setLayout(l) self.forms.append({'tform': tform, 'pform': pform}) self.tabs.addTab(w, t['name']) def onRemoveTab(self, idx): if len(self.model['tmpls']) < 2: return showInfo(_("At least one card type is required.")) 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 self.model['type'] != MODEL_CLOZE: flip = QPushButton(_("Flip")) flip.setAutoDefault(False) l.addWidget(flip) flip.clicked.connect(self.onFlip) more = QPushButton(_("More") + " "+downArrow()) more.setAutoDefault(False) l.addWidget(more) more.clicked.connect(lambda: self.onMore(more)) l.addStretch() close = QPushButton(_("Close")) close.setAutoDefault(False) l.addWidget(close) close.clicked.connect(self.accept) # Cards ########################################################################## def selectCard(self, idx): if self.tabs.currentIndex() == idx: # trigger a re-read self.onCardSelected(idx) else: self.tabs.setCurrentIndex(idx) def onCardSelected(self, idx): if self.redrawing: return self.card = self.cards[idx] self.ord = idx self.tab = self.forms[idx] self.tabs.setCurrentIndex(idx) self.playedAudio = {} self.readCard() self.renderPreview() def readCard(self): t = self.card.template() self.redrawing = True self.tab['tform'].front.setPlainText(t['qfmt']) self.tab['tform'].css.setPlainText(self.model['css']) self.tab['tform'].back.setPlainText(t['afmt']) self.tab['tform'].front.setAcceptRichText(False) self.tab['tform'].css.setAcceptRichText(False) self.tab['tform'].back.setAcceptRichText(False) self.tab['tform'].front.setTabStopWidth(30) self.tab['tform'].css.setTabStopWidth(30) self.tab['tform'].back.setTabStopWidth(30) self.redrawing = False def saveCard(self): if self.redrawing: return text = self.tab['tform'].front.toPlainText() self.card.template()['qfmt'] = text text = self.tab['tform'].css.toPlainText() self.card.model()['css'] = text text = self.tab['tform'].back.toPlainText() self.card.template()['afmt'] = text self.renderPreview() # Preview ########################################################################## def renderPreview(self): c = self.card ti = self.maybeTextInput base = self.mw.baseHTML() self.tab['pform'].frontWeb.setEnabled(False) self.tab['pform'].backWeb.setEnabled(False) self.tab['pform'].frontWeb.stdHtml( ti(mungeQA(self.mw.col, c.q(reload=True))), self.mw.reviewer._styles(), bodyClass="card card%d" % (c.ord+1), head=base), self.tab['pform'].backWeb.stdHtml( ti(mungeQA(self.mw.col, c.a()), type='a'), self.mw.reviewer._styles(), bodyClass="card card%d" % (c.ord+1), head=base), self.tab['pform'].frontWeb.setEnabled(True) self.tab['pform'].backWeb.setEnabled(True) clearAudioQueue() if c.id not in self.playedAudio: playFromText(c.q()) playFromText(c.a()) self.playedAudio[c.id] = True 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.tabs.setTabText(self.tabs.currentIndex(), name) 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): name = self._newCardName() t = self.mm.newTemplate(name) old = self.card.template() t['qfmt'] = "%s
\n%s" % (_("Edit to customize"), 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, button): m = QMenu(self) a = m.addAction(_("Rename")) a.triggered.connect(self.onRename) if self.model['type'] != MODEL_CLOZE: a = m.addAction(_("Reposition")) a.triggered.connect(self.onReorder) 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_(button.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', "")) 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() t['bfont'] = f.font.currentFont().family() t['bsize'] = f.fontSize.value() 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.tab['tform'].front else: obj = self.tab['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): 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")