mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
8170161baf
commit
86cb606dc9
@ -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
|
||||
|
||||
|
@ -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():
|
||||
|
@ -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):
|
||||
|
@ -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 <
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user