Popup footnotes now work

This commit is contained in:
Kovid Goyal 2014-11-05 18:45:00 +05:30
parent d559fad378
commit f8e54c26e3
3 changed files with 135 additions and 18 deletions

View File

@ -29,17 +29,21 @@ inline_styles = (node) ->
return cnode
is_footnote_link = (node, url) ->
if not url or url.substr(0, 'file://'.length).toLowerCase() != 'file://'
return false # Ignore non-local links
# Check for epub:type="noteref", a little complex as we dont operate in XML
# mode
get_epub_type = (node, possible_values) ->
# Try to get the value of the epub:type attribute. Complex as we dont
# operate in XML mode
epub_type = node.getAttributeNS("http://www.idpf.org/2007/ops", 'type') or node.getAttribute('epub:type')
if not epub_type
for x in node.attributes # consider any xxx:type="noteref" attribute as marking a note
if x.nodeName and x.nodeValue == 'noteref' and x.nodeName.slice(-':type'.length) == ':type'
epub_type = 'noteref'
if x.nodeName and x.nodeValue in possible_values and x.nodeName.slice(-':type'.length) == ':type'
epub_type = x.nodeValue
break
return epub_type
is_footnote_link = (node, url) ->
if not url or url.substr(0, 'file://'.length).toLowerCase() != 'file://'
return false # Ignore non-local links
epub_type = get_epub_type(node, ['noteref'])
if epub_type and epub_type.toLowerCase() == 'noteref'
return true
@ -61,6 +65,35 @@ is_footnote_link = (node, url) ->
return false
is_epub_footnote = (node) ->
pv = ['note', 'footnote', 'rearnote']
epub_type = get_epub_type(node, pv)
if epub_type and epub_type.toLowerCase() in pv
return true
return false
get_parents_and_self = (node) ->
ans = []
while node and node isnt document.body
ans.push(node)
node = node.parentNode
return ans
get_page_break = (node) ->
style = getComputedStyle(node)
ans = {}
for x in ['before', 'after']
ans[x] = style.getPropertyValue('page-break-'.concat(x)) in ['always', 'left', 'right']
return ans
hide_children = (node) ->
for child in node.childNodes
if child.nodeType == Node.ELEMENT_NODE
if child.do_not_hide
hide_children(child)
else
child.style.display = 'none'
class CalibreExtract
# This class is a namespace to expose functions via the
# window.calibre_extract object.
@ -88,6 +121,39 @@ class CalibreExtract
ans[url] = 1
return JSON.stringify(ans)
show_footnote: (target, known_targets) ->
if not target
return
start_elem = document.getElementById(target)
if not start_elem
return
in_note = false
is_footnote_container = is_epub_footnote(start_elem)
for elem in get_parents_and_self(start_elem)
elem.do_not_hide = true
for elem in document.body.getElementsByTagName('*')
if in_note
if known_targets.hasOwnProperty(elem.getAttribute('id'))
in_note = false
continue
pb = get_page_break(elem)
if pb['before']
in_note = false
else if pb['after']
in_note = false
for child in elem.getElementsByTagName('*')
child.do_not_hide = true
else
elem.do_not_hide = true
else
if elem is start_elem
in_note = not is_footnote_container and not get_page_break(elem)['after']
if not in_note
for child in elem.getElementsByTagName('*')
child.do_not_hide = true
hide_children(document.body)
location.hash = '#' + target
if window?
window.calibre_extract = new CalibreExtract()

View File

@ -49,7 +49,8 @@ def apply_basic_settings(settings):
settings.setAttribute(QWebSettings.PluginsEnabled, False)
settings.setAttribute(QWebSettings.JavascriptCanOpenWindows, False)
settings.setAttribute(QWebSettings.JavascriptCanAccessClipboard, False)
settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True)
# PrivateBrowsing disables console messages
# settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True)
settings.setAttribute(QWebSettings.NotificationsEnabled, False)
settings.setThirdPartyCookiePolicy(QWebSettings.AlwaysBlockThirdPartyCookies)
@ -1319,6 +1320,7 @@ class DocumentView(QWebView): # {{{
if url.isValid() and self.manager is not None:
fd = self.footnotes.get_footnote_data(url)
if fd:
self.footnotes.show_footnote(fd)
self.manager.show_footnote_view()
ev.accept()
return

View File

