E-book viewer: The Table of contents panel now tracks the current position in the book. As you scroll through the book, the entry you are currently on is highlighted in the Table fo Contents panel. Fixes #995489 ([Enhancement] Track and Display Book Chapters)

This commit is contained in:
Kovid Goyal 2012-05-29 20:03:56 +05:30
parent 8170161baf
commit 86cb606dc9
5 changed files with 151 additions and 25 deletions

View File

@ -29,7 +29,7 @@ def anchor_map(html):
ans = {} ans = {}
for match in re.finditer( for match in re.finditer(
r'''(?:id|name)\s*=\s*['"]([^'"]+)['"]''', html): r'''(?:id|name)\s*=\s*['"]([^'"]+)['"]''', html):
anchor = match.group(0) anchor = match.group(1)
ans[anchor] = ans.get(anchor, match.start()) ans[anchor] = ans.get(anchor, match.start())
return ans return ans

View File

@ -4,14 +4,13 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
# Imports {{{ # Imports {{{
import os, math, glob import os, math, glob, json
from base64 import b64encode from base64 import b64encode
from functools import partial from functools import partial
from PyQt4.Qt import (QSize, QSizePolicy, QUrl, SIGNAL, Qt, from PyQt4.Qt import (QSize, QSizePolicy, QUrl, SIGNAL, Qt, pyqtProperty,
QPainter, QPalette, QBrush, QFontDatabase, QDialog, QPainter, QPalette, QBrush, QFontDatabase, QDialog, QColor, QPoint,
QColor, QPoint, QImage, QRegion, QIcon, QImage, QRegion, QIcon, pyqtSignature, QAction, QMenu, QString,
pyqtSignature, QAction, QMenu,
pyqtSignal, QSwipeGesture, QApplication) pyqtSignal, QSwipeGesture, QApplication)
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
@ -60,7 +59,14 @@ class Document(QWebPage): # {{{
def __init__(self, shortcuts, parent=None, debug_javascript=False): def __init__(self, shortcuts, parent=None, debug_javascript=False):
QWebPage.__init__(self, parent) QWebPage.__init__(self, parent)
self.setObjectName("py_bridge") 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.debug_javascript = debug_javascript
self.anchor_positions = {}
self.index_anchors = set()
self.current_language = None self.current_language = None
self.loaded_javascript = False self.loaded_javascript = False
self.js_loader = JavaScriptLoader( self.js_loader = JavaScriptLoader(
@ -125,6 +131,14 @@ class Document(QWebPage): # {{{
def add_window_objects(self): def add_window_objects(self):
self.mainFrame().addToJavaScriptWindowObject("py_bridge", 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 self.loaded_javascript = False
def load_javascript_libraries(self): def load_javascript_libraries(self):
@ -143,6 +157,16 @@ class Document(QWebPage): # {{{
if self.hyphenate and getattr(self, 'loaded_lang', ''): if self.hyphenate and getattr(self, 'loaded_lang', ''):
self.javascript('do_hyphenation("%s")'%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): def after_load(self):
self.set_bottom_padding(0) self.set_bottom_padding(0)
self.fit_images() self.fit_images()
@ -153,6 +177,18 @@ class Document(QWebPage): # {{{
'document.body.style.marginRight').toString()) 'document.body.style.marginRight').toString())
if self.in_fullscreen_mode: if self.in_fullscreen_mode:
self.switch_to_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): def switch_to_fullscreen_mode(self):
self.in_fullscreen_mode = True self.in_fullscreen_mode = True
@ -531,6 +567,13 @@ class DocumentView(QWebView): # {{{
load_html(path, self, codec=path.encoding, mime_type=getattr(path, load_html(path, self, codec=path.encoding, mime_type=getattr(path,
'mime_type', None), pre_load_callback=callback) '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() self.turn_off_internal_scrollbars()
def initialize_scrollbar(self): def initialize_scrollbar(self):
@ -572,7 +615,8 @@ class DocumentView(QWebView): # {{{
if spine_index > -1: if spine_index > -1:
self.document.set_reference_prefix('%d.'%(spine_index+1)) self.document.set_reference_prefix('%d.'%(spine_index+1))
if scrolled: if scrolled:
self.manager.scrolled(self.document.scroll_fraction) self.manager.scrolled(self.document.scroll_fraction,
onload=True)
self.turn_off_internal_scrollbars() self.turn_off_internal_scrollbars()
if self.flipper.isVisible(): if self.flipper.isVisible():

View File

@ -29,10 +29,11 @@ class JavaScriptLoader(object):
CS = { CS = {
'cfi':'ebooks.oeb.display.cfi', 'cfi':'ebooks.oeb.display.cfi',
'indexing':'ebooks.oeb.display.indexing',
} }
ORDER = ('jquery', 'jquery_scrollTo', 'bookmarks', 'referencing', 'images', ORDER = ('jquery', 'jquery_scrollTo', 'bookmarks', 'referencing', 'images',
'hyphenation', 'hyphenator', 'cfi',) 'hyphenation', 'hyphenator', 'cfi', 'indexing',)
def __init__(self, dynamic_coffeescript=False): def __init__(self, dynamic_coffeescript=False):
@ -64,6 +65,7 @@ class JavaScriptLoader(object):
os.path.exists(calibre.__file__)) os.path.exists(calibre.__file__))
ans = compiled_coffeescript(src, dynamic=dynamic).decode('utf-8') ans = compiled_coffeescript(src, dynamic=dynamic).decode('utf-8')
self._cache[name] = ans self._cache[name] = ans
return ans return ans
def __call__(self, evaljs, lang, default_lang): def __call__(self, evaljs, lang, default_lang):

