In Qt6 QWebEnginepage and QWebEngineView can be imported separately before constructing the QApplication

This commit is contained in:
Kovid Goyal 2021-11-21 17:14:29 +05:30
parent 1c0c88a2f9
commit 8eb7706b8a
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
11 changed files with 151 additions and 169 deletions

View File

@ -16,7 +16,7 @@ from qt.webengine import (
from calibre import detect_ncpus as cpu_count, prints
from calibre.ebooks.oeb.polish.check.base import ERROR, WARN, BaseError
from calibre.gui2 import must_use_qt
from calibre.gui2.webengine import secure_webengine
from calibre.utils.webengine import secure_webengine
class CSSParseError(BaseError):

View File

@ -31,7 +31,7 @@ from calibre.ebooks.pdf.image_writer import (
)
from calibre.ebooks.pdf.render.serialize import PDFStream
from calibre.gui2 import setup_unix_signals
from calibre.gui2.webengine import secure_webengine
from calibre.utils.webengine import secure_webengine
from calibre.srv.render_book import check_for_maths
from calibre.utils.fonts.sfnt.container import Sfnt, UnsupportedFont
from calibre.utils.fonts.sfnt.errors import NoGlyphs

View File

@ -12,7 +12,7 @@ from qt.webengine import QWebEnginePage, QWebEngineScript
from calibre.ebooks.metadata.pdf import page_images
from calibre.gui2 import must_use_qt
from calibre.gui2.webengine import secure_webengine
from calibre.utils.webengine import secure_webengine
from calibre.utils.filenames import atomic_rename
from calibre.utils.monotonic import monotonic

View File

@ -11,7 +11,7 @@ from qt.webengine import QWebEnginePage, QWebEngineScript, QWebEngineView
from calibre.gui2 import error_dialog, gprefs, is_dark_theme, question_dialog
from calibre.gui2.palette import dark_color, dark_link_color, dark_text_color
from calibre.gui2.webengine import secure_webengine
from calibre.utils.webengine import secure_webengine
from calibre.utils.logging import default_log
from calibre.utils.short_uuid import uuid4

View File

@ -33,10 +33,11 @@ from calibre.gui2.palette import dark_color, dark_link_color, dark_text_color
from calibre.gui2.tweak_book import TOP, actions, current_container, editors, tprefs
from calibre.gui2.tweak_book.file_list import OpenWithHandler
from calibre.gui2.viewer.web_view import handle_mathjax_request, send_reply
from calibre.gui2.webengine import (
Bridge, RestartingWebEngineView, create_script, from_js, insert_scripts,
from calibre.utils.webengine import (
Bridge, create_script, from_js, insert_scripts,
secure_webengine, to_js
)
from calibre.gui2.webengine import RestartingWebEngineView
from calibre.gui2.widgets2 import HistoryLineEdit2
from calibre.utils.ipc.simple_worker import offload_worker
from polyglot.builtins import iteritems

View File

@ -28,7 +28,8 @@ from calibre.ebooks.oeb.polish.report import (
gather_data, CSSEntry, CSSFileMatch, MatchLocation, ClassEntry,
ClassFileMatch, ClassElement, CSSRule, LinkLocation)
from calibre.gui2 import error_dialog, question_dialog, choose_save_file, open_url
from calibre.gui2.webengine import secure_webengine, RestartingWebEngineView
from calibre.utils.webengine import secure_webengine
from calibre.gui2.webengine import RestartingWebEngineView
from calibre.gui2.tweak_book import current_container, tprefs, dictionaries
from calibre.gui2.tweak_book.widgets import Dialog
from calibre.gui2.progress_indicator import ProgressIndicator

View File

@ -18,7 +18,7 @@ from calibre import prints, random_user_agent
from calibre.constants import cache_dir
from calibre.gui2 import error_dialog
from calibre.gui2.viewer.web_view import apply_font_settings, vprefs
from calibre.gui2.webengine import create_script, insert_scripts, secure_webengine
from calibre.utils.webengine import create_script, insert_scripts, secure_webengine
from calibre.gui2.widgets2 import Dialog
vprefs.defaults['lookup_locations'] = [

View File

@ -30,10 +30,11 @@ from calibre.gui2 import choose_images, error_dialog, safe_open_url, config
from calibre.gui2.viewer import link_prefix_for_location_links, performance_monitor
from calibre.gui2.viewer.config import viewer_config_dir, vprefs
from calibre.gui2.viewer.tts import TTS
from calibre.gui2.webengine import (
Bridge, RestartingWebEngineView, create_script, from_js, insert_scripts,
from calibre.utils.webengine import (
Bridge, create_script, from_js, insert_scripts,
secure_webengine, to_js
)
from calibre.gui2.webengine import RestartingWebEngineView
from calibre.srv.code import get_translations_data
from calibre.utils.localization import localize_user_manual_link
from calibre.utils.serialize import json_loads

View File

@ -1,140 +1,11 @@
#!/usr/bin/env python
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import json
from qt.core import QObject, Qt, pyqtSignal
from qt.webengine import QWebEnginePage, QWebEngineScript, QWebEngineView, QWebEngineSettings
from qt.core import Qt, pyqtSignal
from qt.webengine import QWebEnginePage, QWebEngineView
from calibre import prints
from calibre.utils.monotonic import monotonic
from calibre.utils.rapydscript import special_title
from polyglot.builtins import iteritems
def secure_webengine(view_or_page_or_settings, for_viewer=False):
s = view_or_page_or_settings.settings() if hasattr(
view_or_page_or_settings, 'settings') else view_or_page_or_settings
a = s.setAttribute
a(QWebEngineSettings.WebAttribute.PluginsEnabled, False)
if not for_viewer:
a(QWebEngineSettings.WebAttribute.JavascriptEnabled, False)
s.setUnknownUrlSchemePolicy(QWebEngineSettings.UnknownUrlSchemePolicy.DisallowUnknownUrlSchemes)
if hasattr(view_or_page_or_settings, 'setAudioMuted'):
view_or_page_or_settings.setAudioMuted(True)
a(QWebEngineSettings.WebAttribute.JavascriptCanOpenWindows, False)
a(QWebEngineSettings.WebAttribute.JavascriptCanAccessClipboard, False)
# ensure javascript cannot read from local files
a(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, False)
a(QWebEngineSettings.WebAttribute.AllowWindowActivationFromJavaScript, False)
return s
def insert_scripts(profile, *scripts):
sc = profile.scripts()
for script in scripts:
for existing in sc.find(script.name()):
sc.remove(existing)
for script in scripts:
sc.insert(script)
def create_script(
name, src, world=QWebEngineScript.ScriptWorldId.ApplicationWorld,
injection_point=QWebEngineScript.InjectionPoint.DocumentReady,
on_subframes=True
):
script = QWebEngineScript()
if isinstance(src, bytes):
src = src.decode('utf-8')
script.setSourceCode(src)
script.setName(name)
script.setWorldId(world)
script.setInjectionPoint(injection_point)
script.setRunsOnSubFrames(on_subframes)
return script
from_js = pyqtSignal
class to_js(str):
def __call__(self, *a):
prints(f'WARNING: Calling {self.name}() before the javascript bridge is ready')
emit = __call__
class to_js_bound(QObject):
def __init__(self, bridge, name):
QObject.__init__(self, bridge)
self.name = name
def __call__(self, *args):
self.parent().page.runJavaScript('if (window.python_comm) python_comm._from_python({}, {})'.format(
json.dumps(self.name), json.dumps(args)), QWebEngineScript.ScriptWorldId.ApplicationWorld)
emit = __call__
class Bridge(QObject):
bridge_ready = pyqtSignal()
def __init__(self, page):
QObject.__init__(self, page)
self._signals = json.dumps(tuple({k for k, v in iteritems(self.__class__.__dict__) if isinstance(v, pyqtSignal)}))
self._signals_registered = False
page.titleChanged.connect(self._title_changed)
for k, v in iteritems(self.__class__.__dict__):
if isinstance(v, to_js):
v.name = k
@property
def page(self):
return self.parent()
@property
def ready(self):
return self._signals_registered
def _title_changed(self, title):
if title.startswith(special_title):
self._poll_for_messages()
def _register_signals(self):
self._signals_registered = True
for k, v in iteritems(self.__class__.__dict__):
if isinstance(v, to_js):
setattr(self, k, to_js_bound(self, k))
self.page.runJavaScript('python_comm._register_signals(' + self._signals + ')', QWebEngineScript.ScriptWorldId.ApplicationWorld)
self.bridge_ready.emit()
def _poll_for_messages(self):
self.page.runJavaScript('python_comm._poll()', QWebEngineScript.ScriptWorldId.ApplicationWorld, self._dispatch_messages)
def _dispatch_messages(self, messages):
try:
for msg in messages:
if isinstance(msg, dict):
mt = msg.get('type')
if mt == 'signal':
signal = getattr(self, msg['name'], None)
if signal is None:
prints('WARNING: No js-to-python signal named: ' + msg['name'])
else:
args = msg['args']
if args:
signal.emit(*args)
else:
signal.emit()
elif mt == 'qt-ready':
self._register_signals()
except Exception:
if messages:
import traceback
traceback.print_exc()
class RestartingWebEngineView(QWebEngineView):
@ -161,29 +32,3 @@ class RestartingWebEngineView(QWebEngineView):
self._last_reload_at = monotonic()
self.render_process_restarted.emit()
prints('Restarting Qt WebEngine')
if __name__ == '__main__':
from calibre.gui2 import Application
from calibre.gui2.tweak_book.preview import WebPage
from qt.core import QMainWindow
app = Application([])
view = QWebEngineView()
page = WebPage(view)
view.setPage(page)
w = QMainWindow()
w.setCentralWidget(view)
class Test(Bridge):
s1 = from_js(object)
j1 = to_js()
t = Test(view.page())
t.s1.connect(print)
w.show()
view.setHtml('''
<p>hello</p>
''')
app.exec()
del t
del page
del app

View File

@ -59,7 +59,7 @@ def compiler():
from calibre import walk
from calibre.gui2 import must_use_qt
from calibre.gui2.webengine import secure_webengine
from calibre.utils.webengine import secure_webengine
must_use_qt()
with lzma.open(P(COMPILER_PATH, allow_user_override=False)) as lzf:
@ -340,7 +340,7 @@ def run_rapydscript_tests():
from calibre.constants import FAKE_HOST, FAKE_PROTOCOL
from calibre.gui2 import must_use_qt
from calibre.gui2.viewer.web_view import send_reply
from calibre.gui2.webengine import secure_webengine, insert_scripts, create_script
from calibre.utils.webengine import secure_webengine, insert_scripts, create_script
must_use_qt()
scheme = QWebEngineUrlScheme(FAKE_PROTOCOL.encode('ascii'))
scheme.setSyntax(QWebEngineUrlScheme.Syntax.Host)

View File

@ -0,0 +1,134 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
import json
from qt.core import QObject, pyqtSignal
from qt.webengine import QWebEngineScript, QWebEngineSettings
from calibre.utils.rapydscript import special_title
def secure_webengine(view_or_page_or_settings, for_viewer=False):
s = view_or_page_or_settings.settings() if hasattr(
view_or_page_or_settings, 'settings') else view_or_page_or_settings
a = s.setAttribute
a(QWebEngineSettings.WebAttribute.PluginsEnabled, False)
if not for_viewer:
a(QWebEngineSettings.WebAttribute.JavascriptEnabled, False)
s.setUnknownUrlSchemePolicy(QWebEngineSettings.UnknownUrlSchemePolicy.DisallowUnknownUrlSchemes)
if hasattr(view_or_page_or_settings, 'setAudioMuted'):
view_or_page_or_settings.setAudioMuted(True)
a(QWebEngineSettings.WebAttribute.JavascriptCanOpenWindows, False)
a(QWebEngineSettings.WebAttribute.JavascriptCanAccessClipboard, False)
# ensure javascript cannot read from local files
a(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, False)
a(QWebEngineSettings.WebAttribute.AllowWindowActivationFromJavaScript, False)
return s
def insert_scripts(profile, *scripts):
sc = profile.scripts()
for script in scripts:
for existing in sc.find(script.name()):
sc.remove(existing)
for script in scripts:
sc.insert(script)
def create_script(
name, src, world=QWebEngineScript.ScriptWorldId.ApplicationWorld,
injection_point=QWebEngineScript.InjectionPoint.DocumentReady,
on_subframes=True
):
script = QWebEngineScript()
if isinstance(src, bytes):
src = src.decode('utf-8')
script.setSourceCode(src)
script.setName(name)
script.setWorldId(world)
script.setInjectionPoint(injection_point)
script.setRunsOnSubFrames(on_subframes)
return script
from_js = pyqtSignal
class to_js(str):
def __call__(self, *a):
print(f'WARNING: Calling {self.name}() before the javascript bridge is ready')
emit = __call__
class to_js_bound(QObject):
def __init__(self, bridge, name):
QObject.__init__(self, bridge)
self.name = name
def __call__(self, *args):
self.parent().page.runJavaScript('if (window.python_comm) python_comm._from_python({}, {})'.format(
json.dumps(self.name), json.dumps(args)), QWebEngineScript.ScriptWorldId.ApplicationWorld)
emit = __call__
class Bridge(QObject):
bridge_ready = pyqtSignal()
def __init__(self, page):
QObject.__init__(self, page)
self._signals = json.dumps(tuple({k for k, v in self.__class__.__dict__.items() if isinstance(v, pyqtSignal)}))
self._signals_registered = False
page.titleChanged.connect(self._title_changed)
for k, v in self.__class__.__dict__.items():
if isinstance(v, to_js):
v.name = k
@property
def page(self):
return self.parent()
@property
def ready(self):
return self._signals_registered
def _title_changed(self, title):
if title.startswith(special_title):
self._poll_for_messages()
def _register_signals(self):
self._signals_registered = True
for k, v in self.__class__.__dict__.items():
if isinstance(v, to_js):
setattr(self, k, to_js_bound(self, k))
self.page.runJavaScript('python_comm._register_signals(' + self._signals + ')', QWebEngineScript.ScriptWorldId.ApplicationWorld)
self.bridge_ready.emit()
def _poll_for_messages(self):
self.page.runJavaScript('python_comm._poll()', QWebEngineScript.ScriptWorldId.ApplicationWorld, self._dispatch_messages)
def _dispatch_messages(self, messages):
try:
for msg in messages:
if isinstance(msg, dict):
mt = msg.get('type')
if mt == 'signal':
signal = getattr(self, msg['name'], None)
if signal is None:
print('WARNING: No js-to-python signal named: ' + msg['name'])
else:
args = msg['args']
if args:
signal.emit(*args)
else:
signal.emit()
elif mt == 'qt-ready':
self._register_signals()
except Exception:
if messages:
import traceback
traceback.print_exc()