@ -9,20 +9,55 @@ __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import json
from collections import defaultdict
from PyQt5.Qt import QUrl, QWidget, QHBoxLayout, QSize
from PyQt5.Qt import QUrl, QWidget, QHBoxLayout, QSize, pyqtSlot
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
from PyQt5.QtWebKit import QWebSettings
from calibre import prints
from calibre.constants import DEBUG
from calibre.ebooks.oeb.display.webview import load_html
class FootnotesPage(QWebPage):
def __init__(self, parent):
QWebPage.__init__(self, parent)
self.js_loader = None
self._footnote_data = ''
from calibre.gui2.viewer.documentview import apply_basic_settings
settings = self.settings()
apply_basic_settings(settings)
settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, False)
self.setLinkDelegationPolicy(self.DelegateAllLinks)
self.mainFrame().javaScriptWindowObjectCleared.connect(self.add_window_objects)
self.add_window_objects()
def add_window_objects(self, add_ready_listener=True):
self.mainFrame().addToJavaScriptWindowObject("py_bridge", self)
evaljs = self.mainFrame().evaluateJavaScript
if self.js_loader is not None:
for x in 'utils extract'.split():
evaljs(self.js_loader.get(x))
@pyqtSlot(str)
def debug(self, msg):
prints(msg)
@pyqtSlot(result=str)
def footnote_data(self):
return self._footnote_data
def set_footnote_data(self, target, known_targets):
self._footnote_data = json.dumps({'target':target, 'known_targets':known_targets})
if self._footnote_data:
self.mainFrame().evaluateJavaScript(
'data = JSON.parse(py_bridge.footnote_data()); calibre_extract.show_footnote(data["target"], data["known_targets"])')
def javaScriptAlert(self, frame, msg):
prints('FootnoteView:alert::', msg)
def javaScriptConsoleMessage(self, msg, lineno, source_id):
if DEBUG:
prints('FootnoteView:%s:%s:'%(unicode(source_id), lineno), unicode(msg))
class FootnotesView(QWidget):
@ -31,10 +66,12 @@ class FootnotesView(QWidget):
QWidget.__init__(self, parent)
self.l = l = QHBoxLayout(self)
self.view = v = QWebView(self)
self._page = FootnotesPage(v)
v.setPage(self._page)
l.addWidget(v)
def page(self):
return self.view.page()
return self._page
def sizeHint(self):
return QSize(400, 200)
@ -49,6 +86,8 @@ class Footnotes(object):
def set_footnotes_view(self, fv):
self.footnotes_view = fv
self.clone_settings()
fv.page().linkClicked.connect(self.view.link_clicked)
fv.page().js_loader = self.view.document.js_loader
def clone_settings(self):
source = self.view.document.settings()
@ -61,26 +100,28 @@ class Footnotes(object):
def clear(self):
self.footnote_data_cache = {}
self.known_footnote_targets = defaultdict(set)
self.showing_url = None
def spine_index(self, path):
def spine_path(self, path):
try:
return self.view.manager.iterator.spine.index(path)
si = self.view.manager.iterator.spine.index(path)
return self.view.manager.iterator.spine[si]
except (AttributeError, ValueError):
return -1
pass
def load_footnote_data(self, current_url):
fd = self.footnote_data_cache[current_url] = {}
try:
raw = self.view.document.javascript('window.calibre_extract.get_footnote_data()', typ='string')
for x in json.loads(raw):
for x in json.loads(raw or '{}'):
if x not in fd:
qu = QUrl(x)
path = qu.toLocalFile()
si = self.spine_index(path)
if si > -1:
spath = self.spine_path(path)
if spath is not None:
target = qu.fragment(QUrl.FullyDecoded)
fd[qu.toString()] = (path, target)
self.known_footnote_targets[path].add(target)
fd[qu.toString()] = (spath, target, qu)
self.known_footnote_targets[spath].add(target)
except Exception:
prints('Failed to get footnote data, with error:')
import traceback
@ -95,3 +136,11 @@ class Footnotes(object):
if fd is None:
fd = self.load_footnote_data(current_url)
return fd.get(qurl.toString())
def show_footnote(self, fd):
path, target, self.showing_url = fd
if hasattr(self, 'footnotes_view'):
if load_html(path, self.footnotes_view.view, codec=getattr(path, 'encoding', 'utf-8'),
mime_type=getattr(path, 'mime_type', 'text/html')):
self.footnotes_view.page().set_footnote_data(target, {k:True for k in self.known_footnote_targets[path]})