mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
In Qt6 QWebEnginepage and QWebEngineView can be imported separately before constructing the QApplication
This commit is contained in:
parent
1c0c88a2f9
commit
8eb7706b8a
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'] = [
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
134
src/calibre/utils/webengine.py
Normal file
134
src/calibre/utils/webengine.py
Normal 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()
|
Loading…
x
Reference in New Issue
Block a user