diff --git a/src/calibre/ebooks/oeb/iterator/spine.py b/src/calibre/ebooks/oeb/iterator/spine.py index 659a2500ea..9445da8def 100644 --- a/src/calibre/ebooks/oeb/iterator/spine.py +++ b/src/calibre/ebooks/oeb/iterator/spine.py @@ -29,7 +29,7 @@ def anchor_map(html): ans = {} for match in re.finditer( r'''(?:id|name)\s*=\s*['"]([^'"]+)['"]''', html): - anchor = match.group(0) + anchor = match.group(1) ans[anchor] = ans.get(anchor, match.start()) return ans diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 7c07deea4f..650911d9ca 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -4,15 +4,14 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' # Imports {{{ -import os, math, glob +import os, math, glob, json from base64 import b64encode from functools import partial -from PyQt4.Qt import (QSize, QSizePolicy, QUrl, SIGNAL, Qt, - QPainter, QPalette, QBrush, QFontDatabase, QDialog, - QColor, QPoint, QImage, QRegion, QIcon, - pyqtSignature, QAction, QMenu, - pyqtSignal, QSwipeGesture, QApplication) +from PyQt4.Qt import (QSize, QSizePolicy, QUrl, SIGNAL, Qt, pyqtProperty, + QPainter, QPalette, QBrush, QFontDatabase, QDialog, QColor, QPoint, + QImage, QRegion, QIcon, pyqtSignature, QAction, QMenu, QString, + pyqtSignal, QSwipeGesture, QApplication) from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings from calibre.gui2.viewer.flip import SlideFlip @@ -60,7 +59,14 @@ class Document(QWebPage): # {{{ def __init__(self, shortcuts, parent=None, debug_javascript=False): QWebPage.__init__(self, parent) self.setObjectName("py_bridge") + # Use this to pass arbitrary JSON encodable objects between python and + # javascript. In python get/set the value as: self.bridge_value. In + # javascript, get/set the value as: py_bridge.value + self.bridge_value = None + self.debug_javascript = debug_javascript + self.anchor_positions = {} + self.index_anchors = set() self.current_language = None self.loaded_javascript = False self.js_loader = JavaScriptLoader( @@ -125,6 +131,14 @@ class Document(QWebPage): # {{{ def add_window_objects(self): self.mainFrame().addToJavaScriptWindowObject("py_bridge", self) + self.javascript(''' + py_bridge.__defineGetter__('value', function() { + return JSON.parse(this._pass_json_value); + }); + py_bridge.__defineSetter__('value', function(val) { + this._pass_json_value = JSON.stringify(val); + }); + ''') self.loaded_javascript = False def load_javascript_libraries(self): @@ -143,6 +157,16 @@ class Document(QWebPage): # {{{ if self.hyphenate and getattr(self, 'loaded_lang', ''): self.javascript('do_hyphenation("%s")'%self.loaded_lang) + def _pass_json_value_getter(self): + val = json.dumps(self.bridge_value) + return QString(val) + + def _pass_json_value_setter(self, value): + self.bridge_value = json.loads(unicode(value)) + + _pass_json_value = pyqtProperty(QString, fget=_pass_json_value_getter, + fset=_pass_json_value_setter) + def after_load(self): self.set_bottom_padding(0) self.fit_images() @@ -153,6 +177,18 @@ class Document(QWebPage): # {{{ 'document.body.style.marginRight').toString()) if self.in_fullscreen_mode: self.switch_to_fullscreen_mode() + self.read_anchor_positions(use_cache=False) + + def read_anchor_positions(self, use_cache=True): + self.bridge_value = tuple(self.index_anchors) + self.javascript(u''' + py_bridge.value = book_indexing.anchor_positions(py_bridge.value, %s); + '''%('true' if use_cache else 'false')) + self.anchor_positions = self.bridge_value + if not isinstance(self.anchor_positions, dict): + # Some weird javascript error happened + self.anchor_positions = {} + return self.anchor_positions def switch_to_fullscreen_mode(self): self.in_fullscreen_mode = True @@ -531,6 +567,13 @@ class DocumentView(QWebView): # {{{ load_html(path, self, codec=path.encoding, mime_type=getattr(path, 'mime_type', None), pre_load_callback=callback) + entries = set() + for ie in getattr(path, 'index_entries', []): + if ie.start_anchor: + entries.add(ie.start_anchor) + if ie.end_anchor: + entries.add(ie.end_anchor) + self.document.index_anchors = entries self.turn_off_internal_scrollbars() def initialize_scrollbar(self): @@ -572,7 +615,8 @@ class DocumentView(QWebView): # {{{ if spine_index > -1: self.document.set_reference_prefix('%d.'%(spine_index+1)) if scrolled: - self.manager.scrolled(self.document.scroll_fraction) + self.manager.scrolled(self.document.scroll_fraction, + onload=True) self.turn_off_internal_scrollbars() if self.flipper.isVisible(): diff --git a/src/calibre/gui2/viewer/javascript.py b/src/calibre/gui2/viewer/javascript.py index 18dd516a8b..c4814cc04e 100644 --- a/src/calibre/gui2/viewer/javascript.py +++ b/src/calibre/gui2/viewer/javascript.py @@ -29,10 +29,11 @@ class JavaScriptLoader(object): CS = { 'cfi':'ebooks.oeb.display.cfi', + 'indexing':'ebooks.oeb.display.indexing', } ORDER = ('jquery', 'jquery_scrollTo', 'bookmarks', 'referencing', 'images', - 'hyphenation', 'hyphenator', 'cfi',) + 'hyphenation', 'hyphenator', 'cfi', 'indexing',) def __init__(self, dynamic_coffeescript=False): @@ -64,6 +65,7 @@ class JavaScriptLoader(object): os.path.exists(calibre.__file__)) ans = compiled_coffeescript(src, dynamic=dynamic).decode('utf-8') self._cache[name] = ans + return ans def __call__(self, evaljs, lang, default_lang): diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index 2b7dc8b41d..f368dfd270 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -236,7 +236,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer): x:self.goto_page(x/100.)) self.search.search.connect(self.find) self.search.focus_to_library.connect(lambda: self.view.setFocus(Qt.OtherFocusReason)) - self.toc.clicked[QModelIndex].connect(self.toc_clicked) + self.toc.pressed[QModelIndex].connect(self.toc_clicked) self.reference.goto.connect(self.goto) self.bookmarks_menu = QMenu() @@ -494,16 +494,18 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.load_path(self.iterator.spine[spine_index]) def toc_clicked(self, index): - item = self.toc_model.itemFromIndex(index) - if item.abspath is not None: - if not os.path.exists(item.abspath): - return error_dialog(self, _('No such location'), - _('The location pointed to by this item' - ' does not exist.'), show=True) - url = QUrl.fromLocalFile(item.abspath) - if item.fragment: - url.setFragment(item.fragment) - self.link_clicked(url) + if QApplication.mouseButtons() & Qt.LeftButton: + item = self.toc_model.itemFromIndex(index) + if item.abspath is not None: + if not os.path.exists(item.abspath): + return error_dialog(self, _('No such location'), + _('The location pointed to by this item' + ' does not exist.'), show=True) + url = QUrl.fromLocalFile(item.abspath) + if item.fragment: + url.setFragment(item.fragment) + self.link_clicked(url) + self.view.setFocus(Qt.OtherFocusReason) def selection_changed(self, selected_text): self.selected_text = selected_text.strip() @@ -644,6 +646,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.current_page = self.iterator.spine[index] self.current_index = index self.set_page_number(self.view.scroll_fraction) + QTimer.singleShot(100, self.update_indexing_state) if self.pending_search is not None: self.do_search(self.pending_search, self.pending_search_dir=='backwards') @@ -859,8 +862,20 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.pos.set_value(page) self.set_vscrollbar_value(page) - def scrolled(self, frac): + def scrolled(self, frac, onload=False): self.set_page_number(frac) + if not onload: + ap = self.view.document.read_anchor_positions() + self.update_indexing_state(ap) + + def update_indexing_state(self, anchor_positions=None): + if hasattr(self, 'current_index'): + if anchor_positions is None: + anchor_positions = self.view.document.read_anchor_positions() + items = self.toc_model.update_indexing_state(self.current_index, + self.view.document.ypos, anchor_positions) + if items: + self.toc.scrollTo(items[-1].index()) def next_document(self): if (hasattr(self, 'current_index') and self.current_index < diff --git a/src/calibre/gui2/viewer/toc.py b/src/calibre/gui2/viewer/toc.py index b702f46577..d05c2048e7 100644 --- a/src/calibre/gui2/viewer/toc.py +++ b/src/calibre/gui2/viewer/toc.py @@ -8,35 +8,100 @@ __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' import re -from PyQt4.Qt import QStandardItem, QStandardItemModel, Qt +from PyQt4.Qt import (QStandardItem, QStandardItemModel, Qt, QFont, + QApplication) from calibre.ebooks.metadata.toc import TOC as MTOC class TOCItem(QStandardItem): - def __init__(self, toc): + def __init__(self, spine, toc, depth, all_items): text = toc.text if text: text = re.sub(r'\s', ' ', text) + self.title = text QStandardItem.__init__(self, text if text else '') self.abspath = toc.abspath self.fragment = toc.fragment + all_items.append(self) + p = QApplication.palette() + self.base = p.base() + self.alternate_base = p.alternateBase() + self.bold_font = QFont(self.font()) + self.bold_font.setBold(True) + self.normal_font = self.font() for t in toc: - self.appendRow(TOCItem(t)) + self.appendRow(TOCItem(spine, t, depth+1, all_items)) self.setFlags(Qt.ItemIsEnabled|Qt.ItemIsSelectable) + spos = 0 + for i, si in enumerate(spine): + if si == self.abspath: + spos = i + break + am = getattr(spine[i], 'anchor_map', {}) + frag = self.fragment if (self.fragment and self.fragment in am) else None + self.starts_at = spos + self.start_anchor = frag + self.depth = depth + self.is_being_viewed = False @classmethod def type(cls): return QStandardItem.UserType+10 + def update_indexing_state(self, spine_index, scroll_pos, anchor_map): + is_being_viewed = False + if spine_index >= self.starts_at and spine_index <= self.ends_at: + start_pos = anchor_map.get(self.start_anchor, 0) + psp = [anchor_map.get(x, 0) for x in self.possible_end_anchors] + if self.ends_at == spine_index: + psp = [x for x in psp if x >= start_pos] + end_pos = min(psp) if psp else (scroll_pos+1 if self.ends_at == + spine_index else 0) + if spine_index > self.starts_at and spine_index < self.ends_at: + is_being_viewed = True + elif spine_index == self.starts_at and scroll_pos >= start_pos: + if spine_index != self.ends_at or scroll_pos < end_pos: + is_being_viewed = True + elif spine_index == self.ends_at and scroll_pos < end_pos: + if spine_index != self.starts_at or scroll_pos >= start_pos: + is_being_viewed = True + changed = is_being_viewed != self.is_being_viewed + self.is_being_viewed = is_being_viewed + if changed: + self.setFont(self.bold_font if is_being_viewed else self.normal_font) + self.setBackground(self.alternate_base if is_being_viewed else + self.base) + class TOC(QStandardItemModel): def __init__(self, spine, toc=None): QStandardItemModel.__init__(self) if toc is None: toc = MTOC() + self.all_items = depth_first = [] for t in toc: - self.appendRow(TOCItem(t)) + self.appendRow(TOCItem(spine, t, 0, depth_first)) self.setHorizontalHeaderItem(0, QStandardItem(_('Table of Contents'))) + for x in depth_first: + possible_enders = [ t for t in depth_first if t.depth <= x.depth + and t.starts_at >= x.starts_at and t is not x] + if possible_enders: + min_spine = min(t.starts_at for t in possible_enders) + possible_enders = { t.fragment for t in possible_enders if + t.starts_at == min_spine } + else: + min_spine = len(spine) - 1 + possible_enders = set() + x.ends_at = min_spine + x.possible_end_anchors = possible_enders + + def update_indexing_state(self, *args): + items_being_viewed = [] + for t in self.all_items: + t.update_indexing_state(*args) + if t.is_being_viewed: + items_being_viewed.append(t) + return items_being_viewed