Anki/aqt/editor.py
2012-01-16 10:27:31 +09:00

965 lines
31 KiB
Python

# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from aqt.qt import *
import re, os, sys, urllib2, ctypes, simplejson, traceback
from anki.utils import stripHTML, isWin, isMac, namedtmp
from anki.sound import play
from anki.hooks import runHook
from aqt.sound import getAudio
from aqt.webview import AnkiWebView
from aqt.utils import shortcut, showInfo, showWarning, getBase, getFile, \
openHelp
import aqt
import anki.js
# fixme: when tab order returns to the webview, the previously focused field
# is focused, which is not good when the user is tabbing through the dialog
# fixme: set rtl in div css
# fixme: commit from tag area causes error
pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif")
audio = ("wav", "mp3", "ogg", "flac")
_html = """
<html><head>%s<style>
.field {
border: 1px solid #aaa; background:#fff; color:#000; padding: 5px;
}
.fname { font-size: 12px; vertical-align: middle; padding: 0; }
#dupes { font-size: 12px; }
img { max-width: 150; max-height: 150; }
body { margin: 5px; }
</style><script>
%s
String.prototype.format = function() {
var args = arguments;
return this.replace(/\{\d+\}/g, function(m){
return args[m.match(/\d+/)]; });
};
var currentField = null;
var changeTimer = null;
function onKey() {
// esc clears focus, allowing dialog to close
if (window.event.which == 27) {
currentField.blur();
return;
}
clearChangeTimer();
changeTimer = setTimeout(function () {
sendState();
saveField("key"); }, 600);
};
function sendState() {
var r = {
'bold': document.queryCommandState("bold"),
'italic': document.queryCommandState("italic"),
'under': document.queryCommandState("underline"),
'super': document.queryCommandState("superscript"),
'sub': document.queryCommandState("subscript"),
'col': document.queryCommandValue("forecolor")
};
py.run("state:" + JSON.stringify(r));
};
function setFormat(cmd, arg, nosave) {
document.execCommand(cmd, false, arg);
if (!nosave) {
saveField('key');
}
};
function clearChangeTimer() {
if (changeTimer) {
clearTimeout(changeTimer);
changeTimer = null;
}
};
function onFocus(elem) {
currentField = elem;
// prevent webkit from highlighting the whole field
$(elem).css("-webkit-user-select", "none");
setTimeout(function () { unfocusHack() }, 1);
py.run("focus:" + currentField.id.substring(1));
function pos(obj) {
var cur = 0;
do {
cur += obj.offsetTop;
} while (obj = obj.offsetParent);
return cur;
}
window.scroll(0,pos(elem)-15);
}
// restore cursor
function unfocusHack() {
$(currentField).css("-webkit-user-select", "text");
var r = document.createRange()
r.selectNodeContents(currentField);
r.collapse();
var s = document.getSelection();
s.addRange(r);
};
function onBlur() {
if (currentField) {
saveField("blur");
}
clearChangeTimer();
// if we lose focus, assume the last field is still targeted
//currentField = null;
};
function saveField(type) {
// type is either 'blur' or 'key'
py.run(type + ":" + currentField.innerHTML);
clearChangeTimer();
};
function wrap(front, back) {
setFormat('removeFormat', null, true);
var s = window.getSelection();
var r = s.getRangeAt(0);
var content = r.extractContents();
var span = document.createElement("span")
span.appendChild(content);
s.removeAllRanges();
s.addRange(r);
var new_ = front + span.innerHTML + back;
var f = currentField.innerHTML;
if (f.length && f[f.length-1] === " ") {
new_ = " " + new_;
}
setFormat('inserthtml', new_);
};
function setFields(fields, focusTo) {
var txt = "";
for (var i=0; i<fields.length; i++) {
var n = fields[i][0];
var f = fields[i][1];
txt += "<tr><td class=fname>{0}</td></tr><tr><td width=100%%>".format(n);
txt += "<div id=f{0} onkeydown='onKey();' onmouseup='onKey();'".format(i);
txt += " onfocus='onFocus(this);' onblur='onBlur();' class=field ";
txt += "contentEditable=true>{0}</div>".format(f);
txt += "</td></tr>";
}
$("#fields").html("<table cellpadding=0 width=100%%>"+txt+"</table>");
if (!focusTo) {
focusTo = 0;
}
$("#f"+focusTo).focus();
};
function setBackgrounds(cols) {
for (var i=0; i<cols.length; i++) {
$("#f"+i).css("background", cols[i]);
}
}
function setFonts(fonts) {
for (var i=0; i<fonts.length; i++) {
$("#f"+i).css("font-family", fonts[i][0]);
$("#f"+i).css("font-size", fonts[i][1]);
$("#f"+i)[0].dir = fonts[i][2] ? "rtl" : "ltr";
}
}
function showDupes() {
$("#dupes").show();
}
function hideDupes() {
$("#dupes").hide();
}
$(function () {
// ignore drops outside the editable area
document.body.ondragover = function () {
e = window.event.srcElement;
do {
if (e.contentEditable == "true") {
return;
}
e = window.parentNode;
} while (e);
window.event.preventDefault();
}
});
</script></head><body>
<div id="fields"></div>
<div id="dupes"><a href="#" onclick="py.run('dupes');return false;">%s</a></div>
</body></html>
"""
# caller is responsible for resetting note on reset
class Editor(object):
def __init__(self, mw, widget, parentWindow, addMode=False):
self.mw = mw
self.widget = widget
self.parentWindow = parentWindow
self.note = None
self.stealFocus = True
self.addMode = addMode
self._loaded = False
self._keepButtons = False
# current card, for card layout
self.card = None
self.setupOuter()
self.setupButtons()
self.setupWeb()
self.setupTagsAndDeck()
self.setupKeyboard()
# 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)
# 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()
if not isMac:
self.iconsBox.setMargin(6)
else:
self.iconsBox.setMargin(0)
self.iconsBox.setSpacing(0)
self.outerLayout.addLayout(self.iconsBox)
b = self._addButton
b("fields", self.onFields, "",
shortcut(_("Customize Fields")), size=False, text=_("Fields..."),
native=True, canDisable=False)
b("layout", self.onCardLayout, "Ctrl+l",
shortcut(_("Customize Card Layout (Ctrl+l)")),
size=False, text=_("Cards..."), native=True, canDisable=False)
# align to right
self.iconsBox.addItem(QSpacerItem(20,1, QSizePolicy.Expanding))
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_remove", self.removeFormat, "Ctrl+r",
_("Remove Formatting (Ctrl+r)"))
but = b("foreground", self.onForeground, "F7", text=" ")
self.setupForegroundButton(but)
but = b("cloze", self.onCloze, "Ctrl+Shift+c",
_("Cloze (Ctrl+Shift+c)"), text="[...]")
but.setFixedWidth(24)
# fixme: better image names
b("mail-attachment", self.onAddMedia, "F3",
_("Attach pictures/audio/video (F3)"))
b("media-record", self.onRecSound, "F5", _("Record audio (F5)"))
b("adv", self.onAdvanced, text=u"")
s = QShortcut(QKeySequence("Ctrl+t, t"), self.widget)
s.connect(s, SIGNAL("activated()"), self.insertLatex)
s = QShortcut(QKeySequence("Ctrl+t, e"), self.widget)
s.connect(s, SIGNAL("activated()"), self.insertLatexEqn)
s = QShortcut(QKeySequence("Ctrl+t, m"), self.widget)
s.connect(s, SIGNAL("activated()"), self.insertLatexMathEnv)
s = QShortcut(QKeySequence("Ctrl+shift+x"), self.widget)
s.connect(s, SIGNAL("activated()"), self.onHtmlEdit)
def enableButtons(self, val=True):
for b in self._buttons.values():
b.setEnabled(val)
def disableButtons(self):
self.enableButtons(False)
def onFields(self):
from aqt.fields import FieldDialog
self.saveNow()
FieldDialog(self.mw, self.note, parent=self.parentWindow)
def onCardLayout(self):
from aqt.clayout import CardLayout
self.saveNow()
if self.card:
ord = self.card.ord
else:
ord = 0
CardLayout(self.mw, self.note, ord=ord, parent=self.parentWindow,
addMode=self.addMode)
self.loadNote()
# 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)
self.note.fields[self.currentField] = self.mungeHTML(txt)
self.mw.requireReset()
if not self.addMode:
self.note.flush()
if type == "blur":
if not self._keepButtons:
self.disableButtons()
runHook("editFocusLost", self.note)
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 = simplejson.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()
else:
print str
def mungeHTML(self, txt):
if txt == "<br>":
txt = ""
return txt
# Setting/unsetting the current note
######################################################################
def _loadFinished(self, w):
self._loaded = True
if self.note:
self.loadNote()
def setNote(self, note, hide=True):
"Make NOTE the current note."
self.note = note
# change timer
if self.note:
self.web.setHtml(_html % (getBase(self.mw.col), anki.js.all,
_("Show Duplicates")),
loadCB=self._loadFinished)
self.updateTagsAndDeck()
self.updateKeyboard()
elif hide:
self.widget.hide()
def loadNote(self, field=0):
if not self._loaded:
# will be loaded when page is ready
return
self.web.eval("setFields(%s, %d);" % (
simplejson.dumps(self.note.items()), field))
self.web.eval("setFonts(%s);" % (
simplejson.dumps(self.fonts())))
self.checkValid()
self.widget.show()
if self.stealFocus:
self.web.setFocus()
def focus(self):
self.web.setFocus()
def fonts(self):
return [(f['font'], f['size'], f['rtl'])
for f in self.note.model()['flds']]
def saveNow(self):
"Must call this before adding cards, closing dialog, etc."
if not self.note:
return
self._keepButtons = True
self.web.eval("saveField('blur');")
self._keepButtons = False
self.saveTagsAndDeck()
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);" % simplejson.dumps(cols))
def showDupes(self):
contents = self.note.fields[0]
browser = aqt.dialogs.open("Browser", self.mw)
browser.form.searchEdit.setText(
"'model:%s' '%s:%s'" % (
self.note.model()['name'],
self.note.model()['flds'][0]['name'],
contents))
browser.onSearch()
def fieldsAreBlank(self):
if not self.note:
return True
for f in self.note.fields:
if f:
return False
return True
# HTML editing
######################################################################
def onHtmlEdit(self):
self.saveNow()
d = QDialog(self.widget)
form = aqt.forms.edithtml.Ui_Dialog()
form.setupUi(d)
d.connect(form.buttonBox, SIGNAL("helpRequested()"),
lambda: openHelp("HtmlEditor"))
form.textEdit.setPlainText(self.note.fields[self.currentField])
form.textEdit.moveCursor(QTextCursor.End)
d.exec_()
self.note.fields[self.currentField] = unicode(
form.textEdit.toPlainText())
self.loadNote(self.currentField)
# Tag & deck handling
######################################################################
def setupTagsAndDeck(self):
import aqt.tagedit
g = QGroupBox(self.widget)
g.setFlat(True)
tb = QGridLayout()
tb.setSpacing(12)
tb.setMargin(6)
# deck
if self.addMode:
l = QLabel(_("Deck"))
tb.addWidget(l, 0, 0)
self.deck = aqt.tagedit.TagEdit(self.widget, type=1)
self.deck.connect(self.deck, SIGNAL("lostFocus"),
self.saveTagsAndDeck)
tb.addWidget(self.deck, 0, 1)
else:
self.deck = None
# tags
l = QLabel(_("Tags"))
tb.addWidget(l, 1, 0)
self.tags = aqt.tagedit.TagEdit(self.widget)
self.tags.connect(self.tags, SIGNAL("lostFocus"),
self.saveTagsAndDeck)
tb.addWidget(self.tags, 1, 1)
g.setLayout(tb)
self.outerLayout.addWidget(g)
def updateTagsAndDeck(self):
if self.tags.col != self.mw.col:
if self.deck:
self.deck.setCol(self.mw.col)
self.tags.setCol(self.mw.col)
if self.addMode:
self.deck.setText(self.mw.col.decks.name(self.note.did))
self.tags.setText(self.note.stringTags().strip())
def saveTagsAndDeck(self):
if not self.note:
return
self.note.tags = self.mw.col.tags.split(self.tags.text())
if self.addMode:
self.note.did = self.mw.col.decks.id(self.deck.text())
# save group and tags to model
m = self.note.model()
m['did'] = self.note.did
m['tags'] = self.note.tags
self.mw.col.models.save(m)
if not self.addMode:
self.note.flush()
runHook("tagsUpdated", self.note)
# 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 onCloze(self):
# check that the model is set up for cloze deletion
if 'cloze' not in self.note.model()['tmpls'][0]['qfmt']:
showInfo(_("Cloze deletion requires a Cloze note type."),
help="ClozeDeletion")
return
f = self.note.fields[self.currentField]
# find the highest existing cloze
m = re.findall("\{\{c(\d+)::", f)
if m:
next = sorted([int(x) for x in m])[-1] + 1
else:
next = 1
self.web.eval("wrap('{{c%d::', '}}');" % next)
# Foreground colour
######################################################################
def setupForegroundButton(self, but):
self.foregroundFrame = QFrame()
self.foregroundFrame.setAutoFillBackground(True)
self.colourChanged()
hbox = QHBoxLayout()
hbox.addWidget(self.foregroundFrame)
hbox.setMargin(5)
but.setLayout(hbox)
def _updateForegroundButton(self, txtcol):
self.foregroundFrame.setPalette(QPalette(QColor(txtcol)))
self.foregroundFrame.setStyleSheet("* {background-color: %s}" %
txtcol)
def colourChanged(self):
recent = self.mw.pm.profile['recentColours']
self._updateForegroundButton(recent[-1])
def onForeground(self):
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)
p = ColourPopup(self.widget)
p.move(self.foregroundFrame.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(self.mw.pm.profile['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.setShortcut(QKeySequence("F5"))
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 = self.mw.pm.profile['recentColours']
recent.remove(colour)
if not recent:
recent.append("#000000")
self.colourDiag.close()
self.onForeground()
self.colourChanged()
def onNextColour(self):
self.colourDiag.focusWidget().nextInFocusChain().setFocus()
def onChooseColourKey(self):
try:
self.colourDiag.focusWidget().click()
except:
# dialog focused
pass
def onChooseColour(self, colour):
recent = self.mw.pm.profile['recentColours']
recent.remove(colour)
recent.append(colour)
self.web.eval("setFormat('forecolor', '%s')" % colour)
self.colourDiag.close()
self.colourChanged()
def onNewColour(self):
new = QColorDialog.getColor(Qt.white, self.widget)
self.widget.raise_()
recent = self.mw.pm.profile['recentColours']
if new.isValid():
txtcol = unicode(new.name())
if txtcol not in recent:
recent.append(txtcol)
self.colourChanged()
self.onChooseColour(txtcol)
# Audio/video/images
######################################################################
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, canDelete=True)
file = getFile(self.widget, _("Add Media"), accept, key, key="media")
def addMedia(self, path, canDelete=False):
html = self._addMedia(path, canDelete)
self.web.eval("setFormat('inserthtml', %s);" % simplejson.dumps(html))
def _addMedia(self, path, canDelete=False):
"Add to media folder and return basename."
# copy to media folder
name = self.mw.col.media.addFile(path)
# remove original?
if canDelete and self.mw.pm.profile['deleteMedia']:
if os.path.abspath(name) != os.path.abspath(path):
try:
os.unlink(old)
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
def onRecSound(self):
try:
file = getAudio(self.widget)
except Exception, e:
showWarning(_(
"Couldn't record audio. Have you installed lame and sox?") +
"\n\n" + unicode(e))
return
self.addMedia(file)
# Advanced menu
######################################################################
def onAdvanced(self):
m = QMenu(self.mw)
a = m.addAction(_("LaTeX"))
a.connect(a, SIGNAL("triggered()"), self.insertLatex)
a = m.addAction(_("LaTeX Equation"))
a.connect(a, SIGNAL("triggered()"), self.insertLatexEqn)
a = m.addAction(_("LaTeX Math Env."))
a.connect(a, SIGNAL("triggered()"), self.insertLatexMathEnv)
a = m.addAction(_("Edit HTML"))
a.connect(a, SIGNAL("triggered()"), self.onHtmlEdit)
m.exec_(QCursor.pos())
# LaTeX
######################################################################
def insertLatex(self):
self.web.eval("wrap('[latex]', '[/latex]');")
def insertLatexEqn(self):
self.web.eval("wrap('[$]', '[/$]');")
def insertLatexMathEnv(self):
self.web.eval("wrap('[$$]', '[/$$]');")
# Keyboard layout
######################################################################
def setupKeyboard(self):
if isWin and self.mw.pm.profile['preserveKeyboard']:
a = ctypes.windll.user32.ActivateKeyboardLayout
a.restype = ctypes.c_void_p
a.argtypes = [ctypes.c_void_p, ctypes.c_uint]
g = ctypes.windll.user32.GetKeyboardLayout
g.restype = ctypes.c_void_p
g.argtypes = [ctypes.c_uint]
else:
a = g = None
self.activateKeyboard = a
self.getKeyboard = g
def updateKeyboard(self):
self.keyboardLayouts = {}
def saveKeyboard(self):
if not self.getKeyboard:
return
self.keyboardLayouts[self.currentField] = self.getKeyboard(0)
def restoreKeyboard(self):
if not self.getKeyboard:
return
if self.currentField in self.keyboardLayouts:
self.activateKeyboard(self.keyboardLayouts[self.currentField], 0)
# Pasting, drag & drop, and keyboard layouts
######################################################################
class EditorWebView(AnkiWebView):
def __init__(self, parent, editor):
AnkiWebView.__init__(self)
self.editor = editor
self.errtxt = _("An error occured while opening %s")
self.strip = self.editor.mw.pm.profile['stripHTML']
def keyPressEvent(self, evt):
self._curKey = True
self.origClip = None
shiftPaste = (evt.modifiers() == (Qt.ShiftModifier | Qt.ControlModifier)
and evt.key() == Qt.Key_V)
if evt.matches(QKeySequence.Paste) or shiftPaste:
self.prepareClip(shiftPaste)
if shiftPaste:
self.triggerPageAction(QWebPage.Paste)
QWebView.keyPressEvent(self, evt)
if self.origClip:
self.restoreClip()
def contextMenuEvent(self, evt):
# adjust in case the user is going to paste
self.prepareClip()
QWebView.contextMenuEvent(self, evt)
self.restoreClip()
def dropEvent(self, evt):
oldmime = evt.mimeData()
# coming from this program?
if evt.source():
# if they're copying just an image, we need to turn it into html
# again
txt = ""
mime = QMimeData()
if not oldmime.hasHtml() and oldmime.hasUrls():
# qt gives it to us twice
txt += '<img src="%s">' % os.path.basename(
oldmime.urls()[0].toString())
mime.setHtml(txt)
else:
mime.setHtml(oldmime.html())
else:
mime = self._processMime(oldmime)
# create a new event with the new mime data
new = QDropEvent(evt.pos(), evt.possibleActions(), mime,
evt.mouseButtons(), evt.keyboardModifiers())
evt.accept()
QWebView.dropEvent(self, new)
def prepareClip(self, keep=False):
clip = self.editor.mw.app.clipboard()
mime = clip.mimeData()
self.saveClip()
if keep:
new = QMimeData()
if mime.hasHtml():
new.setHtml(mime.html())
else:
new.setText(mime.text())
mime = new
else:
mime = self._processMime(mime)
clip.setMimeData(mime)
def restoreClip(self):
clip = self.editor.mw.app.clipboard()
clip.setMimeData(self.origClip)
def saveClip(self):
# we don't own the clipboard object, so we need to copy it
mime = self.editor.mw.app.clipboard().mimeData()
n = QMimeData()
if mime.hasText():
n.setText(mime.text())
if mime.hasHtml():
n.setHtml(mime.html())
if mime.hasUrls():
n.setUrls(mime.urls())
if mime.hasImage():
n.setImageData(mime.imageData())
self.origClip = n
def _processMime(self, mime):
# print "html=%s image=%s urls=%s txt=%s" % (
# mime.hasHtml(), mime.hasImage(), mime.hasUrls(), mime.hasText())
# print "html", mime.html()
# print "urls", mime.urls()
# print "text", mime.text()
if mime.hasUrls():
return self._processUrls(mime)
elif mime.hasImage():
return self._processImage(mime)
elif mime.hasText() and (self.strip or not mime.hasHtml()):
return self._processText(mime)
elif mime.hasHtml():
return self._processHtml(mime)
else:
# nothing
return QMimeData()
def _processUrls(self, mime):
links = []
for url in mime.urls():
url = url.toString()
link = self._retrieveURL(url)
if link:
links.append(link)
mime = QMimeData()
mime.setHtml("".join(links))
return mime
def _processText(self, mime):
txt = unicode(mime.text())
l = txt.lower()
html = None
# firefox on linux just gives us a url for an image
if "\n" in l and (l.startswith("http://") or l.startswith("file://")):
txt = txt.split("\r\n")[0]
html = self._retrieveURL(txt)
new = QMimeData()
if html:
new.setHtml(html)
else:
new.setText(mime.text())
return new
def _processHtml(self, mime):
html = mime.html()
if self.strip:
html = stripHTML(html)
mime = QMimeData()
mime.setHtml(html)
return mime
def _processImage(self, mime):
im = QImage(mime.imageData())
name = namedtmp("paste-%d.png" % im.cacheKey())
uname = unicode(name, sys.getfilesystemencoding())
if im.hasAlphaChannel():
im.save(uname)
else:
im.save(uname, None, 95)
mime = QMimeData()
mime.setHtml(self.editor._addMedia(uname))
return mime
def _retrieveURL(self, url):
# is it media?
ext = url.split(".")[-1].lower()
if ext not in pics and ext not in audio:
return
# fetch it into a temporary folder
try:
req = urllib2.Request(url, None, {
'User-Agent': 'Mozilla/5.0 (compatible; Anki)'})
filecontents = urllib2.urlopen(req).read()
except urllib2.URLError, e:
showWarning(self.errtxt % e)
return
path = namedtmp(os.path.basename(url))
file = open(path, "wb")
file.write(filecontents)
file.close()
return self.editor._addMedia(path)