anki/aqt/webview.py
Damien Elmes 76e508e25d various key handling fixes
- key presses while a webview is focused no longer make it to the
main window's keyPressEvent() routine, so AnkiWebView now uses its
event filter to pass the key events to the main window
- move the shared key handling out of keyPressEvent into
globalKeyHandler()
- make sure all key handling routines return true or false to
indicate if an event was handled or not
- remove focus when esc hit in the main window, to retain old
behaviour of allowing esc to clear focus from the type answer box
2017-06-06 15:56:21 +10:00

243 lines
7.1 KiB
Python

# 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
from anki.hooks import runHook
from aqt.qt import *
from aqt.utils import openLink
from anki.utils import isMac, isWin
import anki.js
# Page for debug messages
##########################################################################
class AnkiWebPage(QWebEnginePage):
def __init__(self, onBridgeCmd):
QWebEnginePage.__init__(self)
self._onBridgeCmd = onBridgeCmd
self._setupBridge()
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)
def javaScriptConsoleMessage(self, lvl, msg, line, srcID):
sys.stdout.write(
(_("JS error on line %(a)d: %(b)s") %
dict(a=line, b=msg+"\n")))
def acceptNavigationRequest(self, url, navType, isMainFrame):
# load all other links in browser
openLink(url)
return False
def _onCmd(self, str):
self._onBridgeCmd(str)
# Main web view
##########################################################################
class AnkiWebView(QWebEngineView):
def __init__(self, canFocus=True):
QWebEngineView.__init__(self)
self.title = "default"
self._page = AnkiWebPage(self._onBridgeCmd)
self._loadFinishedCB = None
self.setPage(self._page)
self.keyEventDelegate = None
self._page.profile().setHttpCacheType(QWebEngineProfile.MemoryHttpCache)
self.resetHandlers()
self.allowDrops = False
self.setCanFocus(canFocus)
self.installEventFilter(self)
def eventFilter(self, obj, evt):
if not isinstance(evt, QKeyEvent) or obj != self:
return False
if evt.matches(QKeySequence.Copy) and isMac:
self.onCopy()
return True
if evt.matches(QKeySequence.Cut) and isMac:
self.onCut()
return True
if evt.matches(QKeySequence.Paste) and isMac:
self.onPaste()
return True
if evt.matches(QKeySequence.SelectAll):
self.triggerPageAction(QWebEnginePage.SelectAll)
return False
if evt.key() == Qt.Key_Escape:
# cheap hack to work around webengine swallowing escape key that
# usually closes dialogs
w = self.parent()
while w:
if isinstance(w, QDialog) or isinstance(w, QMainWindow):
from aqt import mw
if w != mw:
w.close()
else:
self.clearFocus()
break
w = w.parent()
return True
if self.keyEventDelegate:
ret = self.keyEventDelegate(evt)
if ret is None:
raise Exception("add-ons that modify key handlers should make sure true/false is returned")
return ret
return False
def onCopy(self):
self.triggerPageAction(QWebEnginePage.Copy)
def onCut(self):
self.triggerPageAction(QWebEnginePage.Cut)
def onPaste(self):
self.triggerPageAction(QWebEnginePage.Paste)
def contextMenuEvent(self, evt):
if not self._canFocus:
return
m = QMenu(self)
a = m.addAction(_("Copy"))
a.triggered.connect(lambda: self.triggerPageAction(QWebEnginePage.Copy))
runHook("AnkiWebView.contextMenuEvent", self, m)
m.popup(QCursor.pos())
def dropEvent(self, evt):
pass
def setHtml(self, html):
app = QApplication.instance()
oldFocus = app.focusWidget()
self._page.setHtml(html)
# work around webengine stealing focus on setHtml()
if oldFocus:
oldFocus.setFocus()
def zoomFactor(self):
screen = QApplication.desktop().screen()
dpi = screen.logicalDpiX()
return max(1, dpi / 96.0)
def stdHtml(self, body, css="", bodyClass="", js=None, head=""):
if isWin:
buttonspec = "button { font-size: 12px; font-family:'Segoe UI'; }"
fontspec = 'font-size:12px;font-family:"Segoe UI";'
elif isMac:
family="Helvetica"
fontspec = 'font-size:15px;font-family:"%s";'% \
family
buttonspec = """
button { font-size: 13px; -webkit-appearance: none; background: #fff; border: 1px solid #ccc;
border-radius:5px; font-family: Helvetica }"""
else:
buttonspec = ""
family = self.font().family()
fontspec = 'font-size:14px;font-family:%s;'%\
family
self.setHtml("""
<!doctype html>
<html><head><title>%s</title><style>
body { zoom: %f; %s }
%s
%s</style>
<script>
%s
// prevent backspace key from going back a page
document.addEventListener("keydown", function(evt) {
if (evt.keyCode != 8) {
return;
}
var isText = 0;
var nn = evt.target.nodeName;
if (nn == "INPUT" || nn == "TEXTAREA") {
isText = 1;
} else if (nn == "DIV" && evt.target.contentEditable) {
isText = 1;
}
if (!isText) {
evt.preventDefault();
}
});
</script>
%s
</head>
<body class="%s">%s</body></html>""" % (
self.title,
self.zoomFactor(),
fontspec,
buttonspec,
css, js or anki.js.jquery+anki.js.browserSel,
head, bodyClass, body))
def setCanFocus(self, canFocus=False):
self._canFocus = canFocus
if self._canFocus:
self.setFocusPolicy(Qt.WheelFocus)
else:
self.setFocusPolicy(Qt.NoFocus)
def eval(self, js):
self.page().runJavaScript(js)
def evalWithCallback(self, js, cb):
self.page().runJavaScript(js, cb)
def _openLinksExternally(self, url):
openLink(url)
def _onBridgeCmd(self, cmd):
if cmd == "domDone":
self.onLoadFinished()
else:
self.onBridgeCmd(cmd)
def defaultOnBridgeCmd(self, cmd):
print("unhandled bridge cmd:", cmd)
def defaultOnLoadFinished(self):
pass
def resetHandlers(self):
self.onBridgeCmd = self.defaultOnBridgeCmd
self.onLoadFinished = self.defaultOnLoadFinished