# -*- coding: utf-8 -*- # Copyright: Damien Elmes # 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('' + 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, deck=self.deck) if not self.fact.isNew(): self.deck.setModified() self.deck.setUndoEnd(n) return modified def onFocusLost(self, widget): from ankiqt import mw if not self.fact: # 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, deck=self.deck) 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, deck=self.deck) 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.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.white, self.parent) self.parent.raise_() 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 insertLatex(self): w = self.focusedEdit() if w: selected = w.textCursor().selectedText() self.deck.mediaDir(create=True) cur = w.textCursor() pos = cur.position() w.insertHtml("[latex]%s[/latex]" % selected) cur.setPosition(pos+7) w.setTextCursor(cur) def insertLatexEqn(self): w = self.focusedEdit() if w: selected = w.textCursor().selectedText() self.deck.mediaDir(create=True) cur = w.textCursor() pos = cur.position() w.insertHtml("[$]%s[/$]" % selected) cur.setPosition(pos+3) w.setTextCursor(cur) def insertLatexMathEnv(self): w = self.focusedEdit() if w: selected = w.textCursor().selectedText() self.deck.mediaDir(create=True) cur = w.textCursor() pos = cur.position() w.insertHtml("[$$]%s[/$$]" % selected) cur.setPosition(pos+4) w.setTextCursor(cur) 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 '[...%s]' % ( clozeColour, exp) new = re.sub(re1, repl, s) old = re.sub(re2, '\\1' % 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('' % 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, copy=True): self.initMedia() if widget: w = widget else: w = self.focusedEdit() if copy: path = self._addMedia(file) self.maybeDelete(path, file) else: 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 lame to enable recording.'''), parent=self.parent) return raise if file: self._addSound(file, w, copy=False) class FactEdit(QTextEdit): def __init__(self, parent, *args): QTextEdit.__init__(self, *args) self.parent = parent self._tmpDir = None 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): if self._insertFromMimeData(source): self.emit(SIGNAL("lostFocus")) 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 True if source.hasText() and (ankiqt.mw.config['stripHTML'] or not source.hasHtml()): 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) self.parent._addPicture(name, widget=self) elif ext in audio: name = self._retrieveURL(txt) self.parent._addSound(name, widget=self) else: # not image or sound, treat as plain text self.insertPlainText(source.text()) return True except urllib2.URLError, e: ui.utils.showWarning(errtxt % e) else: self.insertPlainText(source.text()) return True if source.hasImage(): im = QImage(source.imageData()) if im.hasAlphaChannel(): (fd, name) = tempfile.mkstemp(prefix="paste", suffix=".png") uname = unicode(name, sys.getfilesystemencoding()) im.save(uname) else: (fd, name) = tempfile.mkstemp(prefix="paste", suffix=".jpg") uname = unicode(name, sys.getfilesystemencoding()) im.save(uname, None, 95) self.parent._addPicture(uname, widget=self) return True 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) self.parent._addPicture(name, widget=self) elif ext in audio: name = self._retrieveURL(url) self.parent._addSound(name, widget=self) except urllib2.URLError, e: ui.utils.showWarning(errtxt % e) return True if source.hasHtml(): self.insertHtml(self.simplifyHTML(unicode(source.html()))) return True def _retrieveURL(self, url): req = urllib2.Request(url, None, { 'User-Agent': 'Mozilla/5.0 (compatible; Anki/%s)' % ankiqt.appVersion }) filecontents = urllib2.urlopen(req).read() path = os.path.join(self.tmpDir(), os.path.basename(url)) file = open(path, "wb") file.write(filecontents) file.close() return path def tmpDir(self): if not self._tmpDir: self._tmpDir = tempfile.mkdtemp(prefix="anki") return self._tmpDir def simplifyHTML(self, html): "Remove all style information and P tags." if not ankiqt.mw.config['stripHTML']: return html html = re.sub("\n", " ", html) html = re.sub("
", "\n", html) html = re.sub("

", "\n\n", html) html = re.sub('', "", html) html = stripHTML(html) html = html.replace("\n", "
") html = html.strip() return html def focusOutEvent(self, evt): QTextEdit.focusOutEvent(self, evt) self.parent.lastFocusedEdit = self self.parent.resetFormatButtons() self.parent.disableButtons() if ankiqt.mw.config['preserveKeyboard'] and 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 ankiqt.mw.config['preserveKeyboard'] and 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)