mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
937 lines
31 KiB
Python
937 lines
31 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 * # fixme: obsolete?
|
|
from PyQt4.QtWebKit import QWebView
|
|
import re, os, sys, tempfile, urllib2, ctypes, simplejson
|
|
from anki.utils import stripHTML
|
|
from anki.sound import play
|
|
from anki.hooks import addHook, removeHook, runHook, runFilter
|
|
from aqt.sound import getAudio
|
|
from aqt.webview import AnkiWebView
|
|
from aqt.utils import shortcut, showInfo, showWarning, getBase
|
|
import anki.js
|
|
|
|
_html = """
|
|
<html><head>%s<style>
|
|
.field {
|
|
border: 1px solid #aaa; background:#fff; color:#000; padding: 5px;
|
|
}
|
|
.fname { font-size: 14px; vertical-align: middle; padding-right: 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"); }, 200);
|
|
};
|
|
|
|
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) {
|
|
document.execCommand(cmd, false, arg);
|
|
};
|
|
|
|
function clearChangeTimer() {
|
|
if (changeTimer) {
|
|
clearTimeout(changeTimer);
|
|
changeTimer = null;
|
|
}
|
|
};
|
|
|
|
function onFocus(elem) {
|
|
currentField = elem;
|
|
setTimeout(foo, 1);
|
|
}
|
|
|
|
function foo() {
|
|
var s = document.getSelection();
|
|
if (s.rangeCount) {
|
|
var r = s.getRangeAt(0);
|
|
r.collapse();
|
|
s.removeAllRanges();
|
|
s.addRange(r);
|
|
}
|
|
};
|
|
|
|
function onBlur() {
|
|
if (currentField) {
|
|
saveField("focus");
|
|
}
|
|
clearChangeTimer();
|
|
currentField = null;
|
|
};
|
|
|
|
function saveField(type) {
|
|
// type is either 'focus' or 'key'
|
|
py.run(type + ":" + currentField.id.substring(1) + ":" + currentField.innerHTML);
|
|
};
|
|
|
|
function cloze() {
|
|
var s = window.getSelection()
|
|
var r = s.getRangeAt(0).cloneContents();
|
|
var c = document.createElement('div');
|
|
c.appendChild(r);
|
|
var txt = c.innerHTML;
|
|
py.run("cloze:" + currentField.id.substring(1) + ":" + txt);
|
|
};
|
|
|
|
function setFields(fields) {
|
|
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><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=3>"+txt+"</table>");
|
|
$("#f0").focus();
|
|
};
|
|
|
|
$(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>
|
|
</body></html>
|
|
"""
|
|
|
|
# fixme: use shortcut() for mac shortcuts
|
|
|
|
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 Editor(object):
|
|
def __init__(self, mw, widget):
|
|
self.widget = widget
|
|
self.mw = mw
|
|
self.fact = None
|
|
self.onChange = None
|
|
self._loaded = False
|
|
# to be handled js side
|
|
#self.lastFocusedEdit = None
|
|
self.changeTimer = None
|
|
# current card, for card layout
|
|
self.card = None
|
|
addHook("deckClosed", self.deckClosedHook)
|
|
addHook("guiReset", self.refresh)
|
|
addHook("colourChanged", self.colourChanged)
|
|
self.setupOuter()
|
|
self.setupButtons()
|
|
self.setupWeb()
|
|
self.setupTags()
|
|
|
|
def close(self):
|
|
removeHook("deckClosed", self.deckClosedHook)
|
|
removeHook("guiReset", self.refresh)
|
|
removeHook("colourChanged", self.colourChanged)
|
|
|
|
# Initial setup
|
|
############################################################
|
|
|
|
def setupOuter(self):
|
|
l = QVBoxLayout()#self.widget)
|
|
l.setMargin(0)
|
|
l.setSpacing(3)
|
|
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):
|
|
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)
|
|
b.setStyle(self.plastiqueStyle)
|
|
b.setFocusPolicy(Qt.NoFocus)
|
|
if not text:
|
|
b.setIcon(QIcon(":/icons/%s.png" % name))
|
|
if key:
|
|
b.setShortcut(key)
|
|
if tip:
|
|
b.setToolTip(tip)
|
|
if check:
|
|
b.setCheckable(True)
|
|
self.iconsBox.addWidget(b)
|
|
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(0)
|
|
self.iconsBox.setSpacing(0)
|
|
self.outerLayout.addLayout(self.iconsBox)
|
|
# align to right
|
|
self.iconsBox.addItem(QSpacerItem(20,1, QSizePolicy.Expanding))
|
|
b = self._addButton
|
|
b("layout", self.onCardLayout, "Ctrl+l",
|
|
shortcut(_("Layout (Ctrl+l)")), size=False, text=_("Layout..."))
|
|
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",
|
|
_("Subscript (Ctrl+r)"))
|
|
but = b("foreground", self.onForeground, "F7", text=" ")
|
|
self.setupForegroundButton(but)
|
|
but = b("cloze", self.onCloze, "F9", _("Cloze (F9)"), text="[...]")
|
|
but.setFixedWidth(24)
|
|
# fixme: better image names
|
|
but = b("colors", self.onAddPicture, "F3", _("Add picture (F3)"))
|
|
but = b("text-speak", self.onAddSound, "F3", _("Add audio/video (F4)"))
|
|
but = b("media-record", self.onRecSound, "F5", _("Record audio (F5)"))
|
|
but = b("tex", self.latexMenu, "Ctrl+t", _("LaTeX (Ctrl+t)"))
|
|
# insertLatex, insertLatexEqn, insertLatexMathEnv
|
|
but = b("text-xml", self.onHtmlEdit, "Ctrl+x", _("Source (Ctrl+x)"))
|
|
|
|
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 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 onCardLayout(self):
|
|
from aqt.clayout import CardLayout
|
|
if self.card:
|
|
type = 1; ord = self.card.ord
|
|
else:
|
|
type = 0; ord = 0
|
|
CardLayout(self.mw, self.fact, type=type, ord=ord, parent=self.widget)
|
|
|
|
# JS->Python bridge
|
|
######################################################################
|
|
|
|
def bridge(self, str):
|
|
print str
|
|
if str.startswith("focus") or str.startswith("key"):
|
|
(type, num, txt) = str.split(":", 2)
|
|
self.fact._fields[int(num)] = txt
|
|
if type == "focus":
|
|
runHook("editor.focusLost", self.fact)
|
|
else:
|
|
runHook("editor.keyPressed", self.fact)
|
|
self.fact.flush()
|
|
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("cloze"):
|
|
(cmd, num, txt) = str.split(":", 2)
|
|
if not txt:
|
|
showInfo(_("Please select some text first."),
|
|
help="ClozeDeletion")
|
|
return
|
|
# check that the model is set up for cloze deletion
|
|
ok = False
|
|
for t in self.fact.model().templates:
|
|
if "cloze" in t['qfmt'] or "cloze" in t['afmt']:
|
|
ok = True
|
|
break
|
|
if not ok:
|
|
showInfo(_("Please add a cloze deletion model."),
|
|
help="ClozeDeletion")
|
|
return
|
|
num = int(num)
|
|
f = self.fact._fields[num]
|
|
# 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.fact._fields[num] = f.replace(
|
|
txt, "{{c%d::%s}}" % (next, txt))
|
|
self.loadFact()
|
|
|
|
# Setting/unsetting the current fact
|
|
######################################################################
|
|
|
|
def _loadFinished(self, w):
|
|
self._loaded = True
|
|
if self.fact:
|
|
self.loadFact()
|
|
|
|
def setFact(self, fact):
|
|
"Make FACT the current fact."
|
|
self.fact = fact
|
|
if self.changeTimer:
|
|
self.changeTimer.stop()
|
|
self.changeTimer = None
|
|
if self.fact:
|
|
self.web.setHtml(_html % (getBase(self.mw.deck), anki.js.all),
|
|
loadCB=self._loadFinished)
|
|
self.updateTags()
|
|
else:
|
|
self.widget.hide()
|
|
|
|
def loadFact(self):
|
|
if not self._loaded:
|
|
# will be loaded when page is ready
|
|
return
|
|
# fixme: focus on first widget
|
|
self.web.eval("setFields(%s);" % simplejson.dumps(self.fact.items()))
|
|
self.widget.show()
|
|
|
|
def refresh(self):
|
|
if self.fact:
|
|
self.fact.load()
|
|
# fixme: what if fact is deleted?
|
|
self.setFact(self.fact)
|
|
|
|
def deckClosedHook(self):
|
|
self.setFact(None)
|
|
|
|
# if field.fieldModel.features:
|
|
# w.setLayoutDirection(Qt.RightToLeft)
|
|
# else:
|
|
# w.setLayoutDirection(Qt.LeftToRight)
|
|
|
|
# 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
|
|
|
|
if check:
|
|
self.checkValid()
|
|
|
|
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)
|
|
|
|
def onHtmlEdit(self):
|
|
def helpRequested():
|
|
aqt.openHelp("HtmlEditor")
|
|
w = self.focusedEdit()
|
|
if w:
|
|
self.saveFields()
|
|
d = QDialog(self.widget)
|
|
form = aqt.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()
|
|
|
|
# Tag and group handling
|
|
######################################################################
|
|
|
|
def setupTags(self):
|
|
import aqt.tagedit
|
|
g = QGroupBox(self.widget)
|
|
tb = QGridLayout()
|
|
tb.setSpacing(12)
|
|
tb.setMargin(6)
|
|
# group
|
|
l = QLabel(_("Group"))
|
|
tb.addWidget(l, 0, 0)
|
|
self.group = aqt.tagedit.TagEdit(self.widget, type=1)
|
|
self.group.connect(self.group, SIGNAL("lostFocus"),
|
|
self.onGroupChange)
|
|
tb.addWidget(self.group, 0, 1)
|
|
# tags
|
|
l = QLabel(_("Tags"))
|
|
tb.addWidget(l, 1, 0)
|
|
self.tags = aqt.tagedit.TagEdit(self.widget)
|
|
self.tags.connect(self.tags, SIGNAL("lostFocus"),
|
|
self.onTagChange)
|
|
tb.addWidget(self.tags, 1, 1)
|
|
g.setLayout(tb)
|
|
self.outerLayout.addWidget(g)
|
|
|
|
def updateTags(self):
|
|
if self.tags.deck != self.mw.deck:
|
|
self.tags.setDeck(self.mw.deck)
|
|
self.group.setDeck(self.mw.deck)
|
|
self.group.setText(self.mw.deck.groupName(
|
|
self.fact.model().conf['gid']))
|
|
|
|
def onGroupChange(self):
|
|
pass
|
|
|
|
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.db.flush()
|
|
self.deck.updateFactTags([self.fact.id])
|
|
self.fact.setModified(textChanged=True, deck=self.deck)
|
|
self.deck.flushMod()
|
|
self.mw.reset(runHooks=False)
|
|
if self.onChange:
|
|
self.onChange('tag')
|
|
|
|
# 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):
|
|
self.removeFormat()
|
|
self.web.eval("cloze();")
|
|
|
|
# Foreground colour
|
|
######################################################################
|
|
|
|
def _updateForegroundButton(self, txtcol):
|
|
self.foregroundFrame.setPalette(QPalette(QColor(txtcol)))
|
|
self.foregroundFrame.setStyleSheet("* {background-color: %s}" %
|
|
txtcol)
|
|
|
|
def colourChanged(self):
|
|
recent = self.mw.config['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.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 = self.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 = self.mw.config['recentColours']
|
|
recent.remove(colour)
|
|
recent.append(colour)
|
|
self.web.eval("setFormat('forecolor', '%s')" % colour)
|
|
self.colourDiag.close()
|
|
runHook("colourChanged")
|
|
|
|
def onNewColour(self):
|
|
new = QColorDialog.getColor(Qt.white, self.widget)
|
|
self.widget.raise_()
|
|
recent = self.mw.config['recentColours']
|
|
if new.isValid():
|
|
txtcol = unicode(new.name())
|
|
if txtcol not in recent:
|
|
recent.append(txtcol)
|
|
runHook("colourChanged")
|
|
self.onChooseColour(txtcol)
|
|
|
|
# Audio/video/images
|
|
######################################################################
|
|
|
|
def initMedia(self):
|
|
os.chdir(self.deck.mediaDir(create=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.widget, _("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.widget)
|
|
|
|
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.widget, _("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 self.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.widget)
|
|
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.widget)
|
|
return
|
|
raise
|
|
if file:
|
|
self._addSound(file, w, copy=False)
|
|
|
|
# LaTeX
|
|
######################################################################
|
|
|
|
def latexMenu(self):
|
|
pass
|
|
|
|
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)
|
|
|
|
# Pasting, drag & drop, and keyboard layouts
|
|
######################################################################
|
|
|
|
class EditorWebView(AnkiWebView):
|
|
|
|
pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif")
|
|
audio = ("wav", "mp3", "ogg", "flac")
|
|
|
|
def __init__(self, parent, editor):
|
|
AnkiWebView.__init__(self, parent)
|
|
self.editor = editor
|
|
self.__tmpDir = None
|
|
self.errtxt = _("An error occured while opening %s")
|
|
# if sys.platform.startswith("win32"):
|
|
# self._ownLayout = None
|
|
|
|
# after the drop/copy, make sure data updated?
|
|
|
|
def keyPressEvent(self, evt):
|
|
self._curKey = True
|
|
if evt.matches(QKeySequence.Paste):
|
|
self.onPaste()
|
|
return QWebView.keyPressEvent(self, evt)
|
|
|
|
def contextMenuEvent(self, evt):
|
|
# adjust in case the user is going to paste
|
|
self.onPaste()
|
|
QWebView.contextMenuEvent(self, evt)
|
|
|
|
def dropEvent(self, evt):
|
|
oldmime = evt.mimeData()
|
|
# coming from us?
|
|
if evt.source() == self:
|
|
# if they're copying just an image, we need to turn it into html
|
|
# again
|
|
txt = ""
|
|
if not oldmime.hasHtml() and oldmime.hasUrls():
|
|
# qt gives it to us twice
|
|
txt += '<img src="%s">' % os.path.basename(
|
|
unicode(oldmime.urls()[0].toString()))
|
|
mime = QMimeData()
|
|
mime.setHtml(txt)
|
|
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 onPaste(self):
|
|
clip = self.editor.mw.app.clipboard()
|
|
mime = clip.mimeData()
|
|
mime = self._processMime(mime)
|
|
clip.setMimeData(mime)
|
|
|
|
def _processMime(self, mime):
|
|
print "html=%s image=%s urls=%s txt=%s" % (
|
|
mime.hasHtml(), mime.hasImage(), mime.hasUrls(), mime.hasText())
|
|
if mime.hasUrls():
|
|
return self._processUrls(mime)
|
|
if mime.hasText() and (self.mw.config['stripHTML'] or
|
|
not mime.hasHtml()):
|
|
txt = unicode(mime.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 mime.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(mime.text())
|
|
return True
|
|
except urllib2.URLError, e:
|
|
ui.utils.showWarning(errtxt % e)
|
|
else:
|
|
self.insertPlainText(mime.text())
|
|
return True
|
|
if mime.hasImage():
|
|
im = QImage(mime.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 mime.hasHtml():
|
|
self.insertHtml(self.simplifyHTML(unicode(mime.html())))
|
|
return True
|
|
|
|
def _processUrls(self, mime):
|
|
links = []
|
|
for url in mime.urls():
|
|
url = unicode(url.toString())
|
|
link = self._retrieveURL(url)
|
|
if link:
|
|
links.append(link)
|
|
mime = QMimeData()
|
|
mime.setHtml("".join(links))
|
|
return mime
|
|
|
|
def _retrieveURL(self, url):
|
|
# 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 = os.path.join(self._tmpDir(), os.path.basename(url))
|
|
file = open(path, "wb")
|
|
file.write(filecontents)
|
|
file.close()
|
|
# copy to media folder
|
|
name = self.editor.mw.deck.media.addFile(path)
|
|
print "name was", name
|
|
# return a local html link
|
|
ext = name.split(".")[-1].lower()
|
|
if ext in self.pics:
|
|
return '<img src="%s">' % name
|
|
else:
|
|
# FIXME: should also autoplay audio
|
|
return '[sound:%s]' % name
|
|
|
|
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."
|
|
# fixme
|
|
if not self.mw.config['stripHTML']:
|
|
return html
|
|
html = stripHTML(html)
|
|
return html
|
|
|
|
# def focusOutEvent(self, evt):
|
|
# if self.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.mw.config['preserveKeyboard'] and sys.platform.startswith("win32"):
|
|
# self._programLayout = GetKeyboardLayout(0)
|
|
# if self._ownLayout == None:
|
|
# self._ownLayout = self._programLayout
|
|
# ActivateKeyboardLayout(self._ownLayout, 0)
|