mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00

- moved progress handling into separate progress.py - moved deck browser code into separate deckbrowser.py - started reworking the state code; views will be rolled into this in the future - the main window has been stripped of the study options, inline editor, congrats screen and so on, and now consists of a single main widget which has a webview placed inside it. The stripped features will be implemented either in separate windows, or as part of the web view
553 lines
21 KiB
Python
553 lines
21 KiB
Python
# Copyright: Damien Elmes <anki@ichi2.net>
|
|
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
|
|
|
|
from PyQt4.QtGui import *
|
|
from PyQt4.QtCore import *
|
|
from PyQt4.QtWebKit import QWebPage, QWebView
|
|
import sys, re
|
|
import aqt.forms
|
|
import anki
|
|
from anki.models import *
|
|
from anki.facts import *
|
|
from anki.cards import Card
|
|
from anki.sound import playFromText, clearAudioQueue
|
|
from aqt.ui.utils import saveGeom, restoreGeom, getBase, mungeQA, \
|
|
saveSplitter, restoreSplitter
|
|
from anki.hooks import runFilter
|
|
from aqt import ui
|
|
|
|
class ResizingTextEdit(QTextEdit):
|
|
def sizeHint(self):
|
|
return QSize(200, 800)
|
|
|
|
class CardLayout(QDialog):
|
|
|
|
def __init__(self, parent, factedit, factOrModel, card=None):
|
|
self.parent = parent
|
|
QDialog.__init__(self, parent, Qt.Window)
|
|
self.mw = aqt.mw
|
|
self.deck = self.mw.deck
|
|
self.factedit = factedit
|
|
self.card = card
|
|
if factedit is not None:
|
|
self.fact = factOrModel
|
|
self.model = self.fact.model
|
|
else:
|
|
self.model = factOrModel
|
|
# see if there's an available fact
|
|
id = self.deck.db.scalar(
|
|
"select id from facts where modelId = :id", id=self.model.id)
|
|
if id:
|
|
self.fact = self.deck.db.query(Fact).get(id)
|
|
else:
|
|
# generate a dummy one
|
|
self.fact = self.deck.newFact(self.model)
|
|
for f in self.fact.keys():
|
|
self.fact[f] = f
|
|
self.plastiqueStyle = None
|
|
if (sys.platform.startswith("darwin") or
|
|
sys.platform.startswith("win32")):
|
|
self.plastiqueStyle = QStyleFactory.create("plastique")
|
|
if self.card:
|
|
# limited to an existing template
|
|
self.cards = [self.deck.db.query(Card).get(id) for id in
|
|
self.deck.db.column0(
|
|
"select id from cards where factId = :fid "
|
|
"order by ordinal", fid=self.fact.id)]
|
|
type = 0
|
|
else:
|
|
if factedit:
|
|
# active & possible
|
|
self.cards = self.deck.previewFact(self.fact)
|
|
type = 1
|
|
else:
|
|
# all
|
|
self.cards = self.deck.previewFact(self.fact, cms=self.model.cardModels)
|
|
type = 2
|
|
if not self.cards:
|
|
ui.utils.showInfo(_(
|
|
"Please enter some text first."),
|
|
parent=self.parent)
|
|
return
|
|
self.form = aqt.forms.clayout.Ui_Dialog()
|
|
self.form.setupUi(self)
|
|
restoreSplitter(self.form.splitter, "clayout")
|
|
if type == 0:
|
|
self.form.templateType.setText(
|
|
_("Templates used by fact:"))
|
|
elif type == 1:
|
|
self.form.templateType.setText(
|
|
_("Templates that will be created:"))
|
|
else:
|
|
self.form.templateType.setText(
|
|
_("All templates:"))
|
|
# FIXME: add this
|
|
self.form.editTemplates.hide()
|
|
self.connect(self.form.buttonBox, SIGNAL("helpRequested()"),
|
|
self.onHelp)
|
|
self.setupCards()
|
|
self.setupFields()
|
|
restoreGeom(self, "CardLayout")
|
|
# hack to ensure we're focused on the active template in the model
|
|
# properties
|
|
if type == 2 and factOrModel.currentCard.ordinal != 0:
|
|
idx = factOrModel.currentCard.ordinal
|
|
self.form.cardList.setCurrentIndex(idx)
|
|
self.cardChanged(idx)
|
|
self.exec_()
|
|
|
|
# Cards & Preview
|
|
##########################################################################
|
|
|
|
def setupCards(self):
|
|
self.needFormatRebuild = False
|
|
self.updatingCards = False
|
|
self.playedAudio = {}
|
|
# replace with more appropriate size hints
|
|
for e in ("cardQuestion", "cardAnswer"):
|
|
w = getattr(self.form, e)
|
|
idx = self.form.templateLayout.indexOf(w)
|
|
r = self.form.templateLayout.getItemPosition(idx)
|
|
self.form.templateLayout.removeWidget(w)
|
|
w.hide()
|
|
w.deleteLater()
|
|
w = ResizingTextEdit(self)
|
|
setattr(self.form, e, w)
|
|
self.form.templateLayout.addWidget(w, r[0], r[1])
|
|
self.connect(self.form.cardList, SIGNAL("activated(int)"),
|
|
self.cardChanged)
|
|
# self.connect(self.form.editTemplates, SIGNAL("clicked())"),
|
|
# self.onEdit)
|
|
self.connect(self.form.cardQuestion, SIGNAL("textChanged()"),
|
|
lambda: self.formatChanged("question"))
|
|
self.connect(self.form.cardAnswer, SIGNAL("textChanged()"),
|
|
lambda: self.formatChanged("answer"))
|
|
self.connect(self.form.alignment,
|
|
SIGNAL("activated(int)"),
|
|
self.saveCard)
|
|
self.connect(self.form.background,
|
|
SIGNAL("clicked()"),
|
|
lambda w=self.form.background:\
|
|
self.chooseColour(w, "card"))
|
|
self.connect(self.form.questionInAnswer,
|
|
SIGNAL("clicked()"), self.saveCard)
|
|
self.connect(self.form.allowEmptyAnswer,
|
|
SIGNAL("clicked()"), self.saveCard)
|
|
self.connect(self.form.typeAnswer, SIGNAL("activated(int)"),
|
|
self.saveCard)
|
|
self.connect(self.form.flipButton, SIGNAL("clicked()"),
|
|
self.onFlip)
|
|
def linkClicked(url):
|
|
QDesktopServices.openUrl(QUrl(url))
|
|
self.form.preview.page().setLinkDelegationPolicy(
|
|
QWebPage.DelegateExternalLinks)
|
|
self.connect(self.form.preview,
|
|
SIGNAL("linkClicked(QUrl)"),
|
|
linkClicked)
|
|
if self.plastiqueStyle:
|
|
self.form.background.setStyle(self.plastiqueStyle)
|
|
self.form.alignment.clear()
|
|
self.form.alignment.addItems(
|
|
QStringList(alignmentLabels().values()))
|
|
self.fillCardList()
|
|
|
|
def formatToScreen(self, fmt):
|
|
fmt = re.sub("%\((.+?)\)s", "{{\\1}}", fmt)
|
|
fmt = fmt.replace("}}<br>", "}}\n")
|
|
return fmt
|
|
|
|
def screenToFormat(self, fmt):
|
|
fmt = fmt.replace("}}\n", "}}<br>")
|
|
return fmt
|
|
|
|
# def onEdit(self):
|
|
# ui.modelproperties.ModelProperties(
|
|
# self, self.deck, self.model, self.mw,
|
|
# onFinish=self.updateModelsList)
|
|
|
|
def formatChanged(self, type):
|
|
if self.updatingCards:
|
|
return
|
|
if type == "question":
|
|
text = unicode(self.form.cardQuestion.toPlainText())
|
|
text = self.screenToFormat(text)
|
|
#self.realCardModel(self.card).qformat = text
|
|
self.card.cardModel.qformat = text
|
|
else:
|
|
text = unicode(self.form.cardAnswer.toPlainText())
|
|
text = self.screenToFormat(text)
|
|
self.card.cardModel.aformat = text
|
|
self.fact.model.setModified()
|
|
self.deck.flushMod()
|
|
d = {}
|
|
for f in self.fact.model.fieldModels:
|
|
d[f.name] = (f.id, self.fact[f.name])
|
|
for card in self.cards:
|
|
qa = formatQA(None, self.fact.modelId, d, card.splitTags(),
|
|
card.cardModel, self.deck)
|
|
card.question = qa['question']
|
|
card.answer = qa['answer']
|
|
card.setModified()
|
|
self.deck.setModified()
|
|
self.needFormatRebuild = True
|
|
self.renderPreview()
|
|
|
|
def onFlip(self):
|
|
q = unicode(self.form.cardQuestion.toPlainText())
|
|
a = unicode(self.form.cardAnswer.toPlainText())
|
|
self.form.cardAnswer.setPlainText(q)
|
|
self.form.cardQuestion.setPlainText(a)
|
|
|
|
def readCard(self):
|
|
card = self.card.cardModel
|
|
self.form.background.setPalette(QPalette(QColor(
|
|
getattr(card, "lastFontColour"))))
|
|
self.updatingCards = True
|
|
self.form.cardQuestion.setPlainText(self.formatToScreen(card.qformat))
|
|
self.form.cardAnswer.setPlainText(self.formatToScreen(card.aformat))
|
|
self.form.questionInAnswer.setChecked(card.questionInAnswer)
|
|
self.form.allowEmptyAnswer.setChecked(card.allowEmptyAnswer)
|
|
self.form.alignment.setCurrentIndex(card.questionAlign)
|
|
self.form.typeAnswer.clear()
|
|
self.typeFieldNames = self.deck.db.column0("""
|
|
select fieldModels.name as n from fieldModels, cardModels
|
|
where cardModels.modelId = fieldModels.modelId
|
|
and cardModels.id = :id
|
|
order by n""", id=card.id)
|
|
s = [_("Don't ask me to type in the answer")]
|
|
s += [_("Compare with field '%s'") % f for f in self.typeFieldNames]
|
|
self.form.typeAnswer.insertItems(0, QStringList(s))
|
|
try:
|
|
idx = self.typeFieldNames.index(card.typeAnswer)
|
|
except ValueError:
|
|
idx = -1
|
|
self.form.typeAnswer.setCurrentIndex(idx + 1)
|
|
self.updatingCards = False
|
|
|
|
def fillCardList(self):
|
|
self.form.cardList.clear()
|
|
cards = []
|
|
idx = 0
|
|
for n, c in enumerate(self.cards):
|
|
if c == self.card:
|
|
cards.append(_("%s (current)") % c.cardModel.name)
|
|
idx = n
|
|
else:
|
|
cards.append(c.cardModel.name)
|
|
self.form.cardList.addItems(
|
|
QStringList(cards))
|
|
self.form.editTemplates.setEnabled(False)
|
|
if idx != 0:
|
|
self.form.cardList.setCurrentIndex(idx)
|
|
self.cardChanged(idx)
|
|
self.form.cardList.setFocus()
|
|
|
|
def cardChanged(self, idx):
|
|
self.card = self.cards[idx]
|
|
self.readCard()
|
|
self.renderPreview()
|
|
|
|
def saveCard(self):
|
|
if self.updatingCards:
|
|
return
|
|
card = self.card.cardModel
|
|
card.questionAlign = self.form.alignment.currentIndex()
|
|
card.lastFontColour = unicode(
|
|
self.form.background.palette().window().color().name())
|
|
card.questionInAnswer = self.form.questionInAnswer.isChecked()
|
|
card.allowEmptyAnswer = self.form.allowEmptyAnswer.isChecked()
|
|
idx = self.form.typeAnswer.currentIndex()
|
|
if not idx:
|
|
card.typeAnswer = u""
|
|
else:
|
|
card.typeAnswer = self.typeFieldNames[idx-1]
|
|
card.model.setModified()
|
|
self.deck.flushMod()
|
|
self.renderPreview()
|
|
|
|
def chooseColour(self, button, type="field"):
|
|
new = QColorDialog.getColor(button.palette().window().color(), self,
|
|
_("Choose Color"),
|
|
QColorDialog.DontUseNativeDialog)
|
|
if new.isValid():
|
|
button.setPalette(QPalette(new))
|
|
if type == "field":
|
|
self.saveField()
|
|
else:
|
|
self.saveCard()
|
|
|
|
def renderPreview(self):
|
|
c = self.card
|
|
styles = (self.deck.rebuildCSS() +
|
|
("\nhtml { background: %s }" % c.cardModel.lastFontColour))
|
|
styles = runFilter("addStyles", styles, c)
|
|
self.form.preview.setHtml(
|
|
('<html><head>%s</head><body>' % getBase(self.deck, c)) +
|
|
"<style>" + styles + "</style>" +
|
|
runFilter("drawQuestion", mungeQA(self.deck, c.htmlQuestion()),
|
|
c) +
|
|
"<hr>" +
|
|
runFilter("drawAnswer", mungeQA(self.deck, c.htmlAnswer()),
|
|
c)
|
|
+ "</body></html>")
|
|
clearAudioQueue()
|
|
if c.id not in self.playedAudio:
|
|
playFromText(c.question)
|
|
playFromText(c.answer)
|
|
self.playedAudio[c.id] = True
|
|
|
|
def reject(self):
|
|
modified = False
|
|
self.mw.startProgress()
|
|
self.deck.updateProgress(_("Applying changes..."))
|
|
reset=True
|
|
if self.needFormatRebuild:
|
|
# need to generate q/a templates
|
|
self.deck.updateCardsFromModel(self.fact.model)
|
|
self.deck.finishProgress()
|
|
modified = True
|
|
if len(self.fieldOrdinalUpdatedIds) > 0:
|
|
self.deck.rebuildFieldOrdinals(self.model.id, self.fieldOrdinalUpdatedIds)
|
|
modified = True
|
|
if self.needFieldRebuild:
|
|
modified = True
|
|
if modified:
|
|
self.fact.model.setModified()
|
|
self.deck.flushMod()
|
|
if self.factedit and self.factedit.onChange:
|
|
self.factedit.onChange("all")
|
|
reset=False
|
|
if reset:
|
|
self.mw.reset()
|
|
self.deck.finishProgress()
|
|
saveGeom(self, "CardLayout")
|
|
saveSplitter(self.form.splitter, "clayout")
|
|
QDialog.reject(self)
|
|
|
|
def onHelp(self):
|
|
QDesktopServices.openUrl(QUrl(aqt.appWiki +
|
|
"CardLayout"))
|
|
|
|
# Fields
|
|
##########################################################################
|
|
|
|
def setupFields(self):
|
|
self.fieldOrdinalUpdatedIds = []
|
|
self.updatingFields = False
|
|
self.needFieldRebuild = False
|
|
self.fillFieldList()
|
|
self.fieldChanged(0)
|
|
self.readField()
|
|
self.connect(self.form.fieldList, SIGNAL("currentRowChanged(int)"),
|
|
self.fieldChanged)
|
|
self.connect(self.form.fieldAdd, SIGNAL("clicked()"),
|
|
self.addField)
|
|
self.connect(self.form.fieldDelete, SIGNAL("clicked()"),
|
|
self.deleteField)
|
|
self.connect(self.form.fieldUp, SIGNAL("clicked()"),
|
|
self.moveFieldUp)
|
|
self.connect(self.form.fieldDown, SIGNAL("clicked()"),
|
|
self.moveFieldDown)
|
|
self.connect(self.form.fieldName, SIGNAL("lostFocus()"),
|
|
self.fillFieldList)
|
|
self.connect(self.form.fontFamily, SIGNAL("currentFontChanged(QFont)"),
|
|
self.saveField)
|
|
self.connect(self.form.fontSize, SIGNAL("valueChanged(int)"),
|
|
self.saveField)
|
|
self.connect(self.form.fontSizeEdit, SIGNAL("valueChanged(int)"),
|
|
self.saveField)
|
|
self.connect(self.form.fieldName, SIGNAL("textEdited(QString)"),
|
|
self.saveField)
|
|
self.connect(self.form.preserveWhitespace, SIGNAL("stateChanged(int)"),
|
|
self.saveField)
|
|
self.connect(self.form.fieldUnique, SIGNAL("stateChanged(int)"),
|
|
self.saveField)
|
|
self.connect(self.form.fieldRequired, SIGNAL("stateChanged(int)"),
|
|
self.saveField)
|
|
self.connect(self.form.numeric, SIGNAL("stateChanged(int)"),
|
|
self.saveField)
|
|
w = self.form.fontColour
|
|
if self.plastiqueStyle:
|
|
w.setStyle(self.plastiqueStyle)
|
|
self.connect(w, SIGNAL("clicked()"),
|
|
lambda w=w: self.chooseColour(w))
|
|
self.connect(self.form.rtl,
|
|
SIGNAL("stateChanged(int)"),
|
|
self.saveField)
|
|
|
|
def fieldChanged(self, idx):
|
|
if self.updatingFields:
|
|
return
|
|
self.field = self.model.fieldModels[idx]
|
|
self.readField()
|
|
self.enableFieldMoveButtons()
|
|
|
|
def readField(self):
|
|
field = self.field
|
|
self.updatingFields = True
|
|
self.form.fieldName.setText(field.name)
|
|
self.form.fieldUnique.setChecked(field.unique)
|
|
self.form.fieldRequired.setChecked(field.required)
|
|
self.form.numeric.setChecked(field.numeric)
|
|
if not field.quizFontFamily:
|
|
# backwards compat
|
|
field.quizFontFamily = u"Arial"
|
|
self.form.fontFamily.setCurrentFont(QFont(
|
|
field.quizFontFamily))
|
|
self.form.fontSize.setValue(field.quizFontSize or 20)
|
|
self.form.fontSizeEdit.setValue(field.editFontSize or 20)
|
|
self.form.fontColour.setPalette(QPalette(QColor(
|
|
field.quizFontColour or "#000000")))
|
|
self.form.rtl.setChecked(not not field.features)
|
|
self.form.preserveWhitespace.setChecked(not not field.editFontFamily)
|
|
self.updatingFields = False
|
|
|
|
def saveField(self, *args):
|
|
self.needFieldRebuild = True
|
|
if self.updatingFields:
|
|
return
|
|
self.updatingFields = True
|
|
field = self.field
|
|
name = unicode(self.form.fieldName.text()) or _("Field")
|
|
if field.name != name:
|
|
oldVal = self.fact[field.name]
|
|
self.deck.renameFieldModel(self.model, field, name)
|
|
# the card models will have been updated
|
|
self.readCard()
|
|
# for add card case
|
|
self.updateFact()
|
|
self.fact[name] = oldVal
|
|
field.unique = self.form.fieldUnique.isChecked()
|
|
field.required = self.form.fieldRequired.isChecked()
|
|
field.numeric = self.form.numeric.isChecked()
|
|
field.quizFontFamily = toCanonicalFont(unicode(
|
|
self.form.fontFamily.currentFont().family()))
|
|
field.quizFontSize = int(self.form.fontSize.value())
|
|
field.editFontSize = int(self.form.fontSizeEdit.value())
|
|
field.quizFontColour = str(
|
|
self.form.fontColour.palette().window().color().name())
|
|
if self.form.rtl.isChecked():
|
|
field.features = u"rtl"
|
|
else:
|
|
field.features = u""
|
|
if self.form.preserveWhitespace.isChecked():
|
|
field.editFontFamily = u"preserve"
|
|
else:
|
|
field.editFontFamily = u""
|
|
field.model.setModified()
|
|
self.deck.flushMod()
|
|
self.renderPreview()
|
|
self.fillFieldList()
|
|
self.updatingFields = False
|
|
|
|
def fillFieldList(self, row = None):
|
|
oldRow = self.form.fieldList.currentRow()
|
|
if oldRow == -1:
|
|
oldRow = 0
|
|
self.form.fieldList.clear()
|
|
n = 1
|
|
for field in self.model.fieldModels:
|
|
label = field.name
|
|
item = QListWidgetItem(label)
|
|
self.form.fieldList.addItem(item)
|
|
n += 1
|
|
count = self.form.fieldList.count()
|
|
if row != None:
|
|
self.form.fieldList.setCurrentRow(row)
|
|
else:
|
|
while (count > 0 and oldRow > (count - 1)):
|
|
oldRow -= 1
|
|
self.form.fieldList.setCurrentRow(oldRow)
|
|
self.enableFieldMoveButtons()
|
|
|
|
def enableFieldMoveButtons(self):
|
|
row = self.form.fieldList.currentRow()
|
|
if row < 1:
|
|
self.form.fieldUp.setEnabled(False)
|
|
else:
|
|
self.form.fieldUp.setEnabled(True)
|
|
if row == -1 or row >= (self.form.fieldList.count() - 1):
|
|
self.form.fieldDown.setEnabled(False)
|
|
else:
|
|
self.form.fieldDown.setEnabled(True)
|
|
|
|
def addField(self):
|
|
f = FieldModel(required=False, unique=False)
|
|
f.name = _("Field %d") % (len(self.model.fieldModels) + 1)
|
|
self.deck.addFieldModel(self.model, f)
|
|
try:
|
|
self.deck.db.refresh(self.fact)
|
|
except:
|
|
# not yet added
|
|
self.updateFact()
|
|
self.fillFieldList()
|
|
self.form.fieldList.setCurrentRow(len(self.model.fieldModels)-1)
|
|
self.form.fieldName.setFocus()
|
|
self.form.fieldName.selectAll()
|
|
|
|
def updateFact(self):
|
|
oldFact = self.fact
|
|
model = self.deck.db.query(Model).get(oldFact.model.id)
|
|
fact = self.deck.newFact(model)
|
|
for field in fact.fields:
|
|
try:
|
|
fact[field.name] = oldFact[field.name]
|
|
except KeyError:
|
|
fact[field.name] = u""
|
|
fact.tags = oldFact.tags
|
|
self.fact = fact
|
|
|
|
def deleteField(self):
|
|
row = self.form.fieldList.currentRow()
|
|
if row == -1:
|
|
return
|
|
if len(self.model.fieldModels) < 2:
|
|
ui.utils.showInfo(
|
|
_("Please add a new field first."),
|
|
parent=self)
|
|
return
|
|
field = self.model.fieldModels[row]
|
|
count = self.deck.fieldModelUseCount(field)
|
|
if count:
|
|
if not ui.utils.askUser(
|
|
_("This field is used by %d cards. If you delete it,\n"
|
|
"all information in this field will be lost.\n"
|
|
"\nReally delete this field?") % count,
|
|
parent=self):
|
|
return
|
|
self.deck.deleteFieldModel(self.model, field)
|
|
self.fillFieldList()
|
|
# need to update q/a format
|
|
self.readCard()
|
|
|
|
def moveFieldUp(self):
|
|
row = self.form.fieldList.currentRow()
|
|
if row == -1:
|
|
return
|
|
if row == 0:
|
|
return
|
|
field = self.model.fieldModels[row]
|
|
tField = self.model.fieldModels[row - 1]
|
|
self.model.fieldModels.remove(field)
|
|
self.model.fieldModels.insert(row - 1, field)
|
|
if field.id not in self.fieldOrdinalUpdatedIds:
|
|
self.fieldOrdinalUpdatedIds.append(field.id)
|
|
if tField.id not in self.fieldOrdinalUpdatedIds:
|
|
self.fieldOrdinalUpdatedIds.append(tField.id)
|
|
self.fillFieldList(row - 1)
|
|
|
|
def moveFieldDown(self):
|
|
row = self.form.fieldList.currentRow()
|
|
if row == -1:
|
|
return
|
|
if row == len(self.model.fieldModels) - 1:
|
|
return
|
|
field = self.model.fieldModels[row]
|
|
tField = self.model.fieldModels[row + 1]
|
|
self.model.fieldModels.remove(field)
|
|
self.model.fieldModels.insert(row + 1, field)
|
|
if field.id not in self.fieldOrdinalUpdatedIds:
|
|
self.fieldOrdinalUpdatedIds.append(field.id)
|
|
if tField.id not in self.fieldOrdinalUpdatedIds:
|
|
self.fieldOrdinalUpdatedIds.append(tField.id)
|
|
self.fillFieldList(row + 1)
|