Anki/aqt/clayout.py
ospalh 588feaa35b Add id to the text input field when editing the template.
Like this styling can be applied to id (or rather, the effect of styling to it can be seen in the layout editor.)
Also removed what looks like an extra '<center></center>'. That is added three lines later.
2013-01-18 16:46:25 +01:00

435 lines
15 KiB
Python

# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from aqt.qt import *
import re
from anki.consts import *
import aqt
from anki.sound import playFromText, clearAudioQueue
from aqt.utils import saveGeom, restoreGeom, getBase, mungeQA,\
showInfo, askUser, getOnlyText, \
showWarning, openHelp, openLink
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)
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 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.exec_()
def redraw(self):
self.cards = self.col.previewCards(self.note, 2)
self.redrawing = True
self.updateTabs()
self.redrawing = False
self.selectCard(self.ord)
def setupTabs(self):
c = self.connect
cloze = self.model['type'] == MODEL_CLOZE
self.tabs = QTabWidget()
self.tabs.setUsesScrollButtons(True)
if not cloze:
add = QPushButton("+")
add.setFixedWidth(30)
add.setToolTip(_("Add new card"))
c(add, SIGNAL("clicked()"), self.onAddCard)
self.tabs.setCornerWidget(add)
c(self.tabs, SIGNAL("currentChanged(int)"), self.selectCard)
def updateTabs(self):
self.forms = []
self.tabs.clear()
for t in self.model['tmpls']:
self.addTab(t)
def addTab(self, t):
c = self.connect
w = QWidget()
l = QHBoxLayout()
l.setMargin(0)
l.setSpacing(3)
left = QWidget()
# template area
tform = aqt.forms.template.Ui_Form()
tform.setupUi(left)
tform.label1.setText(u"")
tform.label2.setText(u"")
tform.labelc1.setText(u"")
tform.labelc2.setText(u"")
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)"))
c(tform.front, SIGNAL("textChanged()"), self.saveCard)
c(tform.css, SIGNAL("textChanged()"), self.saveCard)
c(tform.back, SIGNAL("textChanged()"), 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)
def linkClicked(url):
openLink(url)
for wig in pform.frontWeb, pform.backWeb:
wig.page().setLinkDelegationPolicy(
QWebPage.DelegateExternalLinks)
c(wig, SIGNAL("linkClicked(QUrl)"), linkClicked)
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 is required."))
if not askUser(_("Remove all cards of this type?")):
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):
c = self.connect
l = self.buttons = QHBoxLayout()
help = QPushButton(_("Help"))
help.setAutoDefault(False)
l.addWidget(help)
c(help, SIGNAL("clicked()"), self.onHelp)
l.addStretch()
addField = QPushButton(_("Add Field"))
addField.setAutoDefault(False)
l.addWidget(addField)
c(addField, SIGNAL("clicked()"), self.onAddField)
if self.model['type'] != MODEL_CLOZE:
flip = QPushButton(_("Flip"))
flip.setAutoDefault(False)
l.addWidget(flip)
c(flip, SIGNAL("clicked()"), self.onFlip)
more = QPushButton(_("More") + u"")
more.setAutoDefault(False)
l.addWidget(more)
c(more, SIGNAL("clicked()"), lambda: self.onMore(more))
l.addStretch()
close = QPushButton(_("Close"))
close.setAutoDefault(False)
l.addWidget(close)
c(close, SIGNAL("clicked()"), self.accept)
# Cards
##########################################################################
def selectCard(self, idx):
if self.redrawing:
return
self.ord = idx
if idx >= len(self.cards):
idx = len(self.cards) - 1
self.card = self.cards[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.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 = getBase(self.mw.col)
self.tab['pform'].frontWeb.stdHtml(
ti(mungeQA(c.q(reload=True))), self.mw.reviewer._styles(),
bodyClass="card card%d" % (c.ord+1), head=base,
js=anki.js.browserSel)
self.tab['pform'].backWeb.stdHtml(
ti(mungeQA(c.a()), type='a'), self.mw.reviewer._styles(),
bodyClass="card card%d" % (c.ord+1), head=base,
js=anki.js.browserSel)
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 == 'q':
repl = "<input id='typeans' type=text value=''>"
else:
repl = _("(typing comparison appears here)")
repl = "<center>%s</center>" % repl
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<br>\n%s" % (_("Edit to customize"), old['qfmt'])
t['afmt'] = old['afmt']
self.mm.addTemplate(self.model, t)
self.redraw()
self.selectCard(t['ord'])
def onFlip(self):
old = self.card.template()
self._flipQA(old, old)
self.redraw()
def _flipQA(self, src, dst):
m = re.match("(?s)(.+)<hr id=answer>(.+)", 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<hr id=answer>\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.connect(a, SIGNAL("triggered()"),
self.onRename)
if self.model['type'] != MODEL_CLOZE:
a = m.addAction(_("Reposition"))
a.connect(a, SIGNAL("triggered()"),
self.onReorder)
t = self.card.template()
if t['did']:
s = _(" (on)")
else:
s = _(" (off)")
a = m.addAction(_("Deck Override") + s)
a.connect(a, SIGNAL("triggered()"),
self.onTargetDeck)
a = m.addAction(_("Browser Appearance"))
a.connect(a, SIGNAL("triggered()"),
self.onBrowserDisplay)
if self.model['type'] != MODEL_CLOZE:
a = m.addAction(_("Delete"))
a.connect(a, SIGNAL("triggered()"),
lambda: self.onRemoveTab(self.tabs.currentIndex()))
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))
d.connect(f.buttonBox, SIGNAL("accepted()"),
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)
self.connect(bb, SIGNAL("rejected()"), d, SLOT("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<div style='font-family: %s; font-size: %spx;'>{{%s}}</div>\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")