Example #1
0
 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)
Example #2
0
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('[$$]', '[/$$]');")