mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Drop use of Qt Web Channel
One less possible cause of lifetime related crashes/leaks. Plus my custom solution has a nicer interface. It's slower but since communication between python and js is not a bottleneck...
This commit is contained in:
parent
883717039e
commit
92fe190f63
@ -14,10 +14,9 @@ from functools import partial
|
|||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
from PyQt5.Qt import (
|
from PyQt5.Qt import (
|
||||||
QApplication, QBuffer, QByteArray, QFile, QIcon, QMenu, QObject, QSize, QTimer,
|
QApplication, QBuffer, QByteArray, QIcon, QMenu, QSize, QTimer,
|
||||||
QToolBar, QUrl, QVBoxLayout, QWidget, pyqtSignal, pyqtSlot
|
QToolBar, QUrl, QVBoxLayout, QWidget, pyqtSignal
|
||||||
)
|
)
|
||||||
from PyQt5.QtWebChannel import QWebChannel
|
|
||||||
from PyQt5.QtWebEngineCore import QWebEngineUrlSchemeHandler
|
from PyQt5.QtWebEngineCore import QWebEngineUrlSchemeHandler
|
||||||
from PyQt5.QtWebEngineWidgets import (
|
from PyQt5.QtWebEngineWidgets import (
|
||||||
QWebEnginePage, QWebEngineProfile, QWebEngineScript, QWebEngineView
|
QWebEnginePage, QWebEngineProfile, QWebEngineScript, QWebEngineView
|
||||||
@ -31,7 +30,7 @@ from calibre.ebooks.oeb.base import OEB_DOCS, XHTML_MIME, serialize
|
|||||||
from calibre.ebooks.oeb.polish.parsing import parse
|
from calibre.ebooks.oeb.polish.parsing import parse
|
||||||
from calibre.gui2 import NO_URL_FORMATTING, error_dialog, open_url
|
from calibre.gui2 import NO_URL_FORMATTING, error_dialog, open_url
|
||||||
from calibre.gui2.tweak_book import TOP, actions, current_container, editors, tprefs
|
from calibre.gui2.tweak_book import TOP, actions, current_container, editors, tprefs
|
||||||
from calibre.gui2.webengine import create_script, insert_scripts, secure_webengine
|
from calibre.gui2.webengine import create_script, insert_scripts, secure_webengine, Bridge, from_js, to_js
|
||||||
from calibre.gui2.widgets2 import HistoryLineEdit2
|
from calibre.gui2.widgets2 import HistoryLineEdit2
|
||||||
from calibre.utils.ipc.simple_worker import offload_worker
|
from calibre.utils.ipc.simple_worker import offload_worker
|
||||||
from polyglot.builtins import unicode_type
|
from polyglot.builtins import unicode_type
|
||||||
@ -259,15 +258,8 @@ def create_profile():
|
|||||||
compile_editor()
|
compile_editor()
|
||||||
js = P('editor.js', data=True, allow_user_override=False)
|
js = P('editor.js', data=True, allow_user_override=False)
|
||||||
cparser = P('csscolorparser.js', data=True, allow_user_override=False)
|
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,
|
insert_scripts(ans,
|
||||||
create_script('qwebchannel.js', qwebchannel_js),
|
|
||||||
create_script('csscolorparser.js', cparser),
|
create_script('csscolorparser.js', cparser),
|
||||||
create_script('editor.js', js),
|
create_script('editor.js', js),
|
||||||
)
|
)
|
||||||
@ -281,33 +273,17 @@ def create_profile():
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
class Bridge(QObject):
|
class PreviewBridge(Bridge):
|
||||||
|
|
||||||
sync_requested = pyqtSignal(object, object, object)
|
request_sync = from_js(object, object, object)
|
||||||
split_requested = pyqtSignal(object, object)
|
request_split = from_js(object, object)
|
||||||
go_to_sourceline_address = pyqtSignal(int, 'QStringList')
|
|
||||||
go_to_anchor = pyqtSignal('QString')
|
go_to_sourceline_address = to_js()
|
||||||
set_split_mode = pyqtSignal(int)
|
go_to_anchor = to_js()
|
||||||
|
set_split_mode = to_js()
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QObject.__init__(self, parent)
|
Bridge.__init__(self, parent)
|
||||||
|
|
||||||
@pyqtSlot('QString', 'QString', 'QJsonArray')
|
|
||||||
def request_sync(self, tag_name, href, sourceline_address):
|
|
||||||
address = [sourceline_address[0].toInt(), [x.toString() for x in sourceline_address[1].toArray()]]
|
|
||||||
try:
|
|
||||||
self.sync_requested.emit(tag_name, href, address)
|
|
||||||
except (TypeError, ValueError, OverflowError, AttributeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@pyqtSlot('QJsonArray', 'QJsonArray')
|
|
||||||
def request_split(self, loc, totals):
|
|
||||||
actions['split-in-preview'].setChecked(False)
|
|
||||||
loc, totals = [x.toInt() for x in loc], [x.toInt() for x in 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):
|
class WebPage(QWebEnginePage):
|
||||||
@ -315,10 +291,7 @@ class WebPage(QWebEnginePage):
|
|||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
QWebEnginePage.__init__(self, create_profile(), parent)
|
QWebEnginePage.__init__(self, create_profile(), parent)
|
||||||
secure_webengine(self, for_viewer=True)
|
secure_webengine(self, for_viewer=True)
|
||||||
self.channel = c = QWebChannel(self)
|
self.bridge = PreviewBridge(self)
|
||||||
self.bridge = Bridge(self)
|
|
||||||
self.setWebChannel(c, QWebEngineScript.ApplicationWorld)
|
|
||||||
c.registerObject('bridge', self.bridge)
|
|
||||||
|
|
||||||
def javaScriptConsoleMessage(self, level, msg, linenumber, source_id):
|
def javaScriptConsoleMessage(self, level, msg, linenumber, source_id):
|
||||||
prints('%s:%s: %s' % (source_id, linenumber, msg))
|
prints('%s:%s: %s' % (source_id, linenumber, msg))
|
||||||
@ -348,7 +321,8 @@ class WebPage(QWebEnginePage):
|
|||||||
self.bridge.go_to_sourceline_address.emit(lnum, tags)
|
self.bridge.go_to_sourceline_address.emit(lnum, tags)
|
||||||
|
|
||||||
def split_mode(self, enabled):
|
def split_mode(self, enabled):
|
||||||
self.bridge.set_split_mode.emit(1 if enabled else 0)
|
if self.bridge.ready:
|
||||||
|
self.bridge.set_split_mode.emit(1 if enabled else 0)
|
||||||
|
|
||||||
|
|
||||||
class WebView(QWebEngineView):
|
class WebView(QWebEngineView):
|
||||||
@ -426,8 +400,8 @@ class Preview(QWidget):
|
|||||||
self.setLayout(l)
|
self.setLayout(l)
|
||||||
l.setContentsMargins(0, 0, 0, 0)
|
l.setContentsMargins(0, 0, 0, 0)
|
||||||
self.view = WebView(self)
|
self.view = WebView(self)
|
||||||
self.view._page.bridge.sync_requested.connect(self.request_sync)
|
self.view._page.bridge.request_sync.connect(self.request_sync)
|
||||||
self.view._page.bridge.split_requested.connect(self.request_split)
|
self.view._page.bridge.request_split.connect(self.request_split)
|
||||||
self.view._page.loadFinished.connect(self.load_finished)
|
self.view._page.loadFinished.connect(self.load_finished)
|
||||||
self.inspector = self.view.inspector
|
self.inspector = self.view.inspector
|
||||||
l.addWidget(self.view)
|
l.addWidget(self.view)
|
||||||
@ -510,6 +484,10 @@ class Preview(QWidget):
|
|||||||
self.sync_requested.emit(self.current_name, lnum)
|
self.sync_requested.emit(self.current_name, lnum)
|
||||||
|
|
||||||
def request_split(self, loc, totals):
|
def request_split(self, loc, totals):
|
||||||
|
actions['split-in-preview'].setChecked(False)
|
||||||
|
if not loc or not totals:
|
||||||
|
return error_dialog(self, _('Invalid location'),
|
||||||
|
_('Cannot split on the body tag'), show=True)
|
||||||
if self.current_name:
|
if self.current_name:
|
||||||
self.split_requested.emit(self.current_name, loc, totals)
|
self.split_requested.emit(self.current_name, loc, totals)
|
||||||
|
|
||||||
|
@ -4,7 +4,13 @@
|
|||||||
|
|
||||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||||
|
|
||||||
from PyQt5.QtWebEngineWidgets import QWebEngineScript
|
import json
|
||||||
|
|
||||||
|
from PyQt5.Qt import QObject, pyqtSignal
|
||||||
|
from PyQt5.QtWebEngineWidgets import QWebEngineScript, QWebEngineView
|
||||||
|
|
||||||
|
from calibre import prints
|
||||||
|
from calibre.utils.rapydscript import special_title
|
||||||
|
|
||||||
|
|
||||||
def secure_webengine(view_or_page_or_settings, for_viewer=False):
|
def secure_webengine(view_or_page_or_settings, for_viewer=False):
|
||||||
@ -40,3 +46,106 @@ def create_script(name, src, world=QWebEngineScript.ApplicationWorld, injection_
|
|||||||
script.setInjectionPoint(injection_point)
|
script.setInjectionPoint(injection_point)
|
||||||
script.setRunsOnSubFrames(on_subframes)
|
script.setRunsOnSubFrames(on_subframes)
|
||||||
return script
|
return script
|
||||||
|
|
||||||
|
|
||||||
|
from_js = pyqtSignal
|
||||||
|
|
||||||
|
|
||||||
|
class to_js(type('')):
|
||||||
|
|
||||||
|
def __call__(self, *a):
|
||||||
|
prints('WARNING: Calling {}() before the javascript bridge is ready'.format(self.name))
|
||||||
|
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.ApplicationWorld)
|
||||||
|
emit = __call__
|
||||||
|
|
||||||
|
|
||||||
|
class Bridge(QObject):
|
||||||
|
|
||||||
|
def __init__(self, page):
|
||||||
|
QObject.__init__(self, page)
|
||||||
|
self._signals = json.dumps(tuple({k for k, v in self.__class__.__dict__.iteritems() if isinstance(v, pyqtSignal)}))
|
||||||
|
self._signals_registered = False
|
||||||
|
page.titleChanged.connect(self._title_changed)
|
||||||
|
for k, v in self.__class__.__dict__.iteritems():
|
||||||
|
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__.iteritems():
|
||||||
|
if isinstance(v, to_js):
|
||||||
|
setattr(self, k, to_js_bound(self, k))
|
||||||
|
self.page.runJavaScript('python_comm._register_signals(' + self._signals + ')', QWebEngineScript.ApplicationWorld)
|
||||||
|
|
||||||
|
def _poll_for_messages(self):
|
||||||
|
self.page.runJavaScript('python_comm._poll()', QWebEngineScript.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:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from calibre.gui2 import Application
|
||||||
|
from calibre.gui2.tweak_book.preview import WebPage
|
||||||
|
from PyQt5.Qt 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
|
||||||
|
@ -26,6 +26,7 @@ from polyglot.builtins import itervalues, range, exec_path, raw_input, error_mes
|
|||||||
from polyglot.queue import Empty, Queue
|
from polyglot.queue import Empty, Queue
|
||||||
|
|
||||||
COMPILER_PATH = 'rapydscript/compiler.js.xz'
|
COMPILER_PATH = 'rapydscript/compiler.js.xz'
|
||||||
|
special_title = '__webengine_messages_pending__'
|
||||||
|
|
||||||
|
|
||||||
def abspath(x):
|
def abspath(x):
|
||||||
@ -215,7 +216,7 @@ def compile_editor():
|
|||||||
rapydscript_dir = os.path.join(base, 'src', 'pyj')
|
rapydscript_dir = os.path.join(base, 'src', 'pyj')
|
||||||
fname = os.path.join(rapydscript_dir, 'editor.pyj')
|
fname = os.path.join(rapydscript_dir, 'editor.pyj')
|
||||||
with lopen(fname, 'rb') as f:
|
with lopen(fname, 'rb') as f:
|
||||||
js = compile_fast(f.read(), fname, js_version=6)
|
js = compile_fast(f.read(), fname, js_version=6).replace('__SPECIAL_TITLE__', special_title, 1)
|
||||||
base = os.path.join(base, 'resources')
|
base = os.path.join(base, 'resources')
|
||||||
atomic_write(base, 'editor.js', js)
|
atomic_write(base, 'editor.js', js)
|
||||||
|
|
||||||
|
@ -5,6 +5,9 @@ from __python__ import bound_methods, hash_literals
|
|||||||
|
|
||||||
from elementmaker import E
|
from elementmaker import E
|
||||||
|
|
||||||
|
from qt import to_python, from_python
|
||||||
|
|
||||||
|
|
||||||
def is_hidden(elem):
|
def is_hidden(elem):
|
||||||
while elem:
|
while elem:
|
||||||
if (elem.style and (elem.style.visibility is 'hidden' or elem.style.display is 'none')):
|
if (elem.style and (elem.style.visibility is 'hidden' or elem.style.display is 'none')):
|
||||||
@ -243,142 +246,126 @@ def scroll_to_node(node):
|
|||||||
node.scrollIntoView()
|
node.scrollIntoView()
|
||||||
|
|
||||||
|
|
||||||
class PreviewIntegration:
|
state = {'blocks_found': False, 'in_split_mode': False}
|
||||||
|
|
||||||
###
|
|
||||||
# Namespace to expose all the functions used for integration with the Tweak
|
|
||||||
# Book Preview Panel.
|
|
||||||
###
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.blocks_found = False
|
|
||||||
self.in_split_mode = False
|
|
||||||
if window is window.top:
|
|
||||||
setTimeout(self.connect_channel, 10)
|
|
||||||
document.body.addEventListener('click', self.onclick, True)
|
|
||||||
document.documentElement.appendChild(E.style(
|
|
||||||
type='text/css',
|
|
||||||
'[data-in-split-mode="1"] [data-is-block="1"]:hover { cursor: pointer !important; border-top: solid 5px green !important }'
|
|
||||||
))
|
|
||||||
|
|
||||||
def go_to_line(self, lnum):
|
def go_to_line(lnum):
|
||||||
for node in document.querySelectorAll(f'[data-lnum="{lnum}"]'):
|
for node in document.querySelectorAll(f'[data-lnum="{lnum}"]'):
|
||||||
if is_hidden(node):
|
if is_hidden(node):
|
||||||
continue
|
continue
|
||||||
scroll_to_node(node)
|
scroll_to_node(node)
|
||||||
|
break
|
||||||
|
|
||||||
|
@from_python
|
||||||
|
def go_to_sourceline_address(sourceline, tags):
|
||||||
|
nodes = document.querySelectorAll(f'[data-lnum="{sourceline}"]')
|
||||||
|
for index in range(nodes.length):
|
||||||
|
node = nodes[index]
|
||||||
|
if index >= tags.length or node.tagName.toLowerCase() is not tags[index]:
|
||||||
break
|
break
|
||||||
|
if index == tags.length - 1 and not is_hidden(node):
|
||||||
|
return scroll_to_node(node)
|
||||||
|
go_to_line(sourceline)
|
||||||
|
|
||||||
def go_to_sourceline_address(self, sourceline, tags):
|
def line_numbers():
|
||||||
nodes = document.querySelectorAll(f'[data-lnum="{sourceline}"]')
|
found_body = False
|
||||||
for index in range(nodes.length):
|
ans = v'[]'
|
||||||
node = nodes[index]
|
for node in document.getElementsByTagName('*'):
|
||||||
if index >= tags.length or node.tagName.toLowerCase() is not tags[index]:
|
if not found_body and node.tagName.toLowerCase() is "body":
|
||||||
break
|
found_body = True
|
||||||
if index == tags.length - 1 and not is_hidden(node):
|
if found_body:
|
||||||
return scroll_to_node(node)
|
ans.push(node.dataset.lnum)
|
||||||
self.go_to_line(sourceline)
|
return ans
|
||||||
|
|
||||||
def line_numbers(self):
|
def find_blocks():
|
||||||
found_body = False
|
if state.blocks_found:
|
||||||
ans = v'[]'
|
return
|
||||||
for node in document.getElementsByTagName('*'):
|
for elem in document.body.getElementsByTagName('*'):
|
||||||
if not found_body and node.tagName.toLowerCase() is "body":
|
if is_block(elem) and not in_table(elem):
|
||||||
found_body = True
|
elem.setAttribute('data-is-block', '1')
|
||||||
if found_body:
|
state.blocks_found = True
|
||||||
ans.push(node.dataset.lnum)
|
|
||||||
return ans
|
|
||||||
|
|
||||||
def find_blocks(self):
|
@from_python
|
||||||
if self.blocks_found:
|
def set_split_mode(enabled):
|
||||||
return
|
state.in_split_mode = enabled
|
||||||
for elem in document.body.getElementsByTagName('*'):
|
document.body.dataset.inSplitMode = '1' if enabled else '0'
|
||||||
if is_block(elem) and not in_table(elem):
|
if enabled:
|
||||||
elem.setAttribute('data-is-block', '1')
|
find_blocks()
|
||||||
self.blocks_found = True
|
|
||||||
|
|
||||||
def set_split_mode(self, enabled):
|
def report_split(node):
|
||||||
self.in_split_mode = enabled
|
loc = v'[]'
|
||||||
document.body.dataset.inSplitMode = '1' if enabled else '0'
|
totals = v'[]'
|
||||||
if enabled:
|
parent = find_containing_block(node)
|
||||||
self.find_blocks()
|
while parent and parent.tagName.toLowerCase() is not 'body':
|
||||||
|
totals.push(parent.parentNode.children.length)
|
||||||
|
num = 0
|
||||||
|
sibling = parent.previousElementSibling
|
||||||
|
while sibling:
|
||||||
|
num += 1
|
||||||
|
sibling = sibling.previousElementSibling
|
||||||
|
loc.push(num)
|
||||||
|
parent = parent.parentNode
|
||||||
|
loc.reverse()
|
||||||
|
totals.reverse()
|
||||||
|
to_python.request_split(loc, totals)
|
||||||
|
|
||||||
def report_split(self, node):
|
def onclick(event):
|
||||||
loc = v'[]'
|
event.preventDefault()
|
||||||
totals = v'[]'
|
if state.in_split_mode:
|
||||||
parent = find_containing_block(node)
|
report_split(event.target)
|
||||||
while parent and parent.tagName.toLowerCase() is not 'body':
|
else:
|
||||||
totals.push(parent.parentNode.children.length)
|
e = event.target
|
||||||
num = 0
|
address = get_sourceline_address(e)
|
||||||
sibling = parent.previousElementSibling
|
# Find the closest containing link, if any
|
||||||
while sibling:
|
href = tn = ''
|
||||||
num += 1
|
while e and e is not document.body and e is not document and e is not document.documentElement and (tn is not 'a' or not href):
|
||||||
sibling = sibling.previousElementSibling
|
tn = e.tagName.toLowerCase() if e.tagName else ''
|
||||||
loc.push(num)
|
href = e.getAttribute('href')
|
||||||
parent = parent.parentNode
|
e = e.parentNode
|
||||||
loc.reverse()
|
to_python.request_sync(tn, href, address)
|
||||||
totals.reverse()
|
return False
|
||||||
self.bridge.request_split(loc, totals)
|
|
||||||
|
|
||||||
def connect_channel(self):
|
@from_python
|
||||||
self.qwebchannel = new window.QWebChannel(window.qt.webChannelTransport, def(channel):
|
def go_to_anchor(anchor):
|
||||||
self.bridge = channel.objects.bridge
|
elem = document.getElementById(anchor)
|
||||||
self.bridge.go_to_sourceline_address.connect(self.go_to_sourceline_address)
|
if not elem:
|
||||||
self.bridge.go_to_anchor.connect(self.go_to_anchor)
|
elem = document.querySelector(f'[name="{anchor}"]')
|
||||||
self.bridge.set_split_mode.connect(self.set_split_mode)
|
if elem:
|
||||||
)
|
elem.scrollIntoView()
|
||||||
|
address = get_sourceline_address(elem)
|
||||||
|
to_python.request_sync('', '', address)
|
||||||
|
|
||||||
def onclick(self, event):
|
def live_css(sourceline, tags):
|
||||||
event.preventDefault()
|
target = None
|
||||||
if self.in_split_mode:
|
i = 0
|
||||||
self.report_split(event.target)
|
for node in document.querySelectorAll(f'[data-lnum="{sourceline}"]'):
|
||||||
else:
|
tn = node.tagName.toLowerCase() if node.tagName else ''
|
||||||
e = event.target
|
if tn is not tags[i]:
|
||||||
address = get_sourceline_address(e)
|
return JSON.stringify(None)
|
||||||
# Find the closest containing link, if any
|
i += 1
|
||||||
href = tn = ''
|
target = node
|
||||||
while e and e is not document.body and e is not document and e is not document.documentElement and (tn is not 'a' or not href):
|
if i >= tags.length:
|
||||||
tn = e.tagName.toLowerCase() if e.tagName else ''
|
break
|
||||||
href = e.getAttribute('href')
|
all_properties = {}
|
||||||
e = e.parentNode
|
ans = {'nodes':v'[]', 'computed_css':all_properties}
|
||||||
self.bridge.request_sync(tn, href, address)
|
is_ancestor = False
|
||||||
return False
|
while target and target.ownerDocument:
|
||||||
|
css = get_matched_css(target, is_ancestor, all_properties)
|
||||||
|
# We want to show the Matched CSS rules header even if no rules matched
|
||||||
|
if css.length > 0 or not is_ancestor:
|
||||||
|
tn = target.tagName.toLowerCase() if target.tagName else ''
|
||||||
|
ans.nodes.push({
|
||||||
|
'name': tn,
|
||||||
|
'css': css, 'is_ancestor': is_ancestor,
|
||||||
|
'sourceline': target.getAttribute('data-lnum')
|
||||||
|
})
|
||||||
|
target = target.parentNode
|
||||||
|
is_ancestor = True
|
||||||
|
return JSON.stringify(ans)
|
||||||
|
|
||||||
def go_to_anchor(self, anchor):
|
document.body.addEventListener('click', onclick, True)
|
||||||
elem = document.getElementById(anchor)
|
document.documentElement.appendChild(E.style(
|
||||||
if not elem:
|
type='text/css',
|
||||||
elem = document.querySelector(f'[name="{anchor}"]')
|
'[data-in-split-mode="1"] [data-is-block="1"]:hover { cursor: pointer !important; border-top: solid 5px green !important }'
|
||||||
if elem:
|
))
|
||||||
elem.scrollIntoView()
|
|
||||||
address = get_sourceline_address(elem)
|
|
||||||
self.bridge.request_sync('', '', address)
|
|
||||||
|
|
||||||
def live_css(self, sourceline, tags):
|
|
||||||
target = None
|
|
||||||
i = 0
|
|
||||||
for node in document.querySelectorAll(f'[data-lnum="{sourceline}"]'):
|
|
||||||
tn = node.tagName.toLowerCase() if node.tagName else ''
|
|
||||||
if tn is not tags[i]:
|
|
||||||
return JSON.stringify(None)
|
|
||||||
i += 1
|
|
||||||
target = node
|
|
||||||
if i >= tags.length:
|
|
||||||
break
|
|
||||||
all_properties = {}
|
|
||||||
ans = {'nodes':v'[]', 'computed_css':all_properties}
|
|
||||||
is_ancestor = False
|
|
||||||
while target and target.ownerDocument:
|
|
||||||
css = get_matched_css(target, is_ancestor, all_properties)
|
|
||||||
# We want to show the Matched CSS rules header even if no rules matched
|
|
||||||
if css.length > 0 or not is_ancestor:
|
|
||||||
tn = target.tagName.toLowerCase() if target.tagName else ''
|
|
||||||
ans.nodes.push({
|
|
||||||
'name': tn,
|
|
||||||
'css': css, 'is_ancestor': is_ancestor,
|
|
||||||
'sourceline': target.getAttribute('data-lnum')
|
|
||||||
})
|
|
||||||
target = target.parentNode
|
|
||||||
is_ancestor = True
|
|
||||||
return JSON.stringify(ans)
|
|
||||||
|
|
||||||
|
|
||||||
calibre_preview_integration = PreviewIntegration()
|
|
||||||
|
60
src/pyj/qt.pyj
Normal file
60
src/pyj/qt.pyj
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# vim:fileencoding=utf-8
|
||||||
|
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
from __python__ import bound_methods, hash_literals
|
||||||
|
|
||||||
|
special_title = '__SPECIAL_TITLE__'
|
||||||
|
slots = {}
|
||||||
|
|
||||||
|
def ping(suffix):
|
||||||
|
document.title = special_title + suffix
|
||||||
|
|
||||||
|
|
||||||
|
def signal():
|
||||||
|
args = Array.from(arguments)
|
||||||
|
to_python._queue_message({'type': 'signal', 'name': this, 'args': args})
|
||||||
|
|
||||||
|
|
||||||
|
class ToPython:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._messages = v'[]'
|
||||||
|
self._last_ping_value = 0
|
||||||
|
self._ping_timer_id = -1
|
||||||
|
self._queue_message({'type': 'qt-ready'})
|
||||||
|
|
||||||
|
def _ping(self):
|
||||||
|
if self._ping_timer_id < 0:
|
||||||
|
self._last_ping_value = 0 if self._last_ping_value else 1
|
||||||
|
self._ping_timer_id = setTimeout(def():
|
||||||
|
ping(self._last_ping_value)
|
||||||
|
self._ping_timer_id = -1
|
||||||
|
, 0)
|
||||||
|
|
||||||
|
def _queue_message(self, msg):
|
||||||
|
self._messages.push(msg)
|
||||||
|
self._ping()
|
||||||
|
|
||||||
|
def _register_signals(self, signals):
|
||||||
|
for signal_name in signals:
|
||||||
|
self[signal_name] = signal.bind(signal_name)
|
||||||
|
|
||||||
|
def _poll(self):
|
||||||
|
ans = self._messages
|
||||||
|
self._messages = v'[]'
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def _from_python(self, name, args):
|
||||||
|
callback = slots[name]
|
||||||
|
if callback:
|
||||||
|
callback.apply(None, args)
|
||||||
|
else:
|
||||||
|
console.warn(f'Attempt to call non-existent python-to-js slot named: {name}')
|
||||||
|
|
||||||
|
|
||||||
|
to_python = None # TODO: Implement this via message passing from sub-frames
|
||||||
|
if window is window.top:
|
||||||
|
window.python_comm = to_python = ToPython()
|
||||||
|
|
||||||
|
|
||||||
|
def from_python(func):
|
||||||
|
slots[func.name] = func
|
Loading…
x
Reference in New Issue
Block a user