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 = {}
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

View File

@ -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():

View File

@ -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):

View File

@ -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 <

View File

@ -8,35 +8,100 @@ __copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
__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