From 561edbe1ead0c781fbad41e103b15d5f69cd921b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 27 Jul 2018 21:57:16 +0530 Subject: [PATCH] Two-way syncing work again --- setup/resources.py | 2 +- src/calibre/gui2/tweak_book/preview.py | 89 ++++++++++++++++---------- src/pyj/editor.pyj | 25 +++++--- 3 files changed, 72 insertions(+), 44 deletions(-) diff --git a/setup/resources.py b/setup/resources.py index 71647b7ac4..0a492e3117 100644 --- a/setup/resources.py +++ b/setup/resources.py @@ -285,7 +285,7 @@ class RapydScript(Command): # {{{ def run(self, opts): from calibre.utils.rapydscript import compile_srv, compile_editor if opts.only_module: - locals()['compile_' + opts.only]() + locals()['compile_' + opts.only_module]() else: compile_editor() compile_srv() diff --git a/src/calibre/gui2/tweak_book/preview.py b/src/calibre/gui2/tweak_book/preview.py index 8997166392..c7a545e850 100644 --- a/src/calibre/gui2/tweak_book/preview.py +++ b/src/calibre/gui2/tweak_book/preview.py @@ -6,7 +6,6 @@ from __future__ import absolute_import, division, print_function, unicode_litera # TODO: # live css # check that clicking on both internal and external links works -# check syncing of position back and forth # check all buttons in preview panel # pass user stylesheet with css for split @@ -18,9 +17,10 @@ from functools import partial from threading import Thread from PyQt5.Qt import ( - QApplication, QBuffer, QByteArray, QIcon, QMenu, QSize, QTimer, QToolBar, QUrl, - QVBoxLayout, QWidget, pyqtSignal, pyqtSlot + QApplication, QBuffer, QByteArray, QFile, QIcon, QMenu, QSize, QTimer, QToolBar, QObject, + QUrl, QVBoxLayout, QWidget, pyqtSignal, pyqtSlot ) +from PyQt5.QtWebChannel import QWebChannel from PyQt5.QtWebEngineCore import QWebEngineUrlSchemeHandler from PyQt5.QtWebEngineWidgets import ( QWebEnginePage, QWebEngineProfile, QWebEngineScript, QWebEngineView @@ -259,7 +259,7 @@ def insert_scripts(profile, *scripts): sc.insert(script) -def create_script(name, src, world=QWebEngineScript.ApplicationWorld, injection_point=QWebEngineScript.DocumentCreation, on_subframes=True): +def create_script(name, src, world=QWebEngineScript.ApplicationWorld, injection_point=QWebEngineScript.DocumentReady, on_subframes=True): script = QWebEngineScript() script.setSourceCode(src) script.setName(name) @@ -279,8 +279,19 @@ def create_profile(): from calibre.utils.rapydscript import compile_editor compile_editor() js = P('editor.js', data=True, allow_user_override=False) - js += P('csscolorparser.js', data=True, allow_user_override=False) - insert_scripts(ans, create_script('editor.js', js)) + cparser = P('csscolorparser.js', data=True, allow_user_override=False) + qwebchannel_js = QFile(':/qtwebchannel/qwebchannel.js') + if not qwebchannel_js.open(QBuffer.ReadOnly): + raise RuntimeError( + 'Failed to load qwebchannel.js with error: %s' % qwebchannel_js.errorString()) + qwebchannel_js = bytes(qwebchannel_js.readAll()).decode('utf-8') + qwebchannel_js += 'window.QWebChannel = QWebChannel;' + + insert_scripts(ans, + create_script('qwebchannel.js', qwebchannel_js), + create_script('csscolorparser.js', cparser), + create_script('editor.js', js), + ) url_handler = UrlSchemeHandler(ans) ans.installUrlSchemeHandler(QByteArray(FAKE_PROTOCOL.encode('ascii')), url_handler) s = ans.settings() @@ -291,15 +302,44 @@ def create_profile(): return ans -class WebPage(QWebEnginePage): +class Bridge(QObject): sync_requested = pyqtSignal(object, object, object) split_requested = pyqtSignal(object, object) + go_to_sourceline_address = pyqtSignal(int, 'QStringList') + go_to_anchor = pyqtSignal('QString') + set_split_mode = pyqtSignal(int) + + def __init__(self, parent=None): + QObject.__init__(self, parent) + + @pyqtSlot(native_string_type, native_string_type, native_string_type) + def request_sync(self, tag_name, href, sourceline_address): + try: + self.sync_requested.emit(unicode_type(tag_name), unicode_type(href), json.loads(unicode_type(sourceline_address))) + except (TypeError, ValueError, OverflowError, AttributeError): + pass + + @pyqtSlot(native_string_type, native_string_type) + def request_split(self, loc, totals): + actions['split-in-preview'].setChecked(False) + loc, totals = json.loads(unicode_type(loc)), json.loads(unicode_type(totals)) + if not loc or not totals: + return error_dialog(self.view(), _('Invalid location'), + _('Cannot split on the body tag'), show=True) + self.split_requested.emit(loc, totals) + + +class WebPage(QWebEnginePage): def __init__(self, parent): QWebEnginePage.__init__(self, create_profile(), parent) secure_webengine(self, for_viewer=True) - # TOD: Implement this + self.channel = c = QWebChannel(self) + self.bridge = Bridge(self) + self.setWebChannel(c, QWebEngineScript.ApplicationWorld) + c.registerObject('bridge', self.bridge) + # TODO: Implement this # css = '[data-in-split-mode="1"] [data-is-block="1"]:hover { cursor: pointer !important; border-top: solid 5px green !important }' def javaScriptConsoleMessage(self, level, msg, linenumber, source_id): @@ -313,25 +353,8 @@ class WebPage(QWebEnginePage): open_url(url) return False - @pyqtSlot(native_string_type, native_string_type, native_string_type) - def request_sync(self, tag_name, href, sourceline_address): - try: - self.sync_requested.emit(unicode_type(tag_name), unicode_type(href), json.loads(unicode_type(sourceline_address))) - except (TypeError, ValueError, OverflowError, AttributeError): - pass - - def go_to_anchor(self, anchor, lnum): - self.runjs('window.calibre_preview_integration.go_to_anchor(%s, %s)' % ( - json.dumps(anchor), json.dumps(unicode_type(lnum)))) - - @pyqtSlot(native_string_type, native_string_type) - def request_split(self, loc, totals): - actions['split-in-preview'].setChecked(False) - loc, totals = json.loads(unicode_type(loc)), json.loads(unicode_type(totals)) - if not loc or not totals: - return error_dialog(self.view(), _('Invalid location'), - _('Cannot split on the body tag'), show=True) - self.split_requested.emit(loc, totals) + def go_to_anchor(self, anchor): + self.bridge.go_to_anchor.emit(anchor or '') def runjs(self, src, callback=None): if callback is None: @@ -344,12 +367,10 @@ class WebPage(QWebEnginePage): if lnum is None: return tags = [x.lower() for x in tags] - self.runjs('window.calibre_preview_integration.go_to_sourceline_address(%d, %s)' % (lnum, json.dumps(tags))) + self.bridge.go_to_sourceline_address.emit(lnum, tags) def split_mode(self, enabled): - self.runjs( - 'window.calibre_preview_integration.split_mode(%s)' % ( - 'true' if enabled else 'false')) + self.bridge.set_split_mode.emit(1 if enabled else 0) class WebView(QWebEngineView): @@ -427,8 +448,8 @@ class Preview(QWidget): self.setLayout(l) l.setContentsMargins(0, 0, 0, 0) self.view = WebView(self) - self.view._page.sync_requested.connect(self.request_sync) - self.view._page.split_requested.connect(self.request_split) + self.view._page.bridge.sync_requested.connect(self.request_sync) + self.view._page.bridge.split_requested.connect(self.request_split) self.view._page.loadFinished.connect(self.load_finished) self.inspector = self.view.inspector l.addWidget(self.view) @@ -502,7 +523,7 @@ class Preview(QWidget): else: name = c.href_to_name(href, self.current_name) if href else None if name == self.current_name: - return self.view._page.go_to_anchor(urlparse(href).fragment, lnum) + return self.view._page.go_to_anchor(urlparse(href).fragment) if name and c.exists(name) and c.mime_map[name] in OEB_DOCS: return self.link_clicked.emit(name, urlparse(href).fragment or TOP) self.sync_requested.emit(self.current_name, lnum) diff --git a/src/pyj/editor.pyj b/src/pyj/editor.pyj index b8605627ea..a315e3c8dd 100644 --- a/src/pyj/editor.pyj +++ b/src/pyj/editor.pyj @@ -246,6 +246,9 @@ class PreviewIntegration: def __init__(self): self.blocks_found = False self.in_split_mode = False + if window is window.top: + setTimeout(self.connect_channel, 10) + window.document.body.addEventListener('click', self.onclick, True) def go_to_line(self, lnum): for node in document.querySelectorAll(f'[data-lnum="{lnum}"]'): @@ -282,7 +285,7 @@ class PreviewIntegration: elem.setAttribute('data-is-block', '1') self.blocks_found = True - def split_mode(self, enabled): + def set_split_mode(self, enabled): self.in_split_mode = enabled document.body.setAttribute('data-in-split-mode', '1' if enabled else '0') if enabled: @@ -303,10 +306,15 @@ class PreviewIntegration: parent = parent.parentNode loc.reverse() totals.reverse() - window.py_bridge.request_split(JSON.stringify(loc), JSON.stringify(totals)) + self.bridge.request_split(JSON.stringify(loc), JSON.stringify(totals)) - def onload(self): - window.document.body.addEventListener('click', this.onclick, True) + def connect_channel(self): + self.qwebchannel = new window.QWebChannel(window.qt.webChannelTransport, def(channel): + self.bridge = channel.objects.bridge + self.bridge.go_to_sourceline_address.connect(self.go_to_sourceline_address) + self.bridge.go_to_anchor.connect(self.go_to_anchor) + self.bridge.set_split_mode.connect(self.set_split_mode) + ) def onclick(self, event): event.preventDefault() @@ -321,17 +329,17 @@ class PreviewIntegration: tn = e.tagName?.toLowerCase() href = e.getAttribute('href') e = e.parentNode - window.py_bridge.request_sync(tn, href, JSON.stringify(address)) + self.bridge.request_sync(tn, href, JSON.stringify(address)) return False - def go_to_anchor(self, anchor, lnum): + def go_to_anchor(self, anchor): elem = document.getElementById(anchor) if not elem: elem = document.querySelector(f'[name="{anchor}"]') if elem: elem.scrollIntoView() address = get_sourceline_address(elem) - window.py_bridge.request_sync('', '', address) + self.bridge.request_sync('', '', address) def live_css(self, sourceline, tags): target = None @@ -360,5 +368,4 @@ class PreviewIntegration: return JSON.stringify(ans) -window.calibre_preview_integration = PreviewIntegration() -window.onload = window.calibre_preview_integration.onload +calibre_preview_integration = PreviewIntegration()