View File

@ -236,7 +236,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
x:self.goto_page(x/100.)) x:self.goto_page(x/100.))
self.search.search.connect(self.find) self.search.search.connect(self.find)
self.search.focus_to_library.connect(lambda: self.view.setFocus(Qt.OtherFocusReason)) 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.reference.goto.connect(self.goto)
self.bookmarks_menu = QMenu() self.bookmarks_menu = QMenu()
@ -494,6 +494,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.load_path(self.iterator.spine[spine_index]) self.load_path(self.iterator.spine[spine_index])
def toc_clicked(self, index): def toc_clicked(self, index):
if QApplication.mouseButtons() & Qt.LeftButton:
item = self.toc_model.itemFromIndex(index) item = self.toc_model.itemFromIndex(index)
if item.abspath is not None: if item.abspath is not None:
if not os.path.exists(item.abspath): if not os.path.exists(item.abspath):
@ -504,6 +505,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
if item.fragment: if item.fragment:
url.setFragment(item.fragment) url.setFragment(item.fragment)
self.link_clicked(url) self.link_clicked(url)
self.view.setFocus(Qt.OtherFocusReason)
def selection_changed(self, selected_text): def selection_changed(self, selected_text):
self.selected_text = selected_text.strip() self.selected_text = selected_text.strip()
@ -644,6 +646,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.current_page = self.iterator.spine[index] self.current_page = self.iterator.spine[index]
self.current_index = index self.current_index = index
self.set_page_number(self.view.scroll_fraction) self.set_page_number(self.view.scroll_fraction)
QTimer.singleShot(100, self.update_indexing_state)
if self.pending_search is not None: if self.pending_search is not None:
self.do_search(self.pending_search, self.do_search(self.pending_search,
self.pending_search_dir=='backwards') self.pending_search_dir=='backwards')
@ -859,8 +862,20 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.pos.set_value(page) self.pos.set_value(page)
self.set_vscrollbar_value(page) self.set_vscrollbar_value(page)
def scrolled(self, frac): def scrolled(self, frac, onload=False):
self.set_page_number(frac) 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): def next_document(self):
if (hasattr(self, 'current_index') and self.current_index < if (hasattr(self, 'current_index') and self.current_index <

View File

@ -8,35 +8,100 @@ __copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import re 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 from calibre.ebooks.metadata.toc import TOC as MTOC
class TOCItem(QStandardItem): class TOCItem(QStandardItem):
def __init__(self, toc): def __init__(self, spine, toc, depth, all_items):
text = toc.text text = toc.text
if text: if text:
text = re.sub(r'\s', ' ', text) text = re.sub(r'\s', ' ', text)
self.title = text
QStandardItem.__init__(self, text if text else '') QStandardItem.__init__(self, text if text else '')
self.abspath = toc.abspath self.abspath = toc.abspath
self.fragment = toc.fragment 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: for t in toc:
self.appendRow(TOCItem(t)) self.appendRow(TOCItem(spine, t, depth+1, all_items))
self.setFlags(Qt.ItemIsEnabled|Qt.ItemIsSelectable) 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 @classmethod
def type(cls): def type(cls):
return QStandardItem.UserType+10 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): class TOC(QStandardItemModel):
def __init__(self, spine, toc=None): def __init__(self, spine, toc=None):
QStandardItemModel.__init__(self) QStandardItemModel.__init__(self)
if toc is None: if toc is None:
toc = MTOC() toc = MTOC()
self.all_items = depth_first = []
for t in toc: for t in toc:
self.appendRow(TOCItem(t)) self.appendRow(TOCItem(spine, t, 0, depth_first))
self.setHorizontalHeaderItem(0, QStandardItem(_('Table of Contents'))) 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