mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Paged display: Indexing now (mostly) works
This commit is contained in:
parent
8cab25887d
commit
93a370ef8c
@ -6,20 +6,34 @@
|
|||||||
Released under the GPLv3 License
|
Released under the GPLv3 License
|
||||||
###
|
###
|
||||||
|
|
||||||
body_height = () ->
|
window_scroll_pos = (win=window) -> # {{{
|
||||||
db = document.body
|
if typeof(win.pageXOffset) == 'number'
|
||||||
dde = document.documentElement
|
x = win.pageXOffset
|
||||||
if db? and dde?
|
y = win.pageYOffset
|
||||||
return Math.max(db.scrollHeight, dde.scrollHeight, db.offsetHeight,
|
else # IE < 9
|
||||||
dde.offsetHeight, db.clientHeight, dde.clientHeight)
|
if document.body and ( document.body.scrollLeft or document.body.scrollTop )
|
||||||
return 0
|
x = document.body.scrollLeft
|
||||||
|
y = document.body.scrollTop
|
||||||
|
else if document.documentElement and ( document.documentElement.scrollLeft or document.documentElement.scrollTop)
|
||||||
|
y = document.documentElement.scrollTop
|
||||||
|
x = document.documentElement.scrollLeft
|
||||||
|
return [x, y]
|
||||||
|
# }}}
|
||||||
|
|
||||||
abstop = (elem) ->
|
viewport_to_document = (x, y, doc=window?.document) -> # {{{
|
||||||
ans = elem.offsetTop
|
until doc == window.document
|
||||||
while elem.offsetParent
|
# We are in a frame
|
||||||
elem = elem.offsetParent
|
frame = doc.defaultView.frameElement
|
||||||
ans += elem.offsetTop
|
rect = frame.getBoundingClientRect()
|
||||||
return ans
|
x += rect.left
|
||||||
|
y += rect.top
|
||||||
|
doc = frame.ownerDocument
|
||||||
|
win = doc.defaultView
|
||||||
|
[wx, wy] = window_scroll_pos(win)
|
||||||
|
x += wx
|
||||||
|
y += wy
|
||||||
|
return [x, y]
|
||||||
|
# }}}
|
||||||
|
|
||||||
class BookIndexing
|
class BookIndexing
|
||||||
###
|
###
|
||||||
@ -33,7 +47,7 @@ class BookIndexing
|
|||||||
|
|
||||||
constructor: () ->
|
constructor: () ->
|
||||||
this.cache = {}
|
this.cache = {}
|
||||||
this.body_height_at_last_check = null
|
this.last_check = [null, null]
|
||||||
|
|
||||||
cache_valid: (anchors) ->
|
cache_valid: (anchors) ->
|
||||||
for a in anchors
|
for a in anchors
|
||||||
@ -45,7 +59,9 @@ class BookIndexing
|
|||||||
return true
|
return true
|
||||||
|
|
||||||
anchor_positions: (anchors, use_cache=false) ->
|
anchor_positions: (anchors, use_cache=false) ->
|
||||||
if use_cache and body_height() == this.body_height_at_last_check and this.cache_valid(anchors)
|
body = document.body
|
||||||
|
doc_constant = body.scrollHeight == this.last_check[1] and body.scrollWidth == this.last_check[0]
|
||||||
|
if use_cache and doc_constant and this.cache_valid(anchors)
|
||||||
return this.cache
|
return this.cache
|
||||||
|
|
||||||
ans = {}
|
ans = {}
|
||||||
@ -56,19 +72,24 @@ class BookIndexing
|
|||||||
try
|
try
|
||||||
result = document.evaluate(
|
result = document.evaluate(
|
||||||
".//*[local-name() = 'a' and @name='#{ anchor }']",
|
".//*[local-name() = 'a' and @name='#{ anchor }']",
|
||||||
document.body, null,
|
body, null,
|
||||||
XPathResult.FIRST_ORDERED_NODE_TYPE, null)
|
XPathResult.FIRST_ORDERED_NODE_TYPE, null)
|
||||||
elem = result.singleNodeValue
|
elem = result.singleNodeValue
|
||||||
catch error
|
catch error
|
||||||
# The anchor had a ' or other invalid char
|
# The anchor had a ' or other invalid char
|
||||||
elem = null
|
elem = null
|
||||||
if elem == null
|
if elem == null
|
||||||
pos = body_height() + 10000
|
pos = [body.scrollWidth+1000, body.scrollHeight+1000]
|
||||||
else
|
else
|
||||||
pos = abstop(elem)
|
br = elem.getBoundingClientRect()
|
||||||
|
pos = viewport_to_document(br.left, br.top, elem.ownerDocument)
|
||||||
|
|
||||||
|
if window.paged_display?.in_paged_mode
|
||||||
|
pos[0] = window.paged_display.column_at(pos[0])
|
||||||
ans[anchor] = pos
|
ans[anchor] = pos
|
||||||
|
|
||||||
this.cache = ans
|
this.cache = ans
|
||||||
this.body_height_at_last_check = body_height()
|
this.last_check = [body.scrollWidth, body.scrollHeight]
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
if window?
|
if window?
|
||||||
|
@ -170,9 +170,7 @@ class PagedDisplay
|
|||||||
if this.is_full_screen_layout
|
if this.is_full_screen_layout
|
||||||
window.scrollTo(0, 0)
|
window.scrollTo(0, 0)
|
||||||
return
|
return
|
||||||
pos = 0
|
pos = Math.floor(xpos/this.page_width) * this.page_width
|
||||||
until (pos <= xpos < pos + this.page_width)
|
|
||||||
pos += this.page_width
|
|
||||||
limit = document.body.scrollWidth - this.screen_width
|
limit = document.body.scrollWidth - this.screen_width
|
||||||
pos = limit if pos > limit
|
pos = limit if pos > limit
|
||||||
if animated
|
if animated
|
||||||
@ -180,6 +178,16 @@ class PagedDisplay
|
|||||||
else
|
else
|
||||||
window.scrollTo(pos, 0)
|
window.scrollTo(pos, 0)
|
||||||
|
|
||||||
|
column_at: (xpos) ->
|
||||||
|
# Return the number of the column that contains xpos
|
||||||
|
return Math.floor(xpos/this.page_width)
|
||||||
|
|
||||||
|
column_boundaries: () ->
|
||||||
|
# Return the column numbers at the left edge and after the right edge
|
||||||
|
# of the viewport
|
||||||
|
l = this.column_at(window.pageXOffset + 10)
|
||||||
|
return [l, l + this.cols_per_screen]
|
||||||
|
|
||||||
animated_scroll: (pos, duration=1000, notify=true) ->
|
animated_scroll: (pos, duration=1000, notify=true) ->
|
||||||
# Scroll the window to X-position pos in an animated fashion over
|
# Scroll the window to X-position pos in an animated fashion over
|
||||||
# duration milliseconds. If notify is true, py_bridge.animated_scroll_done is
|
# duration milliseconds. If notify is true, py_bridge.animated_scroll_done is
|
||||||
@ -217,10 +225,7 @@ class PagedDisplay
|
|||||||
if this.is_full_screen_layout
|
if this.is_full_screen_layout
|
||||||
return 0
|
return 0
|
||||||
x = window.pageXOffset + Math.max(10, this.current_margin_side)
|
x = window.pageXOffset + Math.max(10, this.current_margin_side)
|
||||||
edge = Math.floor(x/this.page_width) * this.page_width
|
return Math.floor(x/this.page_width) * this.page_width
|
||||||
while edge < x
|
|
||||||
edge += this.page_width
|
|
||||||
return edge - this.page_width
|
|
||||||
|
|
||||||
next_screen_location: () ->
|
next_screen_location: () ->
|
||||||
# The position to scroll to for the next screen (which could contain
|
# The position to scroll to for the next screen (which could contain
|
||||||
@ -354,7 +359,6 @@ if window?
|
|||||||
window.paged_display = new PagedDisplay()
|
window.paged_display = new PagedDisplay()
|
||||||
|
|
||||||
# TODO:
|
# TODO:
|
||||||
# Indexing
|
|
||||||
# Resizing of images
|
# Resizing of images
|
||||||
# Full screen mode
|
# Full screen mode
|
||||||
# Highlight on jump_to_anchor
|
# Highlight on jump_to_anchor
|
||||||
|
@ -202,7 +202,7 @@ class Document(QWebPage): # {{{
|
|||||||
if not isinstance(self.anchor_positions, dict):
|
if not isinstance(self.anchor_positions, dict):
|
||||||
# Some weird javascript error happened
|
# Some weird javascript error happened
|
||||||
self.anchor_positions = {}
|
self.anchor_positions = {}
|
||||||
return self.anchor_positions
|
return {k:tuple(v) for k, v in self.anchor_positions.iteritems()}
|
||||||
|
|
||||||
def switch_to_paged_mode(self, onresize=False):
|
def switch_to_paged_mode(self, onresize=False):
|
||||||
if onresize and not self.loaded_javascript:
|
if onresize and not self.loaded_javascript:
|
||||||
@ -217,6 +217,13 @@ class Document(QWebPage): # {{{
|
|||||||
sz.setWidth(sz.width()+side_margin)
|
sz.setWidth(sz.width()+side_margin)
|
||||||
self.setPreferredContentsSize(sz)
|
self.setPreferredContentsSize(sz)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def column_boundaries(self):
|
||||||
|
if not self.loaded_javascript:
|
||||||
|
return (0, 1)
|
||||||
|
self.javascript(u'py_bridge.value = paged_display.column_boundaries()')
|
||||||
|
return tuple(self.bridge_value)
|
||||||
|
|
||||||
def after_resize(self):
|
def after_resize(self):
|
||||||
if self.in_paged_mode:
|
if self.in_paged_mode:
|
||||||
self.setPreferredContentsSize(QSize())
|
self.setPreferredContentsSize(QSize())
|
||||||
@ -558,6 +565,18 @@ class DocumentView(QWebView): # {{{
|
|||||||
return (self.document.ypos, self.document.ypos +
|
return (self.document.ypos, self.document.ypos +
|
||||||
self.document.window_height)
|
self.document.window_height)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def viewport_rect(self):
|
||||||
|
# (left, top, right, bottom) of the viewport in document co-ordinates
|
||||||
|
# When in paged mode, left and right are the numbers of the columns
|
||||||
|
# at the left edge and *after* the right edge of the viewport
|
||||||
|
d = self.document
|
||||||
|
if d.in_paged_mode:
|
||||||
|
l, r = d.column_boundaries
|
||||||
|
else:
|
||||||
|
l, r = d.xpos, d.xpos + d.window_width
|
||||||
|
return (l, d.ypos, r, d.ypos + d.window_height)
|
||||||
|
|
||||||
def link_hovered(self, link, text, context):
|
def link_hovered(self, link, text, context):
|
||||||
link, text = unicode(link), unicode(text)
|
link, text = unicode(link), unicode(text)
|
||||||
if link:
|
if link:
|
||||||
|
@ -683,7 +683,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
if hasattr(self, 'current_index'):
|
if hasattr(self, 'current_index'):
|
||||||
entry = self.toc_model.next_entry(self.current_index,
|
entry = self.toc_model.next_entry(self.current_index,
|
||||||
self.view.document.read_anchor_positions(),
|
self.view.document.read_anchor_positions(),
|
||||||
self.view.scroll_pos)
|
self.view.viewport_rect, self.view.document.in_paged_mode)
|
||||||
if entry is not None:
|
if entry is not None:
|
||||||
self.pending_goto_next_section = (
|
self.pending_goto_next_section = (
|
||||||
self.toc_model.currently_viewed_entry, entry, False)
|
self.toc_model.currently_viewed_entry, entry, False)
|
||||||
@ -693,7 +693,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
if hasattr(self, 'current_index'):
|
if hasattr(self, 'current_index'):
|
||||||
entry = self.toc_model.next_entry(self.current_index,
|
entry = self.toc_model.next_entry(self.current_index,
|
||||||
self.view.document.read_anchor_positions(),
|
self.view.document.read_anchor_positions(),
|
||||||
self.view.scroll_pos, backwards=True)
|
self.view.viewport_rect, self.view.document.in_paged_mode,
|
||||||
|
backwards=True)
|
||||||
if entry is not None:
|
if entry is not None:
|
||||||
self.pending_goto_next_section = (
|
self.pending_goto_next_section = (
|
||||||
self.toc_model.currently_viewed_entry, entry, True)
|
self.toc_model.currently_viewed_entry, entry, True)
|
||||||
@ -705,7 +706,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
if anchor_positions is None:
|
if anchor_positions is None:
|
||||||
anchor_positions = self.view.document.read_anchor_positions()
|
anchor_positions = self.view.document.read_anchor_positions()
|
||||||
items = self.toc_model.update_indexing_state(self.current_index,
|
items = self.toc_model.update_indexing_state(self.current_index,
|
||||||
self.view.scroll_pos, anchor_positions)
|
self.view.viewport_rect, anchor_positions,
|
||||||
|
self.view.document.in_paged_mode)
|
||||||
if items:
|
if items:
|
||||||
self.toc.scrollTo(items[-1].index())
|
self.toc.scrollTo(items[-1].index())
|
||||||
if pgns is not None:
|
if pgns is not None:
|
||||||
@ -714,7 +716,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
if pgns[0] is self.toc_model.currently_viewed_entry:
|
if pgns[0] is self.toc_model.currently_viewed_entry:
|
||||||
entry = self.toc_model.next_entry(self.current_index,
|
entry = self.toc_model.next_entry(self.current_index,
|
||||||
self.view.document.read_anchor_positions(),
|
self.view.document.read_anchor_positions(),
|
||||||
self.view.scroll_pos,
|
self.view.viewport_rect,
|
||||||
|
self.view.document.in_paged_mode,
|
||||||
backwards=pgns[2], current_entry=pgns[1])
|
backwards=pgns[2], current_entry=pgns[1])
|
||||||
if entry is not None:
|
if entry is not None:
|
||||||
self.pending_goto_next_section = (
|
self.pending_goto_next_section = (
|
||||||
|
@ -93,9 +93,19 @@ class TOCItem(QStandardItem):
|
|||||||
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):
|
def update_indexing_state(self, spine_index, viewport_rect, anchor_map,
|
||||||
|
in_paged_mode):
|
||||||
|
if in_paged_mode:
|
||||||
|
self.update_indexing_state_paged(spine_index, viewport_rect,
|
||||||
|
anchor_map)
|
||||||
|
else:
|
||||||
|
self.update_indexing_state_unpaged(spine_index, viewport_rect,
|
||||||
|
anchor_map)
|
||||||
|
|
||||||
|
def update_indexing_state_unpaged(self, spine_index, viewport_rect,
|
||||||
|
anchor_map):
|
||||||
is_being_viewed = False
|
is_being_viewed = False
|
||||||
top, bottom = scroll_pos
|
top, bottom = viewport_rect[1], viewport_rect[3]
|
||||||
# We use bottom-25 in the checks below to account for the case where
|
# We use bottom-25 in the checks below to account for the case where
|
||||||
# the next entry has some invisible margin that just overlaps with the
|
# the next entry has some invisible margin that just overlaps with the
|
||||||
# bottom of the screen. In this case it will appear to the user that
|
# bottom of the screen. In this case it will appear to the user that
|
||||||
@ -103,6 +113,9 @@ class TOCItem(QStandardItem):
|
|||||||
# be larger than 25, but that's a decent compromise. Also we dont want
|
# be larger than 25, but that's a decent compromise. Also we dont want
|
||||||
# to count a partial line as being visible.
|
# to count a partial line as being visible.
|
||||||
|
|
||||||
|
# We only care about y position
|
||||||
|
anchor_map = {k:v[1] for k, v in anchor_map.iteritems()}
|
||||||
|
|
||||||
if spine_index >= self.starts_at and spine_index <= self.ends_at:
|
if spine_index >= self.starts_at and spine_index <= self.ends_at:
|
||||||
# The position at which this anchor is present in the document
|
# The position at which this anchor is present in the document
|
||||||
start_pos = anchor_map.get(self.start_anchor, 0)
|
start_pos = anchor_map.get(self.start_anchor, 0)
|
||||||
@ -115,7 +128,7 @@ class TOCItem(QStandardItem):
|
|||||||
# ancestors of this entry.
|
# ancestors of this entry.
|
||||||
psp = [anchor_map.get(x, 0) for x in self.possible_end_anchors]
|
psp = [anchor_map.get(x, 0) for x in self.possible_end_anchors]
|
||||||
psp = [x for x in psp if x >= start_pos]
|
psp = [x for x in psp if x >= start_pos]
|
||||||
# The end position. The first anchor whose pos is >= self.start_pos
|
# The end position. The first anchor whose pos is >= start_pos
|
||||||
# or if the end is not in this spine item, we set it to the bottom
|
# or if the end is not in this spine item, we set it to the bottom
|
||||||
# of the window +1
|
# of the window +1
|
||||||
end_pos = min(psp) if psp else (bottom+1 if self.ends_at >=
|
end_pos = min(psp) if psp else (bottom+1 if self.ends_at >=
|
||||||
@ -141,6 +154,51 @@ class TOCItem(QStandardItem):
|
|||||||
if changed:
|
if changed:
|
||||||
self.setFont(self.bold_font if is_being_viewed else self.normal_font)
|
self.setFont(self.bold_font if is_being_viewed else self.normal_font)
|
||||||
|
|
||||||
|
def update_indexing_state_paged(self, spine_index, viewport_rect,
|
||||||
|
anchor_map):
|
||||||
|
is_being_viewed = False
|
||||||
|
|
||||||
|
left, right = viewport_rect[0], viewport_rect[2]
|
||||||
|
left, right = (left, 0), (right, -1)
|
||||||
|
|
||||||
|
if spine_index >= self.starts_at and spine_index <= self.ends_at:
|
||||||
|
# The position at which this anchor is present in the document
|
||||||
|
start_pos = anchor_map.get(self.start_anchor, (0, 0))
|
||||||
|
psp = []
|
||||||
|
if self.ends_at == spine_index:
|
||||||
|
# Anchors that could possibly indicate the start of the next
|
||||||
|
# section and therefore the end of this section.
|
||||||
|
# self.possible_end_anchors is a set of anchors belonging to
|
||||||
|
# toc entries with depth <= self.depth that are also not
|
||||||
|
# ancestors of this entry.
|
||||||
|
psp = [anchor_map.get(x, (0, 0)) for x in self.possible_end_anchors]
|
||||||
|
psp = [x for x in psp if x >= start_pos]
|
||||||
|
# The end position. The first anchor whose pos is >= start_pos
|
||||||
|
# or if the end is not in this spine item, we set it to the column
|
||||||
|
# after the right edge of the viewport
|
||||||
|
end_pos = min(psp) if psp else (right if self.ends_at >=
|
||||||
|
spine_index else (0, 0))
|
||||||
|
if spine_index > self.starts_at and spine_index < self.ends_at:
|
||||||
|
# The entire spine item is contained in this entry
|
||||||
|
is_being_viewed = True
|
||||||
|
elif (spine_index == self.starts_at and right > start_pos and
|
||||||
|
# This spine item contains the start
|
||||||
|
# The start position is before the end of the viewport
|
||||||
|
(spine_index != self.ends_at or left < end_pos)):
|
||||||
|
# The end position is after the start of the viewport
|
||||||
|
is_being_viewed = True
|
||||||
|
elif (spine_index == self.ends_at and left < end_pos and
|
||||||
|
# This spine item contains the end
|
||||||
|
# The end position is after the start of the viewport
|
||||||
|
(spine_index != self.starts_at or right > start_pos)):
|
||||||
|
# The start position is before the end of the viewport
|
||||||
|
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)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'TOC Item: %s %s#%s'%(self.title, self.abspath, self.fragment)
|
return 'TOC Item: %s %s#%s'%(self.title, self.abspath, self.fragment)
|
||||||
|
|
||||||
@ -183,20 +241,26 @@ class TOC(QStandardItemModel):
|
|||||||
self.currently_viewed_entry = t
|
self.currently_viewed_entry = t
|
||||||
return items_being_viewed
|
return items_being_viewed
|
||||||
|
|
||||||
def next_entry(self, spine_pos, anchor_map, scroll_pos, backwards=False,
|
def next_entry(self, spine_pos, anchor_map, viewport_rect, in_paged_mode,
|
||||||
current_entry=None):
|
backwards=False, current_entry=None):
|
||||||
current_entry = (self.currently_viewed_entry if current_entry is None
|
current_entry = (self.currently_viewed_entry if current_entry is None
|
||||||
else current_entry)
|
else current_entry)
|
||||||
if current_entry is None: return
|
if current_entry is None: return
|
||||||
items = reversed(self.all_items) if backwards else self.all_items
|
items = reversed(self.all_items) if backwards else self.all_items
|
||||||
found = False
|
found = False
|
||||||
top = scroll_pos[0]
|
|
||||||
|
if in_paged_mode:
|
||||||
|
start = viewport_rect[0]
|
||||||
|
anchor_map = {k:v[0] for k, v in anchor_map.iteritems()}
|
||||||
|
else:
|
||||||
|
start = viewport_rect[1]
|
||||||
|
anchor_map = {k:v[1] for k, v in anchor_map.iteritems()}
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
if found:
|
if found:
|
||||||
start_pos = anchor_map.get(item.start_anchor, 0)
|
start_pos = anchor_map.get(item.start_anchor, 0)
|
||||||
if backwards and item.is_being_viewed and start_pos >= top:
|
if backwards and item.is_being_viewed and start_pos >= start:
|
||||||
# Going to this item will either not move the scroll
|
# This item will not cause any scrolling
|
||||||
# position or cause to to *increase* instead of descresing
|
|
||||||
continue
|
continue
|
||||||
if item.starts_at != spine_pos or item.start_anchor:
|
if item.starts_at != spine_pos or item.start_anchor:
|
||||||
return item
|
return item
|
||||||
|
Loading…
x
Reference in New Issue
Block a user