diff --git a/resources/toc.js b/resources/toc.js new file mode 100644 index 0000000000..36bc89c32a --- /dev/null +++ b/resources/toc.js @@ -0,0 +1,53 @@ +/* vim:fileencoding=utf-8 + * + * Copyright (C) 2019 Kovid Goyal + * + * Distributed under terms of the GPLv3 license + */ +(function() { + "use strict"; + var com_id = "COM_ID"; + var com_counter = 0; + + function onclick(event) { + // We dont want this event to trigger onclick on this element's parent + // block, if any. + event.stopPropagation(); + var frac = window.pageYOffset/document.body.scrollHeight; + var loc = []; + var totals = []; + var block = event.currentTarget; + var parent = block; + while (parent && parent.tagName && parent.tagName.toLowerCase() !== 'body') { + totals.push(parent.parentNode.children.length); + var num = 0; + var sibling = parent.previousElementSibling; + while (sibling) { + num += 1; + sibling = sibling.previousElementSibling; + } + loc.push(num); + parent = parent.parentNode; + } + loc.reverse(); + totals.reverse(); + com_counter += 1; + window.calibre_toc_data = [block.tagName.toLowerCase(), block.id, loc, totals, frac]; + document.title = com_id + '-' + com_counter; + } + + function find_blocks() { + for (let elem of document.body.getElementsByTagName('*')) { + style = window.getComputedStyle(elem); + if (style.display === 'block' || style.display === 'flex-box' || style.display === 'box') { + elem.classList.add("calibre_toc_hover"); + elem.onclick = onclick; + } + } + } + + var style = document.createElement('style'); + style.innerText = 'body { background-color: white }' + '.calibre_toc_hover:hover { cursor: pointer !important; border-top: solid 5px green !important }' + '::selection {background:#ffff00; color:#000;}'; + document.documentElement.appendChild(style); + find_blocks(); +})(); diff --git a/src/calibre/ebooks/oeb/polish/choose.coffee b/src/calibre/ebooks/oeb/polish/choose.coffee deleted file mode 100644 index 9843b29224..0000000000 --- a/src/calibre/ebooks/oeb/polish/choose.coffee +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env coffee -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai - -### - Copyright 2013, Kovid Goyal - Released under the GPLv3 License -### - - -if window?.calibre_utils - log = window.calibre_utils.log - -class AnchorLocator - - ### - # Allow the user to click on any block level element to choose it as the - # location for an anchor. - ### - constructor: () -> - if not this instanceof arguments.callee - throw new Error('AnchorLocator constructor called as function') - - find_blocks: () => - for elem in document.body.getElementsByTagName('*') - style = window.getComputedStyle(elem) - if style.display in ['block', 'flex-box', 'box'] - elem.className += " calibre_toc_hover" - elem.onclick = this.onclick - - onclick: (event) -> - # We dont want this event to trigger onclick on this element's parent - # block, if any. - event.stopPropagation() - frac = window.pageYOffset/document.body.scrollHeight - loc = [] - totals = [] - parent = this - while parent and parent.tagName.toLowerCase() != '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() - - window.py_bridge.onclick(this, JSON.stringify(loc), JSON.stringify(totals), frac) - return false - -calibre_anchor_locator = new AnchorLocator() -calibre_anchor_locator.find_blocks() - - diff --git a/src/calibre/gui2/toc/location.py b/src/calibre/gui2/toc/location.py index a76faae5b2..d36b83c76e 100644 --- a/src/calibre/gui2/toc/location.py +++ b/src/calibre/gui2/toc/location.py @@ -1,102 +1,95 @@ #!/usr/bin/env python2 # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +# License: GPLv3 Copyright: 2013, Kovid Goyal from __future__ import absolute_import, division, print_function, unicode_literals -__license__ = 'GPL v3' -__copyright__ = '2013, Kovid Goyal ' -__docformat__ = 'restructuredtext en' - import json -from PyQt5.Qt import (QWidget, QGridLayout, QListWidget, QSize, Qt, QUrl, - pyqtSlot, pyqtSignal, QVBoxLayout, QFrame, QLabel, - QLineEdit, QTimer, QPushButton, QIcon, QSplitter) -from PyQt5.QtWebKitWidgets import QWebView, QWebPage -from PyQt5.QtWebKit import QWebElement +from PyQt5.Qt import ( + QFrame, QGridLayout, QIcon, QLabel, QLineEdit, QListWidget, QPushButton, QSize, + QSplitter, Qt, QUrl, QVBoxLayout, QWidget, pyqtSignal +) +from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript, QWebEngineView -from calibre.ebooks.oeb.display.webview import load_html -from calibre.gui2 import error_dialog, question_dialog, gprefs, secure_web_page +from calibre.gui2 import error_dialog, gprefs, question_dialog +from calibre.gui2.webengine import secure_webengine from calibre.utils.logging import default_log -from polyglot.builtins import native_string_type, range, unicode_type -from polyglot.binary import as_base64_unicode +from calibre.utils.short_uuid import uuid4 +from polyglot.builtins import range, unicode_type -class Page(QWebPage): # {{{ +class Page(QWebEnginePage): # {{{ elem_clicked = pyqtSignal(object, object, object, object, object) + frag_shown = pyqtSignal(object) def __init__(self): self.log = default_log - QWebPage.__init__(self) - secure_web_page(self.settings()) - self.js = None - self.evaljs = self.mainFrame().evaluateJavaScript - nam = self.networkAccessManager() - nam.setNetworkAccessible(nam.NotAccessible) - self.setLinkDelegationPolicy(self.DelegateAllLinks) + self.current_frag = None + self.com_id = unicode_type(uuid4()) + QWebEnginePage.__init__(self) + secure_webengine(self.settings(), for_viewer=True) + self.titleChanged.connect(self.title_changed) + self.loadFinished.connect(self.show_frag) + s = QWebEngineScript() + s.setName('toc.js') + s.setInjectionPoint(QWebEngineScript.DocumentReady) + s.setRunsOnSubFrames(True) + s.setWorldId(QWebEngineScript.ApplicationWorld) + s.setSourceCode(P('toc.js', allow_user_override=False, data=True).decode('utf-8').replace('COM_ID', self.com_id)) + self.scripts().insert(s) - def javaScriptConsoleMessage(self, msg, lineno, msgid): + def javaScriptConsoleMessage(self, level, msg, lineno, msgid): self.log('JS:', unicode_type(msg)) - def javaScriptAlert(self, frame, msg): + def javaScriptAlert(self, origin, msg): self.log(unicode_type(msg)) - @pyqtSlot(result=bool) - def shouldInterruptJavaScript(self): - return True + def title_changed(self, title): + parts = title.split('-', 1) + if len(parts) == 2 and parts[0] == self.com_id: + self.runJavaScript( + 'JSON.stringify(window.calibre_toc_data)', + QWebEngineScript.ApplicationWorld, self.onclick) - @pyqtSlot(QWebElement, native_string_type, native_string_type, float) - def onclick(self, elem, loc, totals, frac): - elem_id = unicode_type(elem.attribute('id')) or None - tag = unicode_type(elem.tagName()).lower() - self.elem_clicked.emit(tag, frac, elem_id, json.loads(unicode_type(loc)), json.loads(unicode_type(totals))) + def onclick(self, data): + try: + tag, elem_id, loc, totals, frac = json.loads(data) + except Exception: + return + elem_id = elem_id or None + self.elem_clicked.emit(tag, frac, elem_id, loc, totals) + + def show_frag(self, ok): + if ok and self.current_frag: + self.runJavaScript(''' + document.location = '#non-existent-anchor'; + document.location = '#' + {0}; + '''.format(json.dumps(self.current_frag))) + self.current_frag = None + self.runJavaScript('window.pageYOffset/document.body.scrollHeight', QWebEngineScript.ApplicationWorld, self.frag_shown.emit) - def load_js(self): - if self.js is None: - from calibre.utils.resources import compiled_coffeescript - self.js = compiled_coffeescript('ebooks.oeb.display.utils') - self.js += compiled_coffeescript('ebooks.oeb.polish.choose') - if isinstance(self.js, bytes): - self.js = self.js.decode('utf-8') - self.mainFrame().addToJavaScriptWindowObject("py_bridge", self) - self.evaljs(self.js) # }}} -class WebView(QWebView): # {{{ +class WebView(QWebEngineView): # {{{ elem_clicked = pyqtSignal(object, object, object, object, object) + frag_shown = pyqtSignal(object) def __init__(self, parent): - QWebView.__init__(self, parent) + QWebEngineView.__init__(self, parent) self._page = Page() self._page.elem_clicked.connect(self.elem_clicked) + self._page.frag_shown.connect(self.frag_shown) self.setPage(self._page) - raw = ''' - body { background-color: white } - .calibre_toc_hover:hover { cursor: pointer !important; border-top: solid 5px green !important } - ''' - raw = '::selection {background:#ffff00; color:#000;}\n'+raw - data = 'data:text/css;charset=utf-8;base64,' - data += as_base64_unicode(raw) - self.settings().setUserStyleSheetUrl(QUrl(data)) - def load_js(self): - self.page().load_js() + def load_path(self, path, frag=None): + self._page.current_frag = frag + self.setUrl(QUrl.fromLocalFile(path)) def sizeHint(self): return QSize(1500, 300) - - def show_frag(self, frag): - self.page().mainFrame().scrollToAnchor(frag) - - @property - def scroll_frac(self): - try: - val = float(self.page().evaljs('window.pageYOffset/document.body.scrollHeight')) - except (TypeError, ValueError): - val = 0 - return val # }}} @@ -105,6 +98,8 @@ class ItemEdit(QWidget): def __init__(self, parent, prefs=None): QWidget.__init__(self, parent) self.prefs = prefs or gprefs + self.pending_search = None + self.current_frag = None self.setLayout(QVBoxLayout()) self.la = la = QLabel(''+_( @@ -126,6 +121,8 @@ class ItemEdit(QWidget): w.setLayout(l) self.view = WebView(self) self.view.elem_clicked.connect(self.elem_clicked) + self.view.frag_shown.connect(self.update_dest_label, type=Qt.QueuedConnection) + self.view.loadFinished.connect(self.load_finished, type=Qt.QueuedConnection) l.addWidget(self.view, 0, 0, 1, 3) sp.addWidget(w) @@ -178,6 +175,11 @@ class ItemEdit(QWidget): if state is not None: sp.restoreState(state) + def load_finished(self, ok): + if self.pending_search: + self.pending_search() + self.pending_search = None + def keyPressEvent(self, ev): if ev.key() in (Qt.Key_Return, Qt.Key_Enter) and self.search_text.hasFocus(): # Prevent pressing enter in the search box from triggering the dialog's accept() method @@ -187,11 +189,14 @@ class ItemEdit(QWidget): def find(self, forwards=True): text = unicode_type(self.search_text.text()).strip() - flags = QWebPage.FindFlags(0) if forwards else QWebPage.FindBackward + flags = QWebEnginePage.FindFlags(0) if forwards else QWebEnginePage.FindBackward + self.find_data = text, flags, forwards + self.view.findText(text, flags, self.find_callback) + + def find_callback(self, found): d = self.dest_list - if d.count() == 1: - flags |= QWebPage.FindWrapsAroundDocument - if not self.view.findText(text, flags) and text: + text, flags, forwards = self.find_data + if not found and text: if d.count() == 1: return error_dialog(self, _('No match found'), _('No match found for: %s')%text, show=True) @@ -223,7 +228,6 @@ class ItemEdit(QWidget): def current_changed(self, item): name = self.current_name = unicode_type(item.data(Qt.DisplayRole) or '') - self.current_frag = None path = self.container.name_to_abspath(name) # Ensure encoding map is populated root = self.container.parsed(name) @@ -236,17 +240,10 @@ class ItemEdit(QWidget): for x in reversed(nasty): body[0].insert(0, x) self.container.commit_item(name, keep_parsed=True) - encoding = self.container.encoding_map.get(name, None) or 'utf-8' - - load_html(path, self.view, codec=encoding, - mime_type=self.container.mime_map[name]) - self.view.load_js() + self.view.load_path(path, self.current_frag) + self.current_frag = None self.dest_label.setText(self.base_msg + '
' + _('File:') + ' ' + name + '
' + _('Top of the file')) - if hasattr(self, 'pending_search'): - f = self.pending_search - del self.pending_search - f() def __call__(self, item, where): self.current_item, self.current_where = item, where @@ -271,20 +268,9 @@ class ItemEdit(QWidget): self.dest_list.setCurrentRow(dest_index) self.dest_list.blockSignals(False) item = self.dest_list.item(dest_index) - self.current_changed(item) if frag: self.current_frag = frag - QTimer.singleShot(1, self.show_frag) - - def show_frag(self): - self.view.show_frag(self.current_frag) - QTimer.singleShot(1, self.check_frag) - - def check_frag(self): - pos = self.view.scroll_frac - if pos == 0: - self.current_frag = None - self.update_dest_label() + self.current_changed(item) def get_loctext(self, frac): frac = int(round(frac * 100)) @@ -301,8 +287,7 @@ class ItemEdit(QWidget): self.dest_label.setText(self.base_msg + '
' + _('File:') + ' ' + self.current_name + '
' + loctext) - def update_dest_label(self): - val = self.view.scroll_frac + def update_dest_label(self, val): self.dest_label.setText(self.base_msg + '
' + _('File:') + ' ' + self.current_name + '
' + self.get_loctext(val))