From 751a6abd04448aedbd73820814fa0c864a5dcb0c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 6 Mar 2013 23:39:08 +0530 Subject: [PATCH] ToC Editor: Location browsing implemented --- resources/compiled_coffeescript.zip | Bin 68147 -> 70081 bytes src/calibre/ebooks/oeb/polish/choose.coffee | 41 ++++ src/calibre/ebooks/oeb/polish/container.py | 19 +- src/calibre/gui2/toc/location.py | 195 ++++++++++++++++++++ src/calibre/gui2/toc/main.py | 28 ++- 5 files changed, 274 insertions(+), 9 deletions(-) create mode 100644 src/calibre/ebooks/oeb/polish/choose.coffee create mode 100644 src/calibre/gui2/toc/location.py diff --git a/resources/compiled_coffeescript.zip b/resources/compiled_coffeescript.zip index b90e6b50eaf8435e0a97c7bf7f2c8de9ca1bede2..ccc9b67225c25ebe06b8a889141f9cc9a7a97cca 100644 GIT binary patch delta 1141 zcmaJ>OK1~87|v`TN!nTwn_8Marcv6YrrQ>(2qlLWU&U8MC>6wIvzbknZg=9&Bx$fA z$6hVdvK|Cm1dofC9j0s{7W<%`#etLMy;(&b}YbwQd9BQg* zq-><9F07b!+{78SQ`kG^NniQaS@8g}r7j?tfI}X1XtJdR@@sV1eTmQ0aLP1s5z;Jz zd4g<6G1N>$n}^tfs#St1KJz>@Q306{nP>r7)Xj0lLxVzwWoaY$&%MC{Xzo+gqvDPeE*E+^6q2HEClm_5DUzV-zSHbZy4Y~yEW?H+(l zqB6#EnNt_tYB_C?oT-*#Fp=PWc4ZI2r6s7?NFT3)x``G?Qh4zw&LIY7PR_}isSoBdlY7iOK80noACRCu$}?mf-Uo*(qH z7yJI&d3M?YU_`p)cLstWq`lRoJRm9Ip1#_PiFISPz0Wcv& smqlD9(ST{uJ1{#$kkN#>^*6)xZb3#3J`M&BhHxeZhGXoEKm!5mDJ-r4 diff --git a/src/calibre/ebooks/oeb/polish/choose.coffee b/src/calibre/ebooks/oeb/polish/choose.coffee new file mode 100644 index 0000000000..5a9361bf18 --- /dev/null +++ b/src/calibre/ebooks/oeb/polish/choose.coffee @@ -0,0 +1,41 @@ +#!/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 + window.py_bridge.onclick(this, frac) + return false + +calibre_anchor_locator = new AnchorLocator() +calibre_anchor_locator.find_blocks() + + diff --git a/src/calibre/ebooks/oeb/polish/container.py b/src/calibre/ebooks/oeb/polish/container.py index 5c5f296fe1..dfdc09f373 100644 --- a/src/calibre/ebooks/oeb/polish/container.py +++ b/src/calibre/ebooks/oeb/polish/container.py @@ -72,6 +72,7 @@ class Container(object): self.mime_map = {} self.name_path_map = {} self.dirtied = set() + self.encoding_map = {} # Map of relative paths with '/' separators from root of unzipped ePub # to absolute paths on filesystem with os-specific separators @@ -162,27 +163,29 @@ class Container(object): data = data[3:] if bom_enc is not None: try: + self.used_encoding = bom_enc return fix_data(data.decode(bom_enc)) except UnicodeDecodeError: pass try: + self.used_encoding = 'utf-8' return fix_data(data.decode('utf-8')) except UnicodeDecodeError: pass - data, _ = xml_to_unicode(data) + data, self.used_encoding = xml_to_unicode(data) return fix_data(data) def parse_xml(self, data): - data = xml_to_unicode(data, strip_encoding_pats=True, assume_utf8=True, - resolve_entities=True)[0].strip() + data, self.used_encoding = xml_to_unicode( + data, strip_encoding_pats=True, assume_utf8=True, resolve_entities=True) return etree.fromstring(data, parser=RECOVER_PARSER) def parse_xhtml(self, data, fname): try: - return parse_html(data, log=self.log, - decoder=self.decode, - preprocessor=self.html_preprocessor, - filename=fname, non_html_file_tags={'ncx'}) + return parse_html( + data, log=self.log, decoder=self.decode, + preprocessor=self.html_preprocessor, filename=fname, + non_html_file_tags={'ncx'}) except NotHTML: return self.parse_xml(data) @@ -212,9 +215,11 @@ class Container(object): def parsed(self, name): ans = self.parsed_cache.get(name, None) if ans is None: + self.used_encoding = None mime = self.mime_map.get(name, guess_type(name)) ans = self.parse(self.name_path_map[name], mime) self.parsed_cache[name] = ans + self.encoding_map[name] = self.used_encoding return ans @property diff --git a/src/calibre/gui2/toc/location.py b/src/calibre/gui2/toc/location.py new file mode 100644 index 0000000000..e45e9c025f --- /dev/null +++ b/src/calibre/gui2/toc/location.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2013, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from base64 import b64encode + +from PyQt4.Qt import (QWidget, QGridLayout, QListWidget, QSize, Qt, QUrl, + pyqtSlot, pyqtSignal, QVBoxLayout, QFrame, QLabel, + QLineEdit) +from PyQt4.QtWebKit import QWebView, QWebPage, QWebElement + +from calibre.ebooks.oeb.display.webview import load_html +from calibre.utils.logging import default_log + +class Page(QWebPage): # {{{ + + elem_clicked = pyqtSignal(object, object, object, object) + + def __init__(self): + self.log = default_log + QWebPage.__init__(self) + self.js = None + self.evaljs = self.mainFrame().evaluateJavaScript + self.bridge_value = None + nam = self.networkAccessManager() + nam.setNetworkAccessible(nam.NotAccessible) + self.setLinkDelegationPolicy(self.DelegateAllLinks) + + def javaScriptConsoleMessage(self, msg, lineno, msgid): + self.log(u'JS:', unicode(msg)) + + def javaScriptAlert(self, frame, msg): + self.log(unicode(msg)) + + def shouldInterruptJavaScript(self): + return True + + @pyqtSlot(QWebElement, float) + def onclick(self, elem, frac): + elem_id = unicode(elem.attribute('id')) or None + tag = unicode(elem.tagName()).lower() + parent = elem + loc = [] + while unicode(parent.tagName()).lower() != 'body': + num = 0 + sibling = parent.previousSibling() + while not sibling.isNull(): + num += 1 + sibling = sibling.previousSibling() + loc.insert(0, num) + parent = parent.parent() + self.elem_clicked.emit(tag, frac, elem_id, tuple(loc)) + + 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') + self.mainFrame().addToJavaScriptWindowObject("py_bridge", self) + self.evaljs(self.js) +# }}} + +class WebView(QWebView): # {{{ + + elem_clicked = pyqtSignal(object, object, object, object) + + def __init__(self, parent): + QWebView.__init__(self, parent) + self._page = Page() + self._page.elem_clicked.connect(self.elem_clicked) + 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 += b64encode(raw.encode('utf-8')) + self.settings().setUserStyleSheetUrl(QUrl(data)) + + def load_js(self): + self.page().load_js() + + def sizeHint(self): + return QSize(1500, 300) +# }}} + +class ItemEdit(QWidget): + + def __init__(self, parent): + QWidget.__init__(self, parent) + self.l = l = QGridLayout() + self.setLayout(l) + + self.la = la = QLabel(''+_( + 'Select a destination for the Table of Contents entry')) + l.addWidget(la, 0, 0, 1, 3) + + self.dest_list = dl = QListWidget(self) + dl.setMinimumWidth(250) + dl.currentItemChanged.connect(self.current_changed) + l.addWidget(dl, 1, 0) + + self.view = WebView(self) + self.view.elem_clicked.connect(self.elem_clicked) + l.addWidget(self.view, 1, 1) + + self.f = f = QFrame() + f.setFrameShape(f.StyledPanel) + f.setMinimumWidth(250) + l.addWidget(f, 1, 2) + l = f.l = QVBoxLayout() + f.setLayout(l) + + f.la = la = QLabel('

