def setupTags(self): from anking.tagedit import TagEdit g = QGroupBox(self.widget) g.setFlat(True) tb = QGridLayout() tb.setSpacing(12) tb.setMargin(6) # tags l = QLabel(_("Tags")) tb.addWidget(l, 1, 0) self.tags = TagEdit(self.widget) self.tags.connect(self.tags, SIGNAL("lostFocus"), self.saveTags) tb.addWidget(self.tags, 1, 1) g.setLayout(tb) self.outerLayout.addWidget(g)
class AnkingEditor(object): def __init__(self, mw, modelChooser, widget, parentWindow): self.mw = mw self.modelChooser = modelChooser self.widget = widget self.parentWindow = parentWindow self.note = None self._loaded = False self.currentField = 0 self.currentSelection = (0, 0) self.setupOuter() self.setupButtons() self.setupWeb() self.setupTags() # Initial setup ############################################################ def setupOuter(self): l = QVBoxLayout() l.setMargin(0) l.setSpacing(0) self.widget.setLayout(l) self.outerLayout = l def setupWeb(self): self.web = EditorWebView(self.widget, self) self.web.allowDrops = True self.web.setBridge(self.bridge) self.outerLayout.addWidget(self.web, 1) # pick up the window colour p = self.web.palette() p.setBrush(QPalette.Base, Qt.transparent) self.web.page().setPalette(p) self.web.setAttribute(Qt.WA_OpaquePaintEvent, False) # Top buttons ###################################################################### def _addButton(self, name, func, key=None, tip=None, size=True, text="", check=False, native=False, canDisable=True): b = QPushButton(text) if check: b.connect(b, SIGNAL("clicked(bool)"), func) else: b.connect(b, SIGNAL("clicked()"), func) if size: b.setFixedHeight(20) b.setFixedWidth(20) if not native: b.setStyle(self.plastiqueStyle) b.setFocusPolicy(Qt.NoFocus) else: b.setAutoDefault(False) if not text: b.setIcon(QIcon(":/icons/%s.png" % name)) if key: b.setShortcut(QKeySequence(key)) if tip: b.setToolTip(shortcut(tip)) if check: b.setCheckable(True) self.iconsBox.addWidget(b) if canDisable: self._buttons[name] = b return b def setupButtons(self): self._buttons = {} # button styles for mac self.plastiqueStyle = QStyleFactory.create("plastique") self.widget.setStyle(self.plastiqueStyle) # icons self.iconsBox = QHBoxLayout() self.iconsBox.setMargin(6) self.iconsBox.setSpacing(0) self.outerLayout.addLayout(self.iconsBox) b = self._addButton # align to right self.iconsBox.addItem(QSpacerItem(20,1, QSizePolicy.Expanding)) # formating b("text_bold", self.toggleBold, _("Ctrl+B"), _("Bold text (Ctrl+B)"), check=True) b("text_italic", self.toggleItalic, _("Ctrl+I"), _("Italic text (Ctrl+I)"), check=True) b("text_under", self.toggleUnderline, _("Ctrl+U"), _("Underline text (Ctrl+U)"), check=True) b("text_super", self.toggleSuper, _("Ctrl+="), _("Superscript (Ctrl+=)"), check=True) b("text_sub", self.toggleSub, _("Ctrl+Shift+="), _("Subscript (Ctrl+Shift+=)"), check=True) b("text_clear", self.removeFormat, _("Ctrl+R"), _("Remove formatting (Ctrl+R)")) but = b("foreground", self.onForeground, _("F7"), text=" ") but.setToolTip(_("Set foreground colour (F7)")) self.setupForegroundButton(but) but = b("change_colour", self.onChangeCol, _("F8"), _("Change colour (F8)"), text=u"▾") but.setFixedWidth(12) # clozes but = b("cloze", self.onClozeInsert, _("Ctrl+Shift+C"), _("Cloze deletion (Ctrl+Shift+C)"), text="[...]") but.setFixedWidth(24) s = QShortcut(QKeySequence(_("Alt+C")), self.parentWindow) s.connect(s, SIGNAL("activated()"), self.onClozeInsert) s = QShortcut(QKeySequence(_("Alt+Shift+C")), self.parentWindow) s.connect(s, SIGNAL("activated()"), self.onClozeInsert) # switch to different models s = QShortcut(QKeySequence(_("Ctrl+G")), self.parentWindow) s.connect(s, SIGNAL("activated()"), self.onBasicModel) # media b("mail-attachment", self.onAddMedia, _("F3"), _("Attach pictures/audio/video (F3)")) but = b("selection", self.onImageSelection, _("Ctrl+O"), _("Insert image selection (Ctrl+O)"), text="Sel.") but.setFixedWidth(24) # latex but = b("latex", self.insertLatex, _("Ctrl+L"), _("LaTeX (Ctrl+L)"), text="LaTeX") but.setFixedWidth(50) but = b("latex-equation", self.insertLatexEqn, _("Ctrl+M"), _("LaTeX Equation (Ctrl+M)"), text="Eq.") but.setFixedWidth(30) but = b("latex-math", self.insertLatexMathEnv, _("Ctrl+Shift+M"), _("LaTeX Math Environment (Ctrl+Shift+M)"), text="Math") but.setFixedWidth(35) # html editing but = b("html", self.onHtmlEdit, _("Ctrl+Shift+H"), _("Edit HTML (Ctrl+Shift+H)"), text="HTML") but.setFixedWidth(45) runHook("setupEditorButtons", self) def enableButtons(self, val=True): for b in self._buttons.values(): b.setEnabled(val) def disableButtons(self): self.enableButtons(False) # JS->Python bridge ###################################################################### def bridge(self, str): if not self.note or not runHook: # shutdown return # focus lost or key/button pressed? if str.startswith("blur") or str.startswith("key"): (type, txt) = str.split(":", 1) txt = self.mungeHTML(txt) # misbehaving apps may include a null byte in the text txt = txt.replace("\x00", "") # reverse the url quoting we added to get images to display txt = unicode(urllib2.unquote( txt.encode("utf8")), "utf8", "replace") self.note.fields[self.currentField] = txt if type == "blur": self.disableButtons() # run any filters if runFilter( "editFocusLost", False, self.note, self.currentField): # something updated the note; schedule reload def onUpdate(): self.loadNote() self.checkValid() else: self.checkValid() else: runHook("editTimer", self.note) self.checkValid() # focused into field? elif str.startswith("focus"): (type, num) = str.split(":", 1) self.enableButtons() self.currentField = int(num) # state buttons changed? elif str.startswith("state"): (cmd, txt) = str.split(":", 1) r = json.loads(txt) self._buttons['text_bold'].setChecked(r['bold']) self._buttons['text_italic'].setChecked(r['italic']) self._buttons['text_under'].setChecked(r['under']) self._buttons['text_super'].setChecked(r['super']) self._buttons['text_sub'].setChecked(r['sub']) elif str.startswith("dupes"): self.showDupes() # save current selection elif str.startswith("selection"): (type, start, end) = str.split(":", 2) self.currentSelection = (int(start), int(end)) else: print str def mungeHTML(self, txt): if txt == "<br>": txt = "" return _filterHTML(txt) def focus(self): self.web.setFocus() def fonts(self): return [(f['font'], f['size'], f['rtl']) for f in self.note.model['flds']] def checkValid(self): cols = [] err = None for f in self.note.fields: cols.append("#fff") err = self.note.dupeOrEmpty() if err == 2: cols[0] = "#fcc" self.web.eval("showDupes();") else: self.web.eval("hideDupes();") self.web.eval("setBackgrounds(%s);" % json.dumps(cols)) def showDupes(self): contents = self.note.fields[0] # Write / load note data def _loadFinished(self, w): self._loaded = True if self.note: self.loadNote() def setNote(self, note): "Make NOTE the current note." self.note = note self.currentField = 0 # change timer if self.note: self.web.setHtml(_html % (self.getBase(), anki.js.jquery), loadCB=self._loadFinished) self.updateTags() else: self.hideCompleters() def getBase(col): mdir = sendToAnki("mediaDir") prefix = u"file://" base = prefix + unicode( urllib.quote(mdir.encode("utf-8")), "utf-8") + "/" return '<base href="%s">' % base def loadNote(self): if not self.note: return field = self.currentField if not self._loaded: # will be loaded when page is ready return data = [] for fld, val in self.note.items(): data.append((fld, self.escapeImages(val))) self.web.eval("setFields(%s, %d);" % ( json.dumps(data), field)) self.web.eval("setFonts(%s);" % ( json.dumps(self.fonts()))) self.checkValid() self.widget.show() self.web.setFocus() def escapeImages(self, string): # Feeding webkit unicode can result in it not finding images, so on # linux/osx we percent escape the image paths as utf8. On Windows the # problem is more complicated - if we percent-escape as utf8 it fixes # some images but breaks others. When filenames are normalized by # dropbox they become unreadable if we escape them. def repl(match): tag = match.group(1) fname = match.group(2) if re.match("(https?|ftp)://", fname): return tag return tag.replace( fname, urllib.quote(fname.encode("utf-8"))) regexp = "(?i)(<img[^>]+src=[\"']?([^\"'>]+)[\"']?[^>]*>)" return re.sub(regexp, repl, string) def saveNow(self): "Must call this before adding cards, closing dialog, etc." if not self.note: return self.saveTags() if self.mw.app.focusWidget() != self.web: # if no fields are focused, there's nothing to save return # move focus out of fields and save tags self.parentWindow.setFocus() # and process events so any focus-lost hooks fire self.mw.app.processEvents() # HTML editing ###################################################################### def onHtmlEdit(self): d = QDialog(self.widget) form = aqt.forms.edithtml.Ui_Dialog() form.setupUi(d) d.connect(form.buttonBox, SIGNAL("helpRequested()"), lambda: openHelp("editor")) form.textEdit.setPlainText(self.note.fields[self.currentField]) form.textEdit.moveCursor(QTextCursor.End) d.exec_() html = form.textEdit.toPlainText() # filter html through beautifulsoup so we can strip out things like a # leading </div> html = unicode(BeautifulSoup(html)) self.note.fields[self.currentField] = html self.loadNote() # focus field so it's saved self.web.setFocus() self.web.eval("focusField(%d);" % self.currentField) # Tag handling ###################################################################### def setupTags(self): from anking.tagedit import TagEdit g = QGroupBox(self.widget) g.setFlat(True) tb = QGridLayout() tb.setSpacing(12) tb.setMargin(6) # tags l = QLabel(_("Tags")) tb.addWidget(l, 1, 0) self.tags = TagEdit(self.widget) self.tags.connect(self.tags, SIGNAL("lostFocus"), self.saveTags) tb.addWidget(self.tags, 1, 1) g.setLayout(tb) self.outerLayout.addWidget(g) def updateTags(self): # update list of tags in tagedit self.tags.updateTags() if not self.tags.text(): self.tags.setText(self.note.tags.strip()) def saveTags(self): if not self.note: return self.note.tags = self.tags.text() runHook("tagsUpdated", self.note) def hideCompleters(self): self.tags.hideCompleter() # Format buttons ###################################################################### def toggleBold(self, bool): self.web.eval("setFormat('bold');") def toggleItalic(self, bool): self.web.eval("setFormat('italic');") def toggleUnderline(self, bool): self.web.eval("setFormat('underline');") def toggleSuper(self, bool): self.web.eval("setFormat('superscript');") def toggleSub(self, bool): self.web.eval("setFormat('subscript');") def removeFormat(self): self.web.eval("setFormat('removeFormat');") def onClozeSwitch(self): # if we are in a cloze deck, switch to Basic, else to Cloze # TODO remember last model? if not self.note.isCloze(): # change to "Cloze" model self.changeToModel("Cloze") else: self.changeToModel("Basic") return def onBasicModel(self): self.changeToModel("Basic") def onClozeInsert(self): # make sure we are in a "Cloze" model if not self.note.isCloze(): self.changeToModel("Cloze") # find the highest existing cloze highest = self.note.highestCloze() # reuse last if Alt is pressed if not self.mw.app.keyboardModifiers() & Qt.AltModifier: highest += 1 # must start at 1 highest = max(1, highest) self.web.eval("wrap('{{c%d::', '}}');" % highest) def changeToModel(self, model): # remember old data self.saveNow() oldNote = self.note oldField = self.currentField self.web.eval("saveSelection();") # change model, get new data self.modelChooser.changeToModel(model) note = self.note if oldNote and oldNote != note: # restore some of the note data for n in range(len(note.fields)): try: note.fields[n] = oldNote.fields[n] except IndexError: break self.loadNote() # restore caret etc. if oldField < len(note.fields): self.currentField = oldField (start, end) = self.currentSelection if start != None and end != None: self.web.eval("setSelection(%d, %d, %d);" % (oldField, start, end)) # Foreground colour ###################################################################### def setupForegroundButton(self, but): self.foregroundFrame = QFrame() self.foregroundFrame.setAutoFillBackground(True) self.foregroundFrame.setFocusPolicy(Qt.NoFocus) self.fcolour = self.mw.pm.profile.get("lastColour", "#00f") self.onColourChanged() hbox = QHBoxLayout() hbox.addWidget(self.foregroundFrame) hbox.setMargin(5) but.setLayout(hbox) # use last colour def onForeground(self): self._wrapWithColour(self.fcolour) # choose new colour def onChangeCol(self): new = QColorDialog.getColor(QColor(self.fcolour), None) # native dialog doesn't refocus us for some reason self.parentWindow.activateWindow() if new.isValid(): self.fcolour = new.name() self.onColourChanged() self._wrapWithColour(self.fcolour) def _updateForegroundButton(self): self.foregroundFrame.setPalette(QPalette(QColor(self.fcolour))) def onColourChanged(self): self._updateForegroundButton() self.mw.pm.profile['lastColour'] = self.fcolour def _wrapWithColour(self, colour): self.web.eval("setFormat('forecolor', '%s')" % colour) # Audio/video/images ###################################################################### def onImageSelection(self): path = subprocess.check_output("selection").strip() if path: self.addMedia(path, delete=True) def onAddMedia(self): key = (_("Media") + " (*.jpg *.png *.gif *.tiff *.svg *.tif *.jpeg "+ "*.mp3 *.ogg *.wav *.avi *.ogv *.mpg *.mpeg *.mov *.mp4 " + "*.mkv *.ogx *.ogv *.oga *.flv *.swf *.flac)") def accept(file): self.addMedia(file) file = getFile(self.widget, _("Add Media"), accept, key, key="media") self.parentWindow.activateWindow() def addMedia(self, path, delete=False): html = self._addMedia(path, delete) self.web.eval("setFormat('inserthtml', %s);" % json.dumps(html)) def _addMedia(self, path, delete=False): "Add to media folder and return basename." # copy to media folder name = sendToAnki("addFile", {"path": path}) # remove original? if delete: if os.path.abspath(name) != os.path.abspath(path): try: os.unlink(path) except: pass # return a local html link ext = name.split(".")[-1].lower() if ext in pics: return '<img src="%s">' % name else: anki.sound.play(name) return '[sound:%s]' % name # LaTeX ###################################################################### def insertLatex(self): self.web.eval("wrap('[latex]', '[/latex]');") def insertLatexEqn(self): self.web.eval("wrap('[$]', '[/$]');") def insertLatexMathEnv(self): self.web.eval("wrap('[$$]', '[/$$]');")