work on porting editor preview to web engine

This commit is contained in:
Kovid Goyal 2018-07-27 16:16:53 +05:30
parent 40cc61397b
commit 8877445c78
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 116 additions and 114 deletions

View File

@ -1361,12 +1361,13 @@ def secure_web_page(qwebpage_or_qwebsettings):
return settings return settings
def secure_webengine(view_or_page_or_settings): 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 s = view_or_page_or_settings.settings() if hasattr(view_or_page_or_settings, 'settings') else view_or_page_or_settings
s.setUnknownUrlSchemePolicy(s.DisallowUnknownUrlSchemes)
a = s.setAttribute a = s.setAttribute
a(s.PluginsEnabled, False) a(s.PluginsEnabled, False)
if not for_viewer:
a(s.JavascriptEnabled, False) a(s.JavascriptEnabled, False)
s.setUnknownUrlSchemePolicy(s.DisallowUnknownUrlSchemes)
a(s.JavascriptCanOpenWindows, False) a(s.JavascriptCanOpenWindows, False)
a(s.JavascriptCanAccessClipboard, False) a(s.JavascriptCanAccessClipboard, False)
a(s.LocalContentCanAccessFileUrls, False) # ensure javascript cannot read from local files a(s.LocalContentCanAccessFileUrls, False) # ensure javascript cannot read from local files

View File

