Anki/ankiqt/ui/facteditor.py
2009-04-11 00:42:23 +09:00

1063 lines
40 KiB
Python

# -*- coding: utf-8 -*-
# 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.QtSvg import *
import re, os, sys, tempfile, urllib2
from anki.utils import stripHTML, tidyHTML, canonifyTags
from anki.sound import playFromText
from ankiqt.ui.sound import getAudio
import anki.sound
from ankiqt import ui
import ankiqt
from ankiqt.ui.utils import mungeQA, saveGeom, restoreGeom
from anki.hooks import addHook, removeHook, runHook
from sqlalchemy.exceptions import InvalidRequestError
clozeColour = "#0000ff"
class FactEditor(object):
"""An editor for new/existing facts.
The fact is updated as it is edited.
Extra widgets can be added to 'fieldsGrid' to represent card-specific
information, etc."""
def __init__(self, parent, widget, deck=None):
self.widget = widget
self.parent = parent
self.deck = deck
self.fact = None
self.fontChanged = False
self.setupFields()
self.onChange = None
self.onFactValid = None
self.onFactInvalid = None
self.lastFocusedEdit = None
self.changeTimer = None
self.lastCloze = None
addHook("deckClosed", self.deckClosedHook)
addHook("guiReset", self.refresh)
addHook("colourChanged", self.colourChanged)
def close(self):
removeHook("deckClosed", self.deckClosedHook)
addHook("colourChanged", self.colourChanged)
removeHook("colourChanged", self.colourChanged)
def setFact(self, fact, noFocus=False, check=False):
"Make FACT the current fact."
self.fact = fact
self.factState = None
if self.changeTimer:
self.changeTimer.stop()
self.changeTimer = None
if self.needToRedraw():
if self.fact:
self.drawFields(noFocus, check)
else:
self.widget.hide()
return
else:
self.loadFields(check)
self.widget.show()
if not noFocus:
# update focus to first field
self.fields[self.fact.fields[0].name][1].setFocus()
self.fontChanged = False
self.deck.setUndoBarrier()
if self.deck.mediaDir(create=False):
self.initMedia()
def refresh(self):
if self.fact:
try:
self.deck.s.refresh(self.fact)
except InvalidRequestError:
# not attached to session yet, add cards dialog will handle
return
self.setFact(self.fact, check=True)
def focusFirst(self):
if self.focusTarget:
self.focusTarget.setFocus()
def initMedia(self):
os.chdir(self.deck.mediaDir(create=True))
def deckClosedHook(self):
self.fact = None
def setupFields(self):
# init for later
self.fields = {}
# top level vbox
self.fieldsBox = QVBoxLayout(self.widget)
self.fieldsBox.setMargin(0)
self.fieldsBox.setSpacing(3)
# icons
self.iconsBox = QHBoxLayout()
self.iconsBox2 = QHBoxLayout()
self.fieldsBox.addLayout(self.iconsBox)
self.fieldsBox.addLayout(self.iconsBox2)
# scrollarea
self.fieldsScroll = QScrollArea()
self.fieldsScroll.setWidgetResizable(True)
self.fieldsScroll.setLineWidth(0)
self.fieldsScroll.setFrameStyle(0)
self.fieldsScroll.setFocusPolicy(Qt.NoFocus)
self.fieldsBox.addWidget(self.fieldsScroll)
self.iconsBox.setMargin(0)
self.iconsBox.addItem(QSpacerItem(20,1, QSizePolicy.Expanding))
self.iconsBox2.setMargin(0)
self.iconsBox2.setMargin(0)
self.iconsBox2.addItem(QSpacerItem(20,1, QSizePolicy.Expanding))
# button styles for mac
self.plastiqueStyle = QStyleFactory.create("plastique")
self.widget.setStyle(self.plastiqueStyle)
# bold
self.bold = QPushButton()
self.bold.setCheckable(True)
self.bold.connect(self.bold, SIGNAL("toggled(bool)"), self.toggleBold)
self.bold.setIcon(QIcon(":/icons/text_bold.png"))
self.bold.setToolTip(_("Bold text (Ctrl+b)"))
self.bold.setShortcut(_("Ctrl+b"))
self.bold.setFocusPolicy(Qt.NoFocus)
self.bold.setEnabled(False)
self.iconsBox.addWidget(self.bold)
self.bold.setStyle(self.plastiqueStyle)
# italic
self.italic = QPushButton(self.widget)
self.italic.setCheckable(True)
self.italic.connect(self.italic, SIGNAL("toggled(bool)"), self.toggleItalic)
self.italic.setIcon(QIcon(":/icons/text_italic.png"))
self.italic.setToolTip(_("Italic text (Ctrl+i)"))
self.italic.setShortcut(_("Ctrl+i"))
self.italic.setFocusPolicy(Qt.NoFocus)
self.italic.setEnabled(False)
self.iconsBox.addWidget(self.italic)
self.italic.setStyle(self.plastiqueStyle)
# underline
self.underline = QPushButton(self.widget)
self.underline.setCheckable(True)
self.underline.connect(self.underline, SIGNAL("toggled(bool)"), self.toggleUnderline)
self.underline.setIcon(QIcon(":/icons/text_under.png"))
self.underline.setToolTip(_("Underline text (Ctrl+u)"))
self.underline.setShortcut(_("Ctrl+u"))
self.underline.setFocusPolicy(Qt.NoFocus)
self.underline.setEnabled(False)
self.iconsBox.addWidget(self.underline)
self.underline.setStyle(self.plastiqueStyle)
# foreground color
self.foreground = QPushButton()
self.foreground.connect(self.foreground, SIGNAL("clicked()"), self.setForeground)
self.foreground.setToolTip(_("Set colour (F7 then F7)"))
self.foreground.setShortcut(_("F7, F7"))
self.foreground.setFocusPolicy(Qt.NoFocus)
self.foreground.setEnabled(False)
self.foreground.setFixedWidth(30)
self.foreground.setFixedHeight(26)
self.foregroundFrame = QFrame()
self.foregroundFrame.setAutoFillBackground(True)
hbox = QHBoxLayout()
hbox.addWidget(self.foregroundFrame)
hbox.setMargin(5)
self.foreground.setLayout(hbox)
self.iconsBox.addWidget(self.foreground)
self.foreground.setStyle(self.plastiqueStyle)
# picker
vbox = QVBoxLayout()
vbox.setMargin(0)
vbox.setSpacing(0)
hbox = QHBoxLayout()
hbox.setMargin(0)
hbox.setSpacing(0)
self.fleft = QPushButton()
self.fleft.connect(self.fleft, SIGNAL("clicked()"), self.previousForeground)
self.fleft.setToolTip(_("Previous colour (F7 then F6)"))
self.fleft.setText("<")
self.fleft.setShortcut(_("F7, F6"))
self.fleft.setFocusPolicy(Qt.NoFocus)
self.fleft.setEnabled(False)
self.fleft.setFixedWidth(15)
self.fleft.setFixedHeight(14)
hbox.addWidget(self.fleft)
self.fleft.setStyle(self.plastiqueStyle)
self.fright = QPushButton()
self.fright.connect(self.fright, SIGNAL("clicked()"), self.nextForeground)
self.fright.setToolTip(_("Next colour (F7 then F8)"))
self.fright.setText(">")
self.fright.setShortcut(_("F7, F8"))
self.fright.setFocusPolicy(Qt.NoFocus)
self.fright.setEnabled(False)
self.fright.setFixedWidth(15)
self.fright.setFixedHeight(14)
hbox.addWidget(self.fright)
self.fright.setStyle(self.plastiqueStyle)
vbox.addLayout(hbox)
self.fchoose = QPushButton()
self.fchoose.connect(self.fchoose, SIGNAL("clicked()"), self.selectForeground)
self.fchoose.setToolTip(_("Choose colour (F7 then F5)"))
self.fchoose.setText("+")
self.fchoose.setShortcut(_("F7, F5"))
self.fchoose.setFocusPolicy(Qt.NoFocus)
self.fchoose.setEnabled(False)
self.fchoose.setFixedWidth(30)
self.fchoose.setFixedHeight(12)
vbox.addWidget(self.fchoose)
self.fchoose.setStyle(self.plastiqueStyle)
self.iconsBox.addLayout(vbox)
# pictures
spc = QSpacerItem(5,5)
self.iconsBox.addItem(spc)
self.addPicture = QPushButton(self.widget)
self.addPicture.connect(self.addPicture, SIGNAL("clicked()"), self.onAddPicture)
self.addPicture.setFocusPolicy(Qt.NoFocus)
self.addPicture.setShortcut(_("F3"))
self.addPicture.setIcon(QIcon(":/icons/colors.png"))
self.addPicture.setEnabled(False)
self.addPicture.setToolTip(_("Add a picture (F3)"))
self.iconsBox.addWidget(self.addPicture)
self.addPicture.setStyle(self.plastiqueStyle)
# sounds
self.addSound = QPushButton(self.widget)
self.addSound.connect(self.addSound, SIGNAL("clicked()"), self.onAddSound)
self.addSound.setFocusPolicy(Qt.NoFocus)
self.addSound.setShortcut(_("F4"))
self.addSound.setEnabled(False)
self.addSound.setIcon(QIcon(":/icons/text-speak.png"))
self.addSound.setToolTip(_("Add audio (F4)"))
self.iconsBox.addWidget(self.addSound)
self.addSound.setStyle(self.plastiqueStyle)
# sounds
self.recSound = QPushButton(self.widget)
self.recSound.connect(self.recSound, SIGNAL("clicked()"), self.onRecSound)
self.recSound.setFocusPolicy(Qt.NoFocus)
self.recSound.setShortcut(_("F5"))
self.recSound.setEnabled(False)
self.recSound.setIcon(QIcon(":/icons/media-record.png"))
self.recSound.setToolTip(_("Record audio (F5)"))
self.iconsBox.addWidget(self.recSound)
self.recSound.setStyle(self.plastiqueStyle)
# more
self.more = QPushButton(self.widget)
self.more.connect(self.more, SIGNAL("clicked()"),
self.onMore)
self.more.setToolTip(_("Show advanced options"))
self.more.setText(">>")
self.more.setFocusPolicy(Qt.NoFocus)
self.more.setEnabled(False)
self.more.setFixedWidth(30)
self.more.setFixedHeight(26)
self.iconsBox.addWidget(self.more)
self.more.setStyle(self.plastiqueStyle)
# preview
spc = QSpacerItem(5,5)
self.iconsBox2.addItem(spc)
self.preview = QPushButton(self.widget)
self.previewSC = QShortcut(QKeySequence(_("F2")), self.widget)
self.preview.connect(self.preview, SIGNAL("clicked()"),
self.onPreview)
self.preview.connect(self.previewSC, SIGNAL("activated()"),
self.onPreview)
self.preview.setToolTip(_("Preview (F2)"))
self.preview.setIcon(QIcon(":/icons/document-preview.png"))
self.preview.setFocusPolicy(Qt.NoFocus)
self.preview.setEnabled(False)
self.iconsBox2.addWidget(self.preview)
self.preview.setStyle(self.plastiqueStyle)
# cloze
self.cloze = QPushButton(self.widget)
self.clozeSC = QShortcut(QKeySequence(_("F9")), self.widget)
self.cloze.connect(self.cloze, SIGNAL("clicked()"),
self.onCloze)
self.cloze.connect(self.clozeSC, SIGNAL("activated()"),
self.onCloze)
self.cloze.setToolTip(_("Cloze (F9)"))
#self.cloze.setIcon(QIcon(":/icons/document-cloze.png"))
self.cloze.setFixedWidth(30)
self.cloze.setFixedHeight(26)
self.cloze.setText("[...]")
self.cloze.setFocusPolicy(Qt.NoFocus)
self.cloze.setEnabled(False)
self.iconsBox2.addWidget(self.cloze)
self.cloze.setStyle(self.plastiqueStyle)
# latex
spc = QSpacerItem(5,5)
self.iconsBox2.addItem(spc)
self.latex = QPushButton(self.widget)
self.latex.setToolTip(_("Latex (Ctrl+l then l)"))
self.latexSC = QShortcut(QKeySequence(_("Ctrl+l, l")), self.widget)
self.latex.connect(self.latex, SIGNAL("clicked()"), self.insertLatex)
self.latex.connect(self.latexSC, SIGNAL("activated()"), self.insertLatex)
self.latex.setIcon(QIcon(":/icons/tex.png"))
self.latex.setFocusPolicy(Qt.NoFocus)
self.latex.setEnabled(False)
self.iconsBox2.addWidget(self.latex)
self.latex.setStyle(self.plastiqueStyle)
# latex eqn
self.latexEqn = QPushButton(self.widget)
self.latexEqn.setToolTip(_("Latex equation (Ctrl+l then e)"))
self.latexEqnSC = QShortcut(QKeySequence(_("Ctrl+l, e")), self.widget)
self.latexEqn.connect(self.latexEqn, SIGNAL("clicked()"), self.insertLatexEqn)
self.latexEqn.connect(self.latexEqnSC, SIGNAL("activated()"), self.insertLatexEqn)
self.latexEqn.setIcon(QIcon(":/icons/math_sqrt.png"))
self.latexEqn.setFocusPolicy(Qt.NoFocus)
self.latexEqn.setEnabled(False)
self.iconsBox2.addWidget(self.latexEqn)
self.latexEqn.setStyle(self.plastiqueStyle)
# latex math env
self.latexMathEnv = QPushButton(self.widget)
self.latexMathEnv.setToolTip(_("Latex math environment (Ctrl+l then m)"))
self.latexMathEnvSC = QShortcut(QKeySequence(_("Ctrl+l, m")), self.widget)
self.latexMathEnv.connect(self.latexMathEnv, SIGNAL("clicked()"),
self.insertLatexMathEnv)
self.latexMathEnv.connect(self.latexMathEnvSC, SIGNAL("activated()"),
self.insertLatexMathEnv)
self.latexMathEnv.setIcon(QIcon(":/icons/math_matrix.png"))
self.latexMathEnv.setFocusPolicy(Qt.NoFocus)
self.latexMathEnv.setEnabled(False)
self.iconsBox2.addWidget(self.latexMathEnv)
self.latexMathEnv.setStyle(self.plastiqueStyle)
# html
self.htmlEdit = QPushButton(self.widget)
self.htmlEdit.setToolTip(_("HTML Editor"))
self.htmlEditSC = QShortcut(QKeySequence(_("Ctrl+F9")), self.widget)
self.htmlEdit.connect(self.htmlEdit, SIGNAL("clicked()"),
self.onHtmlEdit)
self.htmlEdit.connect(self.htmlEditSC, SIGNAL("activated()"),
self.onHtmlEdit)
self.htmlEdit.setIcon(QIcon(":/icons/text-xml.png"))
self.htmlEdit.setFocusPolicy(Qt.NoFocus)
self.htmlEdit.setEnabled(False)
self.iconsBox2.addWidget(self.htmlEdit)
self.htmlEdit.setStyle(self.plastiqueStyle)
#
self.fieldsFrame = None
self.widget.setLayout(self.fieldsBox)
# show advanced buttons?
if not ankiqt.mw.config['factEditorAdvanced']:
self.onMore(False)
# set initial colour
self._updateForegroundButton(ankiqt.mw.config['recentColours'][-1])
def _makeGrid(self):
"Rebuild the grid to avoid trigging QT bugs."
self.fieldsFrame = QWidget()
self.fieldsGrid = QGridLayout(self.fieldsFrame)
self.fieldsFrame.setLayout(self.fieldsGrid)
self.fieldsGrid.setMargin(0)
def drawFields(self, noFocus=False, check=False):
self.parent.setUpdatesEnabled(False)
self._makeGrid()
# add entries for each field
fields = self.fact.fields
self.fields = {}
self.widgets = {}
n = 0
first = True
for field in fields:
# label
l = QLabel(field.name)
self.fieldsGrid.addWidget(l, n, 0)
# edit widget
w = FactEdit(self)
w.setTabChangesFocus(True)
w.setAcceptRichText(True)
w.setMinimumSize(20, 60)
w.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
w.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
w.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.fieldsGrid.addWidget(w, n, 1)
self.fields[field.name] = (field, w)
self.widgets[w] = field
# catch changes
w.connect(w, SIGNAL("lostFocus"),
lambda w=w: self.onFocusLost(w))
w.connect(w, SIGNAL("textChanged()"),
self.onTextChanged)
w.connect(w, SIGNAL("currentCharFormatChanged(QTextCharFormat)"),
lambda w=w: self.formatChanged(w))
if first:
self.focusTarget = w
first = False
n += 1
# tags
self.fieldsGrid.addWidget(QLabel(_("Tags")), n, 0)
self.tags = ui.tagedit.TagEdit(self.parent)
self.tags.connect(self.tags, SIGNAL("lostFocus"),
self.onTagChange)
# update available tags
self.tags.setDeck(self.deck)
self.fieldsGrid.addWidget(self.tags, n, 1)
# update fields
self.loadFields(check)
self.parent.setUpdatesEnabled(True)
self.fieldsScroll.setWidget(self.fieldsFrame)
def needToRedraw(self):
if self.fact is None:
return True
if len(self.fact.fields) != len(self.fields):
return True
for field in self.fact.fields:
if field.name not in self.fields:
return True
return self.fontChanged
def loadFields(self, check=True, font=True):
"Update field text (if changed) and font/colours."
# text
for field in self.fact.fields:
w = self.fields[field.name][1]
self.fields[field.name] = (field, w)
self.widgets[w] = field
new = self.fact[field.name]
old = tidyHTML(unicode(w.toHtml()))
# only update if something has changed
if new != old:
cur = w.textCursor()
w.setHtml('<meta name="qrichtext" content="1"/>' + new)
w.setTextCursor(cur)
if font:
# apply fonts
font = QFont()
# family
family = (field.fieldModel.editFontFamily or
field.fieldModel.quizFontFamily)
if family:
font.setFamily(family)
# size
size = (field.fieldModel.editFontSize or
field.fieldModel.quizFontSize)
if size:
font.setPixelSize(size)
w.setFont(font)
self.tags.blockSignals(True)
self.tags.setText(self.fact.tags)
self.tags.blockSignals(False)
if check:
self.checkValid()
def saveFields(self):
"Save field text into fact."
modified = False
n = _("Edit")
self.deck.setUndoStart(n, merge=True)
for (w, f) in self.widgets.items():
v = tidyHTML(unicode(w.toHtml()))
if self.fact[f.name] != v:
self.fact[f.name] = v
modified = True
if modified:
self.fact.setModified(textChanged=True)
self.deck.updateFactTags([self.fact.id])
self.deck.setModified()
self.deck.setUndoEnd(n)
def onFocusLost(self, widget):
if self.fact is None:
# editor or deck closed
return
self.saveFields()
field = self.widgets[widget]
self.fact.focusLost(field)
self.fact.setModified(textChanged=True)
self.loadFields(font=False)
def onTextChanged(self):
interval = 250
if self.changeTimer:
self.changeTimer.setInterval(interval)
else:
self.changeTimer = QTimer(self.parent)
self.changeTimer.setSingleShot(True)
self.changeTimer.start(interval)
self.parent.connect(self.changeTimer,
SIGNAL("timeout()"),
self.onChangeTimer)
def onChangeTimer(self):
if not self.fact:
return
self.saveFields()
self.checkValid()
if self.onChange:
self.onChange()
self.changeTimer = None
def saveFieldsNow(self):
"Must call this before adding cards, closing dialog, etc."
if not self.fact:
return
# disable timer
if self.changeTimer:
self.changeTimer.stop()
self.changeTimer = None
if self.onChange:
self.onChange()
# save fields and run features
w = self.focusedEdit()
if w:
self.onFocusLost(w)
self.onTagChange()
# ensure valid
self.checkValid()
def checkValid(self):
empty = []
dupe = []
for field in self.fact.fields:
p = QPalette()
p.setColor(QPalette.Text, QColor("#000000"))
if not self.fact.fieldValid(field):
empty.append(field)
p.setColor(QPalette.Base, QColor("#ffffcc"))
self.fields[field.name][1].setPalette(p)
elif not self.fact.fieldUnique(field, self.deck.s):
dupe.append(field)
p.setColor(QPalette.Base, QColor("#ffcccc"))
self.fields[field.name][1].setPalette(p)
else:
p.setColor(QPalette.Base, QColor("#ffffff"))
self.fields[field.name][1].setPalette(p)
# call relevant hooks
invalid = len(empty+dupe)
if self.factState != "valid" and not invalid:
if self.onFactValid:
self.onFactValid(self.fact)
self.factState = "valid"
elif self.factState != "invalid" and invalid:
if self.onFactInvalid:
self.onFactInvalid(self.fact)
self.factState = "invalid"
def onTagChange(self):
if not self.fact:
return
old = self.fact.tags
self.fact.tags = canonifyTags(unicode(self.tags.text()))
if self.onChange:
self.onChange()
if old != self.fact.tags:
self.deck.s.flush()
self.deck.updateFactTags([self.fact.id])
self.deck.updatePriorities([c.id for c in self.fact.cards])
self.fact.setModified(textChanged=True)
self.deck.flushMod()
def focusField(self, fieldName):
self.fields[fieldName][1].setFocus()
def formatChanged(self, fmt):
w = self.focusedEdit()
if not w or w.textCursor().hasSelection():
return
else:
self.bold.setChecked(w.fontWeight() == QFont.Bold)
self.italic.setChecked(w.fontItalic())
self.underline.setChecked(w.fontUnderline())
def resetFormatButtons(self):
self.bold.setChecked(False)
self.italic.setChecked(False)
self.underline.setChecked(False)
def enableButtons(self, val=True):
self.bold.setEnabled(val)
self.italic.setEnabled(val)
self.underline.setEnabled(val)
self.foreground.setEnabled(val)
self.fchoose.setEnabled(val)
self.fleft.setEnabled(val)
self.fright.setEnabled(val)
self.addPicture.setEnabled(val)
self.addSound.setEnabled(val)
self.latex.setEnabled(val)
self.latexEqn.setEnabled(val)
self.latexMathEnv.setEnabled(val)
self.preview.setEnabled(val)
self.cloze.setEnabled(val)
self.htmlEdit.setEnabled(val)
self.recSound.setEnabled(val)
self.more.setEnabled(val)
def disableButtons(self):
self.enableButtons(False)
def focusedEdit(self):
for (name, (field, w)) in self.fields.items():
if w.hasFocus():
return w
return None
def toggleBold(self, bool):
w = self.focusedEdit()
if w:
self.fontChanged = True
w.setFontWeight(bool and QFont.Bold or QFont.Normal)
def toggleItalic(self, bool):
w = self.focusedEdit()
if w:
self.fontChanged = True
w.setFontItalic(bool)
def toggleUnderline(self, bool):
w = self.focusedEdit()
if w:
self.fontChanged = True
w.setFontUnderline(bool)
def _updateForegroundButton(self, txtcol):
# FIXME: working on mac?
self.foregroundFrame.setPalette(QPalette(QColor(txtcol)))
self.foregroundFrame.setStyleSheet("* {background-color: %s}" %
txtcol)
def colourChanged(self):
recent = ankiqt.mw.config['recentColours']
self._updateForegroundButton(recent[-1])
def setForeground(self, w=None):
recent = ankiqt.mw.config['recentColours']
if not w:
w = self.focusedEdit()
if w:
w.setTextColor(QColor(recent[-1]))
self.fontChanged = True
def previousForeground(self):
recent = ankiqt.mw.config['recentColours']
last = recent.pop()
recent.insert(0, last)
runHook("colourChanged")
self.setForeground()
def nextForeground(self):
recent = ankiqt.mw.config['recentColours']
last = recent.pop(0)
recent.append(last)
runHook("colourChanged")
self.setForeground()
def selectForeground(self):
w = self.focusedEdit()
recent = ankiqt.mw.config['recentColours']
new = QColorDialog.getColor(QColor(recent[-1]),
self.parent)
if new.isValid():
txtcol = unicode(new.name())
if txtcol in recent:
recent.remove(txtcol)
recent.append(txtcol)
runHook("colourChanged")
self.setForeground(w)
def insertLatex(self):
w = self.focusedEdit()
if w:
self.deck.mediaDir(create=True)
w.insertHtml("[latex][/latex]")
w.moveCursor(QTextCursor.PreviousWord)
w.moveCursor(QTextCursor.PreviousWord)
w.moveCursor(QTextCursor.PreviousCharacter)
w.moveCursor(QTextCursor.PreviousCharacter)
def insertLatexEqn(self):
w = self.focusedEdit()
if w:
self.deck.mediaDir(create=True)
w.insertHtml("[$][/$]")
w.moveCursor(QTextCursor.PreviousWord)
w.moveCursor(QTextCursor.PreviousWord)
w.moveCursor(QTextCursor.PreviousCharacter)
w.moveCursor(QTextCursor.PreviousCharacter)
def insertLatexMathEnv(self):
w = self.focusedEdit()
if w:
self.deck.mediaDir(create=True)
w.insertHtml("[$$][/$$]")
w.moveCursor(QTextCursor.PreviousWord)
w.moveCursor(QTextCursor.PreviousWord)
w.moveCursor(QTextCursor.PreviousCharacter)
w.moveCursor(QTextCursor.PreviousCharacter)
def onMore(self, toggle=None):
if toggle is None:
toggle = not self.preview.isVisible()
ankiqt.mw.config['factEditorAdvanced'] = toggle
self.preview.setShown(toggle)
self.cloze.setShown(toggle)
self.latex.setShown(toggle)
self.latexEqn.setShown(toggle)
self.latexMathEnv.setShown(toggle)
self.htmlEdit.setShown(toggle)
def onPreview(self):
PreviewDialog(self.parent, self.deck, self.fact)
def onCloze(self):
src = self.focusedEdit()
if not src:
return
re1 = "\[.+?(:(.+?))?\]"
re2 = "\[(.+?)(:.+?)?\]"
# add brackets because selected?
cursor = src.textCursor()
oldSrc = None
if cursor.hasSelection():
oldSrc = src.toHtml()
s = cursor.selectionStart()
e = cursor.selectionEnd()
cursor.setPosition(e)
cursor.insertText("]]")
cursor.setPosition(s)
cursor.insertText("[[")
re1 = "\[" + re1 + "\]"
re2 = "\[" + re2 + "\]"
dst = None
for field in self.fact.fields:
w = self.fields[field.name][1]
if w.hasFocus():
dst = False
continue
if dst is False:
dst = w
break
if not dst:
dst = self.fields[self.fact.fields[0].name][1]
if dst == w:
return
# check if there's alredy something there
if not oldSrc:
oldSrc = src.toHtml()
oldDst = dst.toHtml()
if unicode(dst.toPlainText()):
if (self.lastCloze and
self.lastCloze[1] == oldSrc and
self.lastCloze[2] == oldDst):
src.setHtml(self.lastCloze[0])
dst.setHtml("")
self.lastCloze = None
self.saveFields()
return
else:
ui.utils.showInfo(_("Next field must be blank."),
parent=self.parent)
return
# check if there's anything to change
if not re.search("\[.+?\]", unicode(src.toPlainText())):
QDesktopServices.openUrl(QUrl(ankiqt.appWiki +
"ClozeDeletion"))
return
# create
s = unicode(src.toHtml())
def repl(match):
exp = ""
if match.group(2):
exp = match.group(2)
return '<font color="%s"><b>[...%s]</b></font>' % (
clozeColour, exp)
new = re.sub(re1, repl, s)
old = re.sub(re2, '<font color="%s"><b>\\1</b></font>'
% clozeColour, s)
src.setHtml(new)
dst.setHtml(old)
self.lastCloze = (oldSrc, unicode(src.toHtml()),
unicode(dst.toHtml()))
self.saveFields()
def onHtmlEdit(self):
def helpRequested():
QDesktopServices.openUrl(QUrl(ankiqt.appWiki +
"HtmlEditor"))
w = self.focusedEdit()
if w:
self.saveFields()
d = QDialog(self.parent)
form = ankiqt.forms.edithtml.Ui_Dialog()
form.setupUi(d)
d.connect(form.buttonBox, SIGNAL("helpRequested()"),
helpRequested)
form.textEdit.setPlainText(self.widgets[w].value)
form.textEdit.moveCursor(QTextCursor.End)
d.exec_()
w.setHtml(unicode(form.textEdit.toPlainText()).\
replace("\n", ""))
self.saveFields()
def fieldsAreBlank(self):
for (field, widget) in self.fields.values():
value = tidyHTML(unicode(widget.toHtml()))
if value:
return False
return True
def onAddPicture(self):
# get this before we open the dialog
w = self.focusedEdit()
key = _("Images (*.jpg *.png *.gif *.tiff *.svg *.tif *.jpeg)")
file = ui.utils.getFile(self.parent, _("Add an image"), "picture", key)
if not file:
return
if file.lower().endswith(".svg"):
# convert to a png
s = QSvgRenderer(file)
i = QImage(s.defaultSize(), QImage.Format_ARGB32_Premultiplied)
p = QPainter()
p.begin(i)
s.render(p)
p.end()
(fd, name) = tempfile.mkstemp(prefix="anki", suffix=".png")
file = unicode(name, sys.getfilesystemencoding())
i.save(file)
self._addPicture(file, widget=w)
def _addPicture(self, file, widget=None):
self.initMedia()
if widget:
w = widget
else:
w = self.focusedEdit()
path = self.deck.addMedia(file)
w.insertHtml('<img src="%s">' % path)
def onAddSound(self):
# get this before we open the dialog
w = self.focusedEdit()
key = _("Sounds (*.mp3 *.ogg *.wav)")
file = ui.utils.getFile(self.parent, _("Add audio"), "audio", key)
if not file:
return
self._addSound(file, widget=w)
def _addSound(self, file, widget=None):
self.initMedia()
if widget:
w = widget
else:
w = self.focusedEdit()
path = self.deck.addMedia(file)
anki.sound.play(path)
w.insertHtml('[sound:%s]' % path)
def onRecSound(self):
self.initMedia()
w = self.focusedEdit()
try:
file = getAudio(self.parent)
except:
if sys.platform.startswith("darwin"):
ui.utils.showInfo(_('''\
Please install <a href="http://www.thalictrum.com/software/lame-3.97.dmg.gz">lame</a>
to enable recording.'''), parent=self.parent)
return
raise
if file:
self._addSound(unicode(file), widget=w)
class FactEdit(QTextEdit):
def __init__(self, parent, *args):
QTextEdit.__init__(self, *args)
self.parent = parent
def canInsertFromMimeData(self, source):
return (source.hasUrls() or
source.hasText() or
source.hasImage() or
source.hasHtml())
def insertFromMimeData(self, source):
pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif")
audio = ("wav", "mp3", "ogg", "flac")
errtxt = _("An error occured while opening %s")
if source.hasHtml() and "qrichtext" in unicode(source.html()):
self.insertHtml(source.html())
return
if source.hasText():
txt = unicode(source.text())
l = txt.lower()
if l.startswith("http://") or l.startswith("file://"):
hadN = False
if "\n" in txt:
txt = txt.split("\n")[0]
hadN = True
if not source.hasImage() or hadN:
# firefox on linux just gives us a url
ext = txt.split(".")[-1].lower()
try:
if ext in pics:
name = self._retrieveURL(txt, ext)
self.parent._addPicture(name, widget=self)
elif ext in audio:
name = self._retrieveURL(txt, ext)
self.parent._addSound(name, widget=self)
else:
# not image or sound, treat as plain text
self.insertPlainText(source.text())
except urllib2.URLError, e:
ui.utils.showWarning(errtxt % e)
return
else:
self.insertPlainText(source.text())
return
if source.hasImage():
im = QImage(source.imageData())
if im.hasAlphaChannel():
(fd, name) = tempfile.mkstemp(prefix="anki", suffix=".png")
uname = unicode(name, sys.getfilesystemencoding())
im.save(uname)
else:
(fd, name) = tempfile.mkstemp(prefix="anki", suffix=".jpg")
uname = unicode(name, sys.getfilesystemencoding())
im.save(uname, None, 95)
self.parent._addPicture(uname, widget=self)
return
if source.hasUrls():
for url in source.urls():
url = unicode(url.toString())
ext = url.split(".")[-1].lower()
try:
if ext in pics:
name = self._retrieveURL(url, ext)
self.parent._addPicture(name, widget=self)
elif ext in audio:
name = self._retrieveURL(url, ext)
self.parent._addSound(name, widget=self)
except urllib2.URLError, e:
ui.utils.showWarning(errtxt % e)
return
if source.hasHtml():
self.insertHtml(self.simplifyHTML(unicode(source.html())))
return
def _retrieveURL(self, url, ext):
req = urllib2.Request(url, None, {
'User-Agent': 'Mozilla/5.0 (compatible; Anki/%s)' %
ankiqt.appVersion })
filecontents = urllib2.urlopen(req).read()
(fd, name) = tempfile.mkstemp(prefix="anki", suffix=".%s" %
ext.encode("ascii"))
file = os.fdopen(fd, "wb")
file.write(filecontents)
file.flush()
return unicode(name, sys.getfilesystemencoding())
def simplifyHTML(self, html):
"Remove all style information and P tags."
html = re.sub("\n", " ", html)
html = re.sub("<br ?/?>", "\n", html)
html = re.sub("<p ?/?>", "\n\n", html)
html = re.sub('<style type="text/css">.*?</style>', "", html)
html = stripHTML(html)
html = html.replace("\n", "<br>")
html = html.strip()
return html
def focusOutEvent(self, evt):
QTextEdit.focusOutEvent(self, evt)
self.parent.lastFocusedEdit = self
self.parent.resetFormatButtons()
self.parent.disableButtons()
self.emit(SIGNAL("lostFocus"))
# this shouldn't be necessary if/when we move away from kakasi
def mouseDoubleClickEvent(self, evt):
r = QRegExp("\\{(.*[|,].*)\\}")
r.setMinimal(True)
mouseposition = self.textCursor().position()
blockoffset = 0
result = r.indexIn(self.toPlainText(), 0)
found = ""
while result != -1:
if mouseposition > result and mouseposition < result + r.matchedLength():
mouseposition -= result + 1
frompos = 0
topos = 0
string = r.cap(1)
offset = 0
bits = re.split("[|,]", unicode(string))
for index in range(0, len(bits)):
offset += len(bits[index]) + 1
if mouseposition < offset:
found = bits[index]
break
break
blockoffset= result + r.matchedLength()
result = r.indexIn(self.toPlainText(), blockoffset)
if found == "":
QTextEdit.mouseDoubleClickEvent(self,evt)
return
self.setPlainText(self.toPlainText().replace(result, r.matchedLength(), found))
def focusInEvent(self, evt):
if (self.parent.lastFocusedEdit and
self.parent.lastFocusedEdit is not self):
# remove selection from previous widget
try:
cur = self.parent.lastFocusedEdit.textCursor()
cur.clearSelection()
self.parent.lastFocusedEdit.setTextCursor(cur)
except RuntimeError:
# old widget was deleted
pass
self.lastFocusedEdit = None
QTextEdit.focusInEvent(self, evt)
self.parent.formatChanged(None)
self.parent.enableButtons()
class PreviewDialog(QDialog):
def __init__(self, parent, deck, fact, *args):
QDialog.__init__(self, parent, *args)
self.deck = deck
self.fact = fact
cards = self.deck.previewFact(self.fact)
if not cards:
ui.utils.showInfo(_("No cards to preview."),
parent=parent)
return
self.cards = cards
self.currentCard = 0
self.dialog = ankiqt.forms.previewcards.Ui_Dialog()
self.dialog.setupUi(self)
self.dialog.comboBox.addItems(QStringList(
[c.cardModel.name for c in self.cards]))
self.connect(self.dialog.comboBox, SIGNAL("activated(int)"),
self.onChange)
self.updateCard()
restoreGeom(self, "preview")
self.exec_()
def updateCard(self):
c = self.cards[self.currentCard]
self.dialog.webView.setHtml(
"<style>" + self.deck.css +
("\nhtml { background: %s }" % c.cardModel.lastFontColour) +
"\ndiv { white-space: pre-wrap; }</style>" +
mungeQA(self.deck, c.htmlQuestion()) +
"<br><br><hr><br><br>" +
mungeQA(self.deck, c.htmlAnswer()))
playFromText(c.question)
playFromText(c.answer)
def onChange(self, idx):
self.currentCard = idx
self.updateCard()
def reject(self):
saveGeom(self, "preview")
QDialog.reject(self)