2012-12-21 08:51:59 +01:00
|
|
|
# Copyright: Damien Elmes <anki@ichi2.net>
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
|
|
|
|
import sys
|
2013-07-16 09:42:50 +02:00
|
|
|
from anki.hooks import runHook
|
2012-12-21 08:51:59 +01:00
|
|
|
from aqt.qt import *
|
|
|
|
from aqt.utils import openLink
|
2017-08-10 07:02:46 +02:00
|
|
|
from anki.utils import isMac, isWin, isLin, devMode
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Page for debug messages
|
|
|
|
##########################################################################
|
|
|
|
|
2016-05-31 10:51:40 +02:00
|
|
|
class AnkiWebPage(QWebEnginePage):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2016-06-06 07:50:03 +02:00
|
|
|
def __init__(self, onBridgeCmd):
|
2016-05-31 10:51:40 +02:00
|
|
|
QWebEnginePage.__init__(self)
|
2016-06-06 07:50:03 +02:00
|
|
|
self._onBridgeCmd = onBridgeCmd
|
|
|
|
self._setupBridge()
|
2017-06-22 10:01:01 +02:00
|
|
|
self.setBackgroundColor(Qt.transparent)
|
2016-06-06 07:50:03 +02:00
|
|
|
|
|
|
|
def _setupBridge(self):
|
|
|
|
class Bridge(QObject):
|
|
|
|
@pyqtSlot(str)
|
|
|
|
def cmd(self, str):
|
|
|
|
self.onCmd(str)
|
|
|
|
|
|
|
|
self._bridge = Bridge()
|
|
|
|
self._bridge.onCmd = self._onCmd
|
|
|
|
|
|
|
|
self._channel = QWebChannel(self)
|
|
|
|
self._channel.registerObject("py", self._bridge)
|
|
|
|
self.setWebChannel(self._channel)
|
|
|
|
|
|
|
|
js = QFile(':/qtwebchannel/qwebchannel.js')
|
|
|
|
assert js.open(QIODevice.ReadOnly)
|
|
|
|
js = bytes(js.readAll()).decode('utf-8')
|
|
|
|
|
|
|
|
script = QWebEngineScript()
|
|
|
|
script.setSourceCode(js + '''
|
|
|
|
var pycmd;
|
|
|
|
new QWebChannel(qt.webChannelTransport, function(channel) {
|
|
|
|
pycmd = channel.objects.py.cmd;
|
|
|
|
pycmd("domDone");
|
|
|
|
});
|
|
|
|
''')
|
|
|
|
script.setWorldId(QWebEngineScript.MainWorld)
|
|
|
|
script.setInjectionPoint(QWebEngineScript.DocumentReady)
|
|
|
|
script.setRunsOnSubFrames(False)
|
|
|
|
self.profile().scripts().insert(script)
|
2016-05-31 10:51:40 +02:00
|
|
|
|
|
|
|
def javaScriptConsoleMessage(self, lvl, msg, line, srcID):
|
2017-08-06 05:10:51 +02:00
|
|
|
# not translated because console usually not visible,
|
|
|
|
# and may only accept ascii text
|
|
|
|
sys.stdout.write("JS error on line %(a)d: %(b)s" %
|
|
|
|
dict(a=line, b=msg+"\n"))
|
2016-05-31 10:51:40 +02:00
|
|
|
|
|
|
|
def acceptNavigationRequest(self, url, navType, isMainFrame):
|
2017-07-17 04:40:38 +02:00
|
|
|
if not isMainFrame:
|
|
|
|
return True
|
2017-08-11 12:59:15 +02:00
|
|
|
from aqt import mw
|
|
|
|
# ignore href=#
|
|
|
|
if url.toString().startswith(mw.serverURL()):
|
|
|
|
return False
|
2016-06-06 07:50:03 +02:00
|
|
|
# load all other links in browser
|
|
|
|
openLink(url)
|
|
|
|
return False
|
|
|
|
|
|
|
|
def _onCmd(self, str):
|
2017-08-06 07:12:28 +02:00
|
|
|
self._onBridgeCmd(str)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Main web view
|
|
|
|
##########################################################################
|
|
|
|
|
2016-05-31 10:51:40 +02:00
|
|
|
class AnkiWebView(QWebEngineView):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2017-06-22 08:36:54 +02:00
|
|
|
def __init__(self, parent=None):
|
|
|
|
QWebEngineView.__init__(self, parent=parent)
|
2016-07-07 09:23:13 +02:00
|
|
|
self.title = "default"
|
2016-06-06 07:50:03 +02:00
|
|
|
self._page = AnkiWebPage(self._onBridgeCmd)
|
|
|
|
|
2017-08-01 06:30:04 +02:00
|
|
|
self._domDone = True
|
2017-08-07 08:01:35 +02:00
|
|
|
self._pendingActions = []
|
2012-12-21 08:51:59 +01:00
|
|
|
self.setPage(self._page)
|
2017-01-08 11:02:49 +01:00
|
|
|
|
2017-06-22 09:06:24 +02:00
|
|
|
self._page.profile().setHttpCacheType(QWebEngineProfile.NoCache)
|
2016-05-31 10:51:40 +02:00
|
|
|
self.resetHandlers()
|
2012-12-21 08:51:59 +01:00
|
|
|
self.allowDrops = False
|
2017-06-22 08:36:54 +02:00
|
|
|
QShortcut(QKeySequence("Esc"), self,
|
|
|
|
context=Qt.WidgetWithChildrenShortcut, activated=self.onEsc)
|
|
|
|
if isMac:
|
|
|
|
for key, fn in [
|
|
|
|
(QKeySequence.Copy, self.onCopy),
|
|
|
|
(QKeySequence.Paste, self.onPaste),
|
|
|
|
(QKeySequence.Cut, self.onCut),
|
|
|
|
(QKeySequence.SelectAll, self.onSelectAll),
|
|
|
|
]:
|
|
|
|
QShortcut(key, self,
|
|
|
|
context=Qt.WidgetWithChildrenShortcut,
|
|
|
|
activated=fn)
|
|
|
|
|
2017-06-23 06:34:56 +02:00
|
|
|
self.focusProxy().installEventFilter(self)
|
|
|
|
|
|
|
|
def eventFilter(self, obj, evt):
|
|
|
|
# disable pinch to zoom gesture
|
|
|
|
if isinstance(evt, QNativeGestureEvent):
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2017-06-22 08:36:54 +02:00
|
|
|
def onEsc(self):
|
|
|
|
w = self.parent()
|
|
|
|
while w:
|
|
|
|
if isinstance(w, QDialog) or isinstance(w, QMainWindow):
|
|
|
|
from aqt import mw
|
|
|
|
# esc in a child window closes the window
|
|
|
|
if w != mw:
|
|
|
|
w.close()
|
|
|
|
else:
|
|
|
|
# in the main window, removes focus from type in area
|
|
|
|
self.parent().setFocus()
|
|
|
|
break
|
|
|
|
w = w.parent()
|
2016-06-06 09:54:39 +02:00
|
|
|
|
|
|
|
def onCopy(self):
|
|
|
|
self.triggerPageAction(QWebEnginePage.Copy)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2016-06-06 09:54:39 +02:00
|
|
|
def onCut(self):
|
|
|
|
self.triggerPageAction(QWebEnginePage.Cut)
|
|
|
|
|
|
|
|
def onPaste(self):
|
|
|
|
self.triggerPageAction(QWebEnginePage.Paste)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2017-06-22 08:36:54 +02:00
|
|
|
def onSelectAll(self):
|
|
|
|
self.triggerPageAction(QWebEnginePage.SelectAll)
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def contextMenuEvent(self, evt):
|
|
|
|
m = QMenu(self)
|
|
|
|
a = m.addAction(_("Copy"))
|
2017-06-22 08:36:54 +02:00
|
|
|
a.triggered.connect(self.onCopy)
|
2013-07-16 09:42:50 +02:00
|
|
|
runHook("AnkiWebView.contextMenuEvent", self, m)
|
2012-12-21 08:51:59 +01:00
|
|
|
m.popup(QCursor.pos())
|
|
|
|
|
|
|
|
def dropEvent(self, evt):
|
|
|
|
pass
|
|
|
|
|
2016-05-31 10:51:40 +02:00
|
|
|
def setHtml(self, html):
|
2017-08-07 08:01:35 +02:00
|
|
|
self._queueAction("setHtml", html)
|
|
|
|
|
|
|
|
def _setHtml(self, html):
|
2016-05-31 10:51:40 +02:00
|
|
|
app = QApplication.instance()
|
|
|
|
oldFocus = app.focusWidget()
|
2017-08-01 06:30:04 +02:00
|
|
|
self._domDone = False
|
2016-05-31 10:51:40 +02:00
|
|
|
self._page.setHtml(html)
|
|
|
|
# work around webengine stealing focus on setHtml()
|
|
|
|
if oldFocus:
|
|
|
|
oldFocus.setFocus()
|
|
|
|
|
2017-08-10 07:02:46 +02:00
|
|
|
# need to do this manually for Linux as Qt doesn't automatically scale webview
|
2016-06-07 06:27:33 +02:00
|
|
|
def zoomFactor(self):
|
2017-08-10 07:02:46 +02:00
|
|
|
if not isLin:
|
|
|
|
return 1
|
2016-06-07 06:27:33 +02:00
|
|
|
screen = QApplication.desktop().screen()
|
|
|
|
dpi = screen.logicalDpiX()
|
|
|
|
return max(1, dpi / 96.0)
|
|
|
|
|
2017-08-10 11:02:32 +02:00
|
|
|
def stdHtml(self, body, css=[], js=["jquery.js"], head=""):
|
2016-07-08 08:17:06 +02:00
|
|
|
if isWin:
|
2016-07-12 08:40:13 +02:00
|
|
|
buttonspec = "button { font-size: 12px; font-family:'Segoe UI'; }"
|
2016-07-12 08:28:14 +02:00
|
|
|
fontspec = 'font-size:12px;font-family:"Segoe UI";'
|
2016-07-08 08:17:06 +02:00
|
|
|
elif isMac:
|
2016-07-26 02:34:16 +02:00
|
|
|
family="Helvetica"
|
|
|
|
fontspec = 'font-size:15px;font-family:"%s";'% \
|
2016-07-12 08:28:14 +02:00
|
|
|
family
|
|
|
|
buttonspec = """
|
2016-07-26 02:34:16 +02:00
|
|
|
button { font-size: 13px; -webkit-appearance: none; background: #fff; border: 1px solid #ccc;
|
|
|
|
border-radius:5px; font-family: Helvetica }"""
|
2016-07-08 08:17:06 +02:00
|
|
|
else:
|
2016-07-12 08:40:13 +02:00
|
|
|
buttonspec = ""
|
2016-07-08 08:17:06 +02:00
|
|
|
family = self.font().family()
|
2016-07-12 08:28:14 +02:00
|
|
|
fontspec = 'font-size:14px;font-family:%s;'%\
|
2016-07-08 08:17:06 +02:00
|
|
|
family
|
2017-08-10 11:02:32 +02:00
|
|
|
csstxt = "\n".join([self.bundledCSS("webview.css")]+
|
|
|
|
[self.bundledCSS(fname) for fname in css])
|
2017-08-01 09:04:55 +02:00
|
|
|
jstxt = "\n".join([self.bundledScript("webview.js")]+
|
|
|
|
[self.bundledScript(fname) for fname in js])
|
2017-08-11 12:59:15 +02:00
|
|
|
from aqt import mw
|
|
|
|
head = mw.baseHTML() + head + csstxt + jstxt
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2017-08-10 11:02:32 +02:00
|
|
|
html=f"""
|
|
|
|
<!doctype html>
|
|
|
|
<html><head>
|
|
|
|
<title>{self.title}</title>
|
|
|
|
|
|
|
|
<style>
|
|
|
|
body {{ zoom: {self.zoomFactor()}; {fontspec} }}
|
|
|
|
{buttonspec}
|
|
|
|
</style>
|
|
|
|
|
|
|
|
{head}
|
2012-12-21 08:51:59 +01:00
|
|
|
</head>
|
2017-08-10 11:02:32 +02:00
|
|
|
|
|
|
|
<body>{body}</body>
|
|
|
|
</html>"""
|
2017-06-22 10:01:01 +02:00
|
|
|
#print(html)
|
|
|
|
self.setHtml(html)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2017-07-28 08:19:06 +02:00
|
|
|
def webBundlePath(self, path):
|
|
|
|
from aqt import mw
|
2017-08-08 06:56:34 +02:00
|
|
|
return "http://localhost:%d/_anki/%s" % (mw.mediaServer.getPort(), path)
|
2017-07-28 08:19:06 +02:00
|
|
|
|
|
|
|
def bundledScript(self, fname):
|
|
|
|
return '<script src="%s"></script>' % self.webBundlePath(fname)
|
|
|
|
|
|
|
|
def bundledCSS(self, fname):
|
|
|
|
return '<link rel="stylesheet" type="text/css" href="%s">' % self.webBundlePath(fname)
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def eval(self, js):
|
2017-08-02 07:39:49 +02:00
|
|
|
self.evalWithCallback(js, None)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2016-06-07 06:27:33 +02:00
|
|
|
def evalWithCallback(self, js, cb):
|
2017-08-07 08:01:35 +02:00
|
|
|
self._queueAction("eval", js, cb)
|
|
|
|
|
|
|
|
def _evalWithCallback(self, js, cb):
|
|
|
|
if cb:
|
|
|
|
self.page().runJavaScript(js, cb)
|
2017-08-02 07:39:49 +02:00
|
|
|
else:
|
2017-08-07 08:01:35 +02:00
|
|
|
self.page().runJavaScript(js)
|
|
|
|
|
|
|
|
def _queueAction(self, name, *args):
|
|
|
|
self._pendingActions.append((name, args))
|
|
|
|
self._maybeRunActions()
|
|
|
|
|
|
|
|
def _maybeRunActions(self):
|
|
|
|
while self._pendingActions and self._domDone:
|
|
|
|
name, args = self._pendingActions.pop(0)
|
|
|
|
|
|
|
|
if name == "eval":
|
|
|
|
self._evalWithCallback(*args)
|
|
|
|
elif name == "setHtml":
|
|
|
|
self._setHtml(*args)
|
|
|
|
else:
|
|
|
|
raise Exception("unknown action: {}".format(name))
|
2016-06-07 06:27:33 +02:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def _openLinksExternally(self, url):
|
|
|
|
openLink(url)
|
|
|
|
|
2016-06-06 07:50:03 +02:00
|
|
|
def _onBridgeCmd(self, cmd):
|
2017-08-07 05:12:47 +02:00
|
|
|
# ignore webchannel messages that arrive after underlying webview
|
|
|
|
# deleted
|
|
|
|
if sip.isdeleted(self):
|
|
|
|
return
|
|
|
|
|
2016-06-06 07:50:03 +02:00
|
|
|
if cmd == "domDone":
|
2017-08-01 06:30:04 +02:00
|
|
|
self._domDone = True
|
2017-08-07 08:01:35 +02:00
|
|
|
self._maybeRunActions()
|
2016-06-06 07:50:03 +02:00
|
|
|
else:
|
|
|
|
self.onBridgeCmd(cmd)
|
2016-05-31 10:51:40 +02:00
|
|
|
|
2016-06-06 07:50:03 +02:00
|
|
|
def defaultOnBridgeCmd(self, cmd):
|
|
|
|
print("unhandled bridge cmd:", cmd)
|
2016-05-31 10:51:40 +02:00
|
|
|
|
|
|
|
def resetHandlers(self):
|
2016-06-06 07:50:03 +02:00
|
|
|
self.onBridgeCmd = self.defaultOnBridgeCmd
|
2017-08-02 08:22:54 +02:00
|
|
|
|
|
|
|
def adjustHeightToFit(self):
|
|
|
|
self.evalWithCallback("$(document.body).height()", self._onHeight)
|
|
|
|
|
|
|
|
def _onHeight(self, qvar):
|
|
|
|
height = int(qvar*self.zoomFactor())
|
|
|
|
self.setFixedHeight(height)
|