@ -472,6 +472,7 @@ class LiveCSS(QWidget):
self.stack.setCurrentIndex(1) self.stack.setCurrentIndex(1)
def read_data(self, sourceline, tags): def read_data(self, sourceline, tags):
return None # TODO: Implement this
mf = self.preview.view.page().mainFrame() mf = self.preview.view.page().mainFrame()
tags = [x.lower() for x in tags] tags = [x.lower() for x in tags]
result = unicode_type(mf.evaluateJavaScript( result = unicode_type(mf.evaluateJavaScript(

View File

@ -1,35 +1,43 @@
#!/usr/bin/env python2 #!/usr/bin/env python2
# vim:fileencoding=utf-8 # vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3' # TODO:
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' # inspect element
# live css
# check that clicking on both internal and external links works
# check if you can remove the restriction that prevents inspector dock from being undocked
# check the context menu
# rewrite JS from coffeescript to rapydscript
import time, textwrap, json import json
from bisect import bisect_right import textwrap
from threading import Thread import time
from functools import partial from functools import partial
from threading import Thread
from PyQt5.Qt import ( from PyQt5.Qt import (
QWidget, QVBoxLayout, QApplication, QSize, QNetworkAccessManager, QMenu, QIcon, QApplication, QIcon, QMenu, QNetworkAccessManager, QNetworkReply,
QNetworkReply, QTimer, QNetworkRequest, QUrl, Qt, QToolBar, QNetworkRequest, QSize, QTimer, QToolBar, QUrl, QVBoxLayout, QWidget, pyqtSignal,
pyqtSlot, pyqtSignal) pyqtSlot
from PyQt5.QtWebKitWidgets import QWebView, QWebInspector, QWebPage )
from PyQt5.QtWebEngineWidgets import (
QWebEnginePage, QWebEngineProfile, QWebEngineScript, QWebEngineView
)
from calibre import prints from calibre import prints
from calibre.constants import FAKE_PROTOCOL, FAKE_HOST from calibre.constants import FAKE_HOST, FAKE_PROTOCOL, __version__
from calibre.ebooks.oeb.base import OEB_DOCS, serialize
from calibre.ebooks.oeb.polish.parsing import parse from calibre.ebooks.oeb.polish.parsing import parse
from calibre.ebooks.oeb.base import serialize, OEB_DOCS from calibre.gui2 import NO_URL_FORMATTING, error_dialog, open_url, secure_webengine
from calibre.gui2 import error_dialog, open_url, NO_URL_FORMATTING, secure_web_page from calibre.gui2.tweak_book import TOP, actions, current_container, editors, tprefs
from calibre.gui2.tweak_book import current_container, editors, tprefs, actions, TOP
from calibre.gui2.viewer.documentview import apply_settings
from calibre.gui2.viewer.config import config
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 filter, map, native_string_type, unicode_type
from polyglot.urllib import urlparse
from polyglot.queue import Queue, Empty
from polyglot.binary import as_base64_unicode from polyglot.binary import as_base64_unicode
from polyglot.builtins import native_string_type, unicode_type
from polyglot.queue import Empty, Queue
from polyglot.urllib import urlparse
shutdown = object() shutdown = object()
@ -246,54 +254,68 @@ def uniq(vals):
return tuple(x for x in vals if x not in seen and not seen_add(x)) return tuple(x for x in vals if x not in seen and not seen_add(x))
def find_le(a, x): def insert_scripts(profile, *scripts):
'Find rightmost value in a less than or equal to x' sc = profile.scripts()
try: for script in scripts:
return a[bisect_right(a, x)] for existing in sc.findScripts(script.name()):
except IndexError: sc.remove(existing)
return a[-1] for script in scripts:
sc.insert(script)
class WebPage(QWebPage): def create_script(name, src, world=QWebEngineScript.ApplicationWorld, injection_point=QWebEngineScript.DocumentCreation, on_subframes=True):
script = QWebEngineScript()
script.setSourceCode(src)
script.setName(name)
script.setWorldId(world)
script.setInjectionPoint(injection_point)
script.setRunsOnSubFrames(on_subframes)
return script
def create_profile():
ans = getattr(create_profile, 'ans', None)
if ans is None:
ans = QWebEngineProfile(QApplication.instance())
ua = 'calibre-editor-preview ' + __version__
ans.setHttpUserAgent(ua)
from calibre.utils.resources import compiled_coffeescript
js = compiled_coffeescript('ebooks.oeb.display.utils', dynamic=False)
js += P('csscolorparser.js', data=True, allow_user_override=False)
js += compiled_coffeescript('ebooks.oeb.polish.preview', dynamic=False)
insert_scripts(ans, create_script('editor-preview.js', js))
# ans.url_handler = UrlSchemeHandler(ans)
# ans.installUrlSchemeHandler(QByteArray(FAKE_PROTOCOL.encode('ascii')), ans.url_handler)
s = ans.settings()
s.setDefaultTextEncoding('utf-8')
s.setAttribute(s.FullScreenSupportEnabled, False)
s.setAttribute(s.LinksIncludedInFocusChain, False)
create_profile.ans = ans
return ans
class WebPage(QWebEnginePage):
sync_requested = pyqtSignal(object, object, object) sync_requested = pyqtSignal(object, object, object)
split_requested = pyqtSignal(object, object) split_requested = pyqtSignal(object, object)
def __init__(self, parent): def __init__(self, parent):
QWebPage.__init__(self, parent) QWebEnginePage.__init__(self, create_profile(), parent)
settings = self.settings() secure_webengine(self, for_viewer=True)
apply_settings(settings, config().parse())
settings.setMaximumPagesInCache(0)
secure_web_page(settings)
settings.setAttribute(settings.PrivateBrowsingEnabled, True)
settings.setAttribute(settings.LinksIncludedInFocusChain, False)
settings.setAttribute(settings.DeveloperExtrasEnabled, True)
settings.setDefaultTextEncoding('utf-8')
data = 'data:text/css;charset=utf-8;base64,' data = 'data:text/css;charset=utf-8;base64,'
css = '[data-in-split-mode="1"] [data-is-block="1"]:hover { cursor: pointer !important; border-top: solid 5px green !important }' css = '[data-in-split-mode="1"] [data-is-block="1"]:hover { cursor: pointer !important; border-top: solid 5px green !important }'
data += as_base64_unicode(css) data += as_base64_unicode(css)
settings.setUserStyleSheetUrl(QUrl(data))
self.setNetworkAccessManager(NetworkAccessManager(self)) def javaScriptConsoleMessage(self, level, msg, linenumber, source_id):
self.setLinkDelegationPolicy(self.DelegateAllLinks) prints('%s:%s: %s' % (source_id, linenumber, msg))
self.mainFrame().javaScriptWindowObjectCleared.connect(self.init_javascript)
self.init_javascript()
def javaScriptConsoleMessage(self, msg, lineno, source_id): def acceptNavigationRequest(self, url, req_type, is_main_frame):
prints('preview js:%s:%s:'%(unicode_type(source_id), lineno), unicode_type(msg)) if req_type == self.NavigationTypeReload:
return True
def init_javascript(self): if url.scheme() == FAKE_PROTOCOL:
if not hasattr(self, 'js'): return True
from calibre.utils.resources import compiled_coffeescript open_url(url)
self.js = compiled_coffeescript('ebooks.oeb.display.utils', dynamic=False) return False
self.js += P('csscolorparser.js', data=True, allow_user_override=False)
self.js += compiled_coffeescript('ebooks.oeb.polish.preview', dynamic=False)
if isinstance(self.js, bytes):
self.js = self.js.decode('utf-8')
self._line_numbers = None
mf = self.mainFrame()
mf.addToJavaScriptWindowObject("py_bridge", self)
mf.evaluateJavaScript(self.js)
@pyqtSlot(native_string_type, native_string_type, native_string_type) @pyqtSlot(native_string_type, native_string_type, native_string_type)
def request_sync(self, tag_name, href, sourceline_address): def request_sync(self, tag_name, href, sourceline_address):
@ -303,7 +325,7 @@ class WebPage(QWebPage):
pass pass
def go_to_anchor(self, anchor, lnum): def go_to_anchor(self, anchor, lnum):
self.mainFrame().evaluateJavaScript('window.calibre_preview_integration.go_to_anchor(%s, %s)' % ( self.runjs('window.calibre_preview_integration.go_to_anchor(%s, %s)' % (
json.dumps(anchor), json.dumps(unicode_type(lnum)))) json.dumps(anchor), json.dumps(unicode_type(lnum))))
@pyqtSlot(native_string_type, native_string_type) @pyqtSlot(native_string_type, native_string_type)
@ -315,70 +337,48 @@ class WebPage(QWebPage):
_('Cannot split on the body tag'), show=True) _('Cannot split on the body tag'), show=True)
self.split_requested.emit(loc, totals) self.split_requested.emit(loc, totals)
@property def runjs(self, src, callback=None):
def line_numbers(self): if callback is None:
if self._line_numbers is None: self.runJavaScript(src, QWebEngineScript.ApplicationWorld)
def atoi(x): else:
try: self.runJavaScript(src, QWebEngineScript.ApplicationWorld, callback)
ans = int(x)
except (TypeError, ValueError):
ans = None
return ans
val = self.mainFrame().evaluateJavaScript('window.calibre_preview_integration.line_numbers()')
self._line_numbers = sorted(uniq(list(filter(lambda x:x is not None, map(atoi, val)))))
return self._line_numbers
def go_to_line(self, lnum):
try:
lnum = find_le(self.line_numbers, lnum)
except IndexError:
return
self.mainFrame().evaluateJavaScript(
'window.calibre_preview_integration.go_to_line(%d)' % lnum)
def go_to_sourceline_address(self, sourceline_address): def go_to_sourceline_address(self, sourceline_address):
lnum, tags = sourceline_address lnum, tags = sourceline_address
if lnum is None: if lnum is None:
return return
tags = [x.lower() for x in tags] tags = [x.lower() for x in tags]
self.mainFrame().evaluateJavaScript( self.runjs('window.calibre_preview_integration.go_to_sourceline_address(%d, %s)' % (lnum, json.dumps(tags)))
'window.calibre_preview_integration.go_to_sourceline_address(%d, %s)' % (lnum, json.dumps(tags)))
def split_mode(self, enabled): def split_mode(self, enabled):
self.mainFrame().evaluateJavaScript( self.runjs(
'window.calibre_preview_integration.split_mode(%s)' % ( 'window.calibre_preview_integration.split_mode(%s)' % (
'true' if enabled else 'false')) 'true' if enabled else 'false'))
class WebView(QWebView): class WebView(QWebEngineView):
def __init__(self, parent=None): def __init__(self, parent=None):
QWebView.__init__(self, parent) QWebEngineView.__init__(self, parent)
self.inspector = QWebInspector(self) self.inspector = QWebEngineView(self)
w = QApplication.instance().desktop().availableGeometry(self).width() w = QApplication.instance().desktop().availableGeometry(self).width()
self._size_hint = QSize(int(w/3), int(w/2)) self._size_hint = QSize(int(w/3), int(w/2))
self._page = WebPage(self) self._page = WebPage(self)
self.setPage(self._page) self.setPage(self._page)
self.inspector.setPage(self._page)
self.clear() self.clear()
self.setAcceptDrops(False) self.setAcceptDrops(False)
self.renderProcessTerminated.connect(self.render_process_terminated)
def render_process_terminated(self):
error_dialog(self, _('Render process crashed'), _(
'The Qt WebEngine Render process has crashed so Preview/Live css'
' will not work. You should try restarting the editor.'), show=True)
def sizeHint(self): def sizeHint(self):
return self._size_hint return self._size_hint
def refresh(self): def refresh(self):
self.pageAction(self.page().Reload).trigger() self.pageAction(QWebEnginePage.Reload).trigger()
@property
def scroll_pos(self):
mf = self.page().mainFrame()
return (mf.scrollBarValue(Qt.Horizontal), mf.scrollBarValue(Qt.Vertical))
@scroll_pos.setter
def scroll_pos(self, val):
mf = self.page().mainFrame()
mf.setScrollBarValue(Qt.Horizontal, val[0])
mf.setScrollBarValue(Qt.Vertical, val[1])
def clear(self): def clear(self):
self.setHtml(_( self.setHtml(_(
@ -394,17 +394,18 @@ class WebView(QWebView):
''')) '''))
def inspect(self): def inspect(self):
self.inspector.parent().show() raise NotImplementedError('TODO: Implement this')
self.inspector.parent().raise_() # self.inspector.parent().show()
self.pageAction(self.page().InspectElement).trigger() # self.inspector.parent().raise_()
# self.pageAction(self.page().InspectElement).trigger()
def contextMenuEvent(self, ev): def contextMenuEvent(self, ev):
menu = QMenu(self) menu = QMenu(self)
p = self.page() p = self._page
mf = p.mainFrame() mf = p.mainFrame()
r = mf.hitTestContent(ev.pos()) r = mf.hitTestContent(ev.pos())
url = unicode_type(r.linkUrl().toString(NO_URL_FORMATTING)).strip() url = unicode_type(r.linkUrl().toString(NO_URL_FORMATTING)).strip()
ca = self.pageAction(QWebPage.Copy) ca = self.pageAction(QWebEnginePage.Copy)
if ca.isEnabled(): if ca.isEnabled():
menu.addAction(ca) menu.addAction(ca)
menu.addAction(actions['reload-preview']) menu.addAction(actions['reload-preview'])
@ -429,11 +430,10 @@ 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().sync_requested.connect(self.request_sync) self.view._page.sync_requested.connect(self.request_sync)
self.view.page().split_requested.connect(self.request_split) self.view._page.split_requested.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
self.inspector.setPage(self.view.page())
l.addWidget(self.view) l.addWidget(self.view)
self.bar = QToolBar(self) self.bar = QToolBar(self)
l.addWidget(self.bar) l.addWidget(self.bar)
@ -477,7 +477,7 @@ class Preview(QWidget):
self.search = HistoryLineEdit2(self) self.search = HistoryLineEdit2(self)
self.search.initialize('tweak_book_preview_search') self.search.initialize('tweak_book_preview_search')
self.search.setPlaceholderText(_('Search in preview')) self.search.setPlaceholderText(_('Search in preview'))
connect_lambda(self.search.returnPressed, self, lambda self: self.find('next')) self.search.returnPressed.connect(self.find_next)
self.bar.addSeparator() self.bar.addSeparator()
self.bar.addWidget(self.search) self.bar.addWidget(self.search)
for d in ('next', 'prev'): for d in ('next', 'prev'):
@ -487,8 +487,8 @@ class Preview(QWidget):
def find(self, direction): def find(self, direction):
text = unicode_type(self.search.text()) text = unicode_type(self.search.text())
self.view.findText(text, QWebPage.FindWrapsAroundDocument | ( self.view.findText(text, QWebEnginePage.FindWrapsAroundDocument | (
QWebPage.FindBackward if direction == 'prev' else QWebPage.FindFlags(0))) QWebEnginePage.FindBackward if direction == 'prev' else QWebEnginePage.FindFlags(0)))
def find_next(self): def find_next(self):
self.find('next') self.find('next')
@ -505,7 +505,7 @@ class Preview(QWidget):
else: else:
name = c.href_to_name(href, self.current_name) if href else None name = c.href_to_name(href, self.current_name) if href else None
if name == self.current_name: 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, lnum)
if name and c.exists(name) and c.mime_map[name] in OEB_DOCS: 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) return self.link_clicked.emit(name, urlparse(href).fragment or TOP)
self.sync_requested.emit(self.current_name, lnum) self.sync_requested.emit(self.current_name, lnum)
@ -528,7 +528,7 @@ class Preview(QWidget):
return # Happens if current_sync_request is None return # Happens if current_sync_request is None
sourceline_address = self.current_sync_request[1] sourceline_address = self.current_sync_request[1]
self.current_sync_request = None self.current_sync_request = None
self.view.page().go_to_sourceline_address(sourceline_address) self.view._page.go_to_sourceline_address(sourceline_address)
def report_worker_launch_error(self): def report_worker_launch_error(self):
if parse_worker.launch_error is not None: if parse_worker.launch_error is not None:
@ -617,10 +617,10 @@ class Preview(QWidget):
if checked: if checked:
self.split_start_requested.emit() self.split_start_requested.emit()
else: else:
self.view.page().split_mode(False) self.view._page.split_mode(False)
def do_start_split(self): def do_start_split(self):
self.view.page().split_mode(True) self.view._page.split_mode(True)
def stop_split(self): def stop_split(self):
actions['split-in-preview'].setChecked(False) actions['split-in-preview'].setChecked(False)
@ -633,7 +633,7 @@ class Preview(QWidget):
self.stop_split() self.stop_split()
def apply_settings(self): def apply_settings(self):
s = self.view.page().settings() s = self.view.settings()
s.setFontSize(s.DefaultFontSize, tprefs['preview_base_font_size']) s.setFontSize(s.DefaultFontSize, tprefs['preview_base_font_size'])
s.setFontSize(s.DefaultFixedFontSize, tprefs['preview_mono_font_size']) s.setFontSize(s.DefaultFixedFontSize, tprefs['preview_mono_font_size'])
s.setFontSize(s.MinimumLogicalFontSize, tprefs['preview_minimum_font_size']) s.setFontSize(s.MinimumLogicalFontSize, tprefs['preview_minimum_font_size'])