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 = {}
|
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
|
||||||
|
|
||||||
|
@ -4,15 +4,14 @@ __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
|
||||||
|
|
||||||
from calibre.gui2.viewer.flip import SlideFlip
|
from calibre.gui2.viewer.flip import SlideFlip
|
||||||
@ -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():
|
||||||
|
@ -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):
|
||||||
|
@ -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,16 +494,18 @@ 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):
|
||||||
item = self.toc_model.itemFromIndex(index)
|
if QApplication.mouseButtons() & Qt.LeftButton:
|
||||||
if item.abspath is not None:
|
item = self.toc_model.itemFromIndex(index)
|
||||||
if not os.path.exists(item.abspath):
|
if item.abspath is not None:
|
||||||
return error_dialog(self, _('No such location'),
|
if not os.path.exists(item.abspath):
|
||||||
_('The location pointed to by this item'
|
return error_dialog(self, _('No such location'),
|
||||||
' does not exist.'), show=True)
|
_('The location pointed to by this item'
|
||||||
url = QUrl.fromLocalFile(item.abspath)
|
' does not exist.'), show=True)
|
||||||
if item.fragment:
|
url = QUrl.fromLocalFile(item.abspath)
|
||||||
url.setFragment(item.fragment)
|
if item.fragment:
|
||||||
self.link_clicked(url)
|
url.setFragment(item.fragment)
|
||||||
|
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 <
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user