Anki/ankiqt/ui/facteditor.py
2010-12-07 18:03:43 +09:00

1149 lines
42 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 *
from PyQt4.QtWebKit import QWebPage
import re, os, sys, tempfile, urllib2, ctypes
from anki.utils import stripHTML, tidyHTML, canonifyTags
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, runFilter
from sqlalchemy.exceptions import InvalidRequestError
clozeColour = "#0000ff"
if sys.platform.startswith("win32"):
ActivateKeyboardLayout = ctypes.windll.user32.ActivateKeyboardLayout
ActivateKeyboardLayout.restype = ctypes.c_void_p
ActivateKeyboardLayout.argtypes = [ctypes.c_void_p, ctypes.c_uint]
GetKeyboardLayout = ctypes.windll.user32.GetKeyboardLayout
GetKeyboardLayout.restype = ctypes.c_void_p
GetKeyboardLayout.argtypes = [ctypes.c_uint]
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.addMode = False
self.setupFields()
self.onChange = None
self.onFactValid = None
self.onFactInvalid = None
self.lastFocusedEdit = None
self.changeTimer = None
self.lastCloze = None
self.resetOnEdit = True
self.card=None
addHook("deckClosed", self.deckClosedHook)
addHook("guiReset", self.refresh)
addHook("colourChanged", self.colourChanged)
def close(self):
removeHook("deckClosed", self.deckClosedHook)
removeHook("guiReset", self.refresh)
removeHook("colourChanged", self.colourChanged)
def setFact(self, fact, noFocus=False, check=False, scroll=False,
forceRedraw=False):
"Make FACT the current fact."
self.fact = fact
self.factState = None
if self.changeTimer:
self.changeTimer.stop()
self.changeTimer = None
if self.needToRedraw() or forceRedraw:
if self.fact:
self.drawFields(noFocus, check)
else:
self.widget.hide()
return
else:
self.loadFields(check)
self.widget.show()
if scroll:
self.fieldsScroll.ensureVisible(0, 0)
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, forceRedraw=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 = {}
# button styles for mac
self.plastiqueStyle = QStyleFactory.create("plastique")
self.widget.setStyle(self.plastiqueStyle)
# 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)
# card layout
self.iconsBox.addItem(QSpacerItem(20,1, QSizePolicy.Expanding))
self.clayout = QPushButton("Card Layout")
self.clayout.connect(self.clayout, SIGNAL("clicked()"), self.onCardLayout)
self.clayout.setSizePolicy(QSizePolicy.Preferred,QSizePolicy.Preferred)
self.clayout.setFixedHeight(20)
# self.clayout.setFixedWidth(48)
self.clayout.setIcon(QIcon(":/icons/layout.png"))
#self.clayout.setIconSize(QSize(32,32))
self.clayout.setToolTip(_("Edit how cards are displayed (F2)"))
self.clayout.setShortcut(_("F2"))
self.clayout.setFocusPolicy(Qt.NoFocus)
self.iconsBox.addWidget(self.clayout)
self.clayout.setStyle(self.plastiqueStyle)
# 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)
# tags
self.tagsBox = QHBoxLayout()
self.tagsLabel = QLabel(_("Tags"))
self.tagsBox.addWidget(self.tagsLabel)
self.tags = ui.tagedit.TagEdit(self.parent)
self.tags.connect(self.tags, SIGNAL("lostFocus"),
self.onTagChange)
self.tagsBox.addWidget(self.tags)
self.fieldsBox.addLayout(self.tagsBox)
# icons
self.iconsBox.setMargin(0)
self.iconsBox2.setMargin(0)
# bold
spc = QSpacerItem(5,5)
self.iconsBox.addItem(spc)
self.bold = QPushButton()
self.bold.setFixedHeight(20)
self.bold.setFixedWidth(20)
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.setFixedHeight(20)
self.italic.setFixedWidth(20)
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.setFixedHeight(20)
self.underline.setFixedWidth(20)
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.onForeground)
self.foreground.setToolTip(
_("Set colour (F7; repeat to choose next; F6 to use)"))
self.foreground.setShortcut(_("F7"))
self.foreground.setFocusPolicy(Qt.NoFocus)
self.foreground.setEnabled(False)
self.foreground.setFixedWidth(20)
self.foreground.setFixedHeight(20)
self.foregroundFrame = QFrame()
self.foregroundFrame.setAutoFillBackground(True)
self.colourChanged()
hbox = QHBoxLayout()
hbox.addWidget(self.foregroundFrame)
hbox.setMargin(5)
self.foreground.setLayout(hbox)
self.iconsBox.addWidget(self.foreground)
self.foreground.setStyle(self.plastiqueStyle)
self.iconsBox.addItem(QSpacerItem(5,1, QSizePolicy.Fixed))
# cloze
self.cloze = QPushButton(self.widget)
self.cloze.setFixedHeight(20)
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.setFixedWidth(24)
self.cloze.setText("[...]")
self.cloze.setFocusPolicy(Qt.NoFocus)
self.cloze.setEnabled(False)
self.iconsBox.addWidget(self.cloze)
self.cloze.setStyle(self.plastiqueStyle)
# pictures
self.addPicture = QPushButton(self.widget)
self.addPicture.setFixedHeight(20)
self.addPicture.setFixedWidth(20)
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.setFixedHeight(20)
self.addSound.setFixedWidth(20)
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/video (F4)"))
self.iconsBox.addWidget(self.addSound)
self.addSound.setStyle(self.plastiqueStyle)
# sounds
self.recSound = QPushButton(self.widget)
self.recSound.setFixedHeight(20)
self.recSound.setFixedWidth(20)
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.setFixedHeight(20)
self.more.setFixedWidth(20)
self.more.connect(self.more, SIGNAL("clicked()"),
self.onMore)
self.more.setToolTip(_("Show advanced options"))
self.more.setText(">>")
self.more.setFocusPolicy(Qt.NoFocus)
self.iconsBox.addWidget(self.more)
self.more.setStyle(self.plastiqueStyle)
# latex
spc = QSpacerItem(5,5, QSizePolicy.Expanding)
self.iconsBox2.addItem(spc)
self.latex = QPushButton(self.widget)
self.latex.setFixedHeight(20)
self.latex.setFixedWidth(20)
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.setFixedHeight(20)
self.latexEqn.setFixedWidth(20)
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.setFixedHeight(20)
self.latexMathEnv.setFixedWidth(20)
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.setFixedHeight(20)
self.htmlEdit.setFixedWidth(20)
self.htmlEdit.setToolTip(_("HTML Editor (Ctrl+F9)"))
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)
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)
self.fieldsGrid.setSpacing(5)
def drawField(self, field, n):
# label
l = QLabel(field.name)
self.labels.append(l)
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)
if field.fieldModel.features:
w.setLayoutDirection(Qt.RightToLeft)
else:
w.setLayoutDirection(Qt.LeftToRight)
runHook("makeField", w, field)
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))
return w
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 = {}
self.labels = []
n = 0
first = True
last = None
for field in fields:
w = self.drawField(field, n)
last = w
if first:
self.focusTarget = w
first = False
n += 1
# update available tags
self.tags.setDeck(self.deck)
# update fields
self.loadFields(check)
self.parent.setUpdatesEnabled(True)
self.fieldsScroll.setWidget(self.fieldsFrame)
if sys.platform.startswith("darwin"):
extra = 5
elif sys.platform.startswith("win32"):
extra = 3
else:
extra = 2
tagsw = self.tagsLabel.sizeHint().width()
self.tagsLabel.setFixedWidth(max(tagsw,
max(*([
l.width() for l in self.labels] + [0])))
+ extra)
self.parent.setTabOrder(last, self.tags)
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.quizFontFamily
if family:
font.setFamily(family)
# size
size = field.fieldModel.editFontSize
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)
if not self.fact.isNew():
self.deck.setModified()
self.deck.setUndoEnd(n)
return modified
def onFocusLost(self, widget):
from ankiqt import mw
if self.fact is None:
# editor or deck closed
return
if mw.inDbHandler:
return
modified = self.saveFields()
field = self.widgets[widget]
self.fact.focusLost(field)
self.fact.setModified(textChanged=True)
self.loadFields(font=False)
if modified and self.resetOnEdit:
ankiqt.mw.reset(runHooks=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):
from ankiqt import mw
interval = 250
if not self.fact:
return
if mw.inDbHandler:
self.changeTimer.start(interval)
return
self.checkValid()
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('field')
# 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.fieldValid(field):
empty.append(field)
p.setColor(QPalette.Base, QColor("#ffffcc"))
self.fields[field.name][1].setPalette(p)
elif not self.fieldUnique(field):
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 textForField(self, field):
"Current edited value for field."
w = self.fields[field.name][1]
v = tidyHTML(unicode(w.toHtml()))
return v
def fieldValid(self, field):
return not (field.fieldModel.required and
not self.textForField(field).strip())
def fieldUnique(self, field):
if not field.fieldModel.unique:
return True
req = ("select value from fields "
"where fieldModelId = :fmid and value = :val and id != :id")
return not self.deck.s.scalar(
req, val=self.textForField(field), fmid=field.fieldModel.id, id=field.id)
def onTagChange(self):
if not self.fact:
return
old = self.fact.tags
self.fact.tags = canonifyTags(unicode(self.tags.text()))
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()
if self.resetOnEdit:
ankiqt.mw.reset(runHooks=False)
if self.onChange:
self.onChange('tag')
def focusField(self, fieldName):
self.fields[fieldName][1].setFocus()
def formatChanged(self, fmt):
w = self.focusedEdit()
if not w:
return
else:
l = self.bold, self.italic, self.underline
for b in l:
b.blockSignals(True)
self.bold.setChecked(w.fontWeight() == QFont.Bold)
self.italic.setChecked(w.fontItalic())
self.underline.setChecked(w.fontUnderline())
for b in l:
b.blockSignals(False)
def resetFormatButtons(self):
for b in self.bold, self.italic, self.underline:
b.blockSignals(True)
b.setChecked(False)
b.blockSignals(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.cloze.setEnabled(val)
self.htmlEdit.setEnabled(val)
self.recSound.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):
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 onForeground(self):
self.lastFocusedEdit = self.focusedEdit()
p = ColourPopup(self.parent)
p.move(self.foreground.mapToGlobal(QPoint(0,0)))
g = QGridLayout(p)
g.setMargin(4)
g.setSpacing(0)
p.setLayout(g)
lastWidget = None
self.colourNext = QShortcut(QKeySequence("F7"), p)
p.connect(self.colourNext, SIGNAL("activated()"),
self.onNextColour)
self.colourChoose = QShortcut(QKeySequence("F6"), p)
p.connect(self.colourChoose, SIGNAL("activated()"),
self.onChooseColourKey)
for n, c in enumerate(reversed(ankiqt.mw.config['recentColours'])):
col = QToolButton()
col.setAutoRaise(True)
col.setFixedWidth(64)
col.setFixedHeight(16)
col.setAutoFillBackground(True)
col.setPalette(QPalette(QColor(c)))
col.setStyleSheet("* {background-color: %s}" %
c)
col.connect(col, SIGNAL("clicked()"),
lambda c=c: self.onChooseColour(c))
g.addWidget(col, n, 0)
if lastWidget:
p.setTabOrder(lastWidget, col)
lastWidget = col
but = QPushButton("X")
but.setFixedWidth(16)
but.setFixedHeight(16)
but.setAutoDefault(False)
but.connect(but, SIGNAL("clicked()"),
lambda c=c: self.onRemoveColour(c))
g.addWidget(but, n, 1)
spc = QSpacerItem(5,10, QSizePolicy.Fixed)
g.addItem(spc, n+1, 0)
cb = QPushButton(_("+"))
cb.connect(cb, SIGNAL("clicked()"), self.onNewColour)
cb.setFixedWidth(80)
cb.setFixedHeight(16)
cb.setAutoDefault(False)
g.addWidget(cb, n+2, 0, 1, 2)
self.colourDiag = p
p.show()
def onRemoveColour(self, colour):
recent = ankiqt.mw.config['recentColours']
recent.remove(colour)
if not recent:
recent.append("#000000")
self.colourDiag.close()
self.onForeground()
runHook("colourChanged")
def onNextColour(self):
try:
self.colourDiag.focusWidget().nextInFocusChain().setFocus()
except:
ui.utils.showInfo("Your Qt version is too old to support this.")
def onChooseColourKey(self):
try:
self.colourDiag.focusWidget().click()
except:
# dialog focused
pass
def onChooseColour(self, colour):
recent = ankiqt.mw.config['recentColours']
recent.remove(colour)
recent.append(colour)
w = self.lastFocusedEdit
w.setTextColor(QColor(colour))
self.fontChanged = True
self.colourDiag.close()
runHook("colourChanged")
def onNewColour(self):
new = QColorDialog.getColor(Qt.black, self.colourDiag)
recent = ankiqt.mw.config['recentColours']
if new.isValid():
txtcol = unicode(new.name())
if txtcol not in recent:
recent.append(txtcol)
runHook("colourChanged")
self.onChooseColour(txtcol)
def _needExtraWord(self):
ver = ui.main.QtConfig.qt_version >> 8
if ver == 0x404:
# qt4.4 behaviour is wrong
return False
return True
def insertLatex(self):
w = self.focusedEdit()
if w:
selected = w.textCursor().selectedText()
self.deck.mediaDir(create=True)
w.insertHtml("[latex]%s[/latex]" % selected)
w.moveCursor(QTextCursor.PreviousWord)
if self._needExtraWord():
w.moveCursor(QTextCursor.PreviousWord)
w.moveCursor(QTextCursor.PreviousCharacter)
w.moveCursor(QTextCursor.PreviousCharacter)
def insertLatexEqn(self):
w = self.focusedEdit()
if w:
selected = w.textCursor().selectedText()
self.deck.mediaDir(create=True)
w.insertHtml("[$]%s[/$]" % selected)
w.moveCursor(QTextCursor.PreviousWord)
if self._needExtraWord():
w.moveCursor(QTextCursor.PreviousWord)
w.moveCursor(QTextCursor.PreviousCharacter)
w.moveCursor(QTextCursor.PreviousCharacter)
def insertLatexMathEnv(self):
w = self.focusedEdit()
if w:
selected = w.textCursor().selectedText()
self.deck.mediaDir(create=True)
w.insertHtml("[$$]%s[/$$]" % selected)
w.moveCursor(QTextCursor.PreviousWord)
if self._needExtraWord():
w.moveCursor(QTextCursor.PreviousWord)
w.moveCursor(QTextCursor.PreviousCharacter)
w.moveCursor(QTextCursor.PreviousCharacter)
def onMore(self, toggle=None):
if toggle is None:
toggle = not self.latex.isVisible()
ankiqt.mw.config['factEditorAdvanced'] = toggle
self.latex.setShown(toggle)
self.latexEqn.setShown(toggle)
self.latexMathEnv.setShown(toggle)
self.htmlEdit.setShown(toggle)
def onCardLayout(self):
self.saveFields()
ui.clayout.CardLayout(self.parent, self, self.fact, self.card)
# FIXME: in some future version, we should use a different delimiter, as
# [sound] et al conflicts
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."),
help="ClozeDeletion",
parent=self.parent)
return
# check if there's anything to change
if not re.search("\[.+?\]", unicode(src.toPlainText())):
ui.utils.showInfo(
_("You didn't specify anything to occlude."),
help="ClozeDeletion",
parent=self.parent)
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._addMedia(file)
self.maybeDelete(path, file)
w.insertHtml('<img src="%s">' % path)
def _addMedia(self, file):
try:
return self.deck.addMedia(file)
except (IOError, OSError), e:
ui.utils.showWarning(_("Unable to add media: %s") % unicode(e),
parent=self.parent)
def onAddSound(self):
# get this before we open the dialog
w = self.focusedEdit()
key = (_("Sounds/Videos") +
" (*.mp3 *.ogg *.wav *.avi *.ogv *.mpg *.mpeg *.mov *.mp4 " +
"*.mkv *.ogx *.ogv *.oga *.flv *.swf *.flac)")
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._addMedia(file)
self.maybeDelete(path, file)
anki.sound.play(path)
w.insertHtml('[sound:%s]' % path)
def maybeDelete(self, new, old):
if not ankiqt.mw.config['deleteMedia']:
return
if new == os.path.basename(old):
return
try:
os.unlink(old)
except:
pass
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
if sys.platform.startswith("win32"):
self._ownLayout = None
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 "\r" in txt:
txt = txt.split("\r")[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()
if sys.platform.startswith("win32"):
self._ownLayout = GetKeyboardLayout(0)
ActivateKeyboardLayout(self._programLayout, 0)
self.emit(SIGNAL("lostFocus"))
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()
if sys.platform.startswith("win32"):
self._programLayout = GetKeyboardLayout(0)
if self._ownLayout == None:
self._ownLayout = self._programLayout
ActivateKeyboardLayout(self._ownLayout, 0)
class ColourPopup(QDialog):
def __init__(self, parent):
QDialog.__init__(self, parent, Qt.FramelessWindowHint)
def event(self, evt):
if evt.type() == QEvent.WindowDeactivate:
self.close()
return QDialog.event(self, evt)