mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
1061 lines
39 KiB
Python
1061 lines
39 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)
|
|
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 +
|
|
"\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)
|