'+_( + 'Here you can choose a destination for the Table of Contents\' entry' + ' to point to. First choose a file from the book in the left-most panel. The' + ' file will open in the central panel.

' + + 'Then choose a location inside the file. To do so, simply click on' + ' the place in the central panel that you want to use as the' + ' destination. As you move the mouse around the central panel, a' + ' thick green line appears, indicating the precise location' + ' that will be selected when you click.')) + la.setStyleSheet('QLabel { margin-bottom: 20px }') + la.setWordWrap(True) + l.addWidget(la) + + f.la2 = la = QLabel(_('&Name of the ToC entry:')) + l.addWidget(la) + self.name = QLineEdit(self) + la.setBuddy(self.name) + l.addWidget(self.name) + + self.base_msg = _('Currently selected destination:') + self.dest_label = la = QLabel(self.base_msg) + la.setTextFormat(Qt.PlainText) + la.setWordWrap(True) + la.setStyleSheet('QLabel { margin-top: 20px }') + l.addWidget(la) + + l.addStretch() + + def load(self, container): + self.container = container + spine_names = [container.abspath_to_name(p) for p in + container.spine_items] + spine_names = [n for n in spine_names if container.has_name(n)] + self.dest_list.addItems(spine_names) + + def current_changed(self, item): + name = self.current_name = unicode(item.data(Qt.DisplayRole).toString()) + path = self.container.name_to_abspath(name) + # Ensure encoding map is populated + self.container.parsed(name) + 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.dest_label.setText(self.base_msg + '\n' + _('File:') + ' ' + + name + '\n' + _('Top of the file')) + + def __call__(self, item, where): + self.current_item, self.current_where = item, where + self.current_name = None + self.current_frag = None + if item is None: + self.dest_list.setCurrentRow(0) + self.name.setText(_('(Untitled)')) + self.dest_label.setText(self.base_msg + '\n' + _('None')) + + def elem_clicked(self, tag, frac, elem_id, loc): + self.current_frag = elem_id or loc + frac = int(round(frac * 100)) + base = _('Location: A <%s> tag inside the file')%tag + if frac == 0: + loctext = _('Top of the file') + else: + loctext = _('Approximately %d%% from the top')%frac + loctext = base + ' [%s]'%loctext + self.dest_label.setText(self.base_msg + '\n' + + _('File:') + ' ' + self.current_name + '\n' + loctext) + + @property + def result(self): + return (self.current_item, self.current_where, self.current_name, + self.current_frag) + + diff --git a/src/calibre/gui2/toc/main.py b/src/calibre/gui2/toc/main.py index 2835ef9043..f8df524b1f 100644 --- a/src/calibre/gui2/toc/main.py +++ b/src/calibre/gui2/toc/main.py @@ -19,6 +19,7 @@ from calibre.ebooks.oeb.polish.container import get_container from calibre.ebooks.oeb.polish.toc import get_toc from calibre.gui2 import Application from calibre.gui2.progress_indicator import ProgressIndicator +from calibre.gui2.toc.location import ItemEdit from calibre.utils.logging import GUILog ICON_SIZE = 24 @@ -46,13 +47,15 @@ class ItemView(QFrame): # {{{ 'Entries with a green tick next to them point to a location that has ' 'been verified to exist. Entries with a red dot are broken and may need' ' to be fixed.')) + la.setStyleSheet('QLabel { margin-bottom: 20px }') la.setWordWrap(True) l = QVBoxLayout() rp.setLayout(l) - l.addWidget(la, alignment=Qt.AlignTop) + l.addWidget(la) self.add_new_to_root_button = b = QPushButton(_('Create a &new entry')) b.clicked.connect(self.add_new_to_root) - l.addWidget(b, alignment=Qt.AlignTop) + l.addWidget(b) + l.addStretch() def add_new_to_root(self): self.add_new_item.emit(None, None) @@ -267,7 +270,10 @@ class TOCEditor(QDialog): # {{{ la.setStyleSheet('QLabel { font-size: 20pt }') ll.addWidget(la, alignment=Qt.AlignHCenter|Qt.AlignTop) self.toc_view = TOCView(self) + self.toc_view.add_new_item.connect(self.add_new_item) s.addWidget(self.toc_view) + self.item_edit = ItemEdit(self) + s.addWidget(self.item_edit) bb = self.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) l.addWidget(bb) @@ -278,6 +284,23 @@ class TOCEditor(QDialog): # {{{ self.resize(950, 630) + def add_new_item(self, item, where): + self.item_edit(item, where) + self.stacks.setCurrentIndex(2) + + def accept(self): + if self.stacks.currentIndex() == 2: + self.toc_view.update_item(self.item_edit.result) + self.stacks.setCurrentIndex(1) + else: + super(TOCEditor, self).accept() + + def reject(self): + if self.stacks.currentIndex() == 2: + self.stacks.setCurrentIndex(1) + else: + super(TOCEditor, self).accept() + def start(self): t = Thread(target=self.explode) t.daemon = True @@ -293,6 +316,7 @@ class TOCEditor(QDialog): # {{{ def read_toc(self): self.pi.stopAnimation() self.toc_view(self.ebook) + self.item_edit.load(self.ebook) self.stacks.setCurrentIndex(1) # }}}