Paged display: Indexing now (mostly) works

This commit is contained in:
Kovid Goyal 2012-06-26 16:25:08 +05:30
parent 8cab25887d
commit 93a370ef8c
5 changed files with 152 additions and 41 deletions

View File

@ -6,20 +6,34 @@
Released under the GPLv3 License
###
body_height = () ->
db = document.body
dde = document.documentElement
if db? and dde?
return Math.max(db.scrollHeight, dde.scrollHeight, db.offsetHeight,
dde.offsetHeight, db.clientHeight, dde.clientHeight)
return 0
window_scroll_pos = (win=window) -> # {{{
if typeof(win.pageXOffset) == 'number'
x = win.pageXOffset
y = win.pageYOffset
else # IE < 9
if document.body and ( document.body.scrollLeft or document.body.scrollTop )
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) ->
ans = elem.offsetTop
while elem.offsetParent
elem = elem.offsetParent
ans += elem.offsetTop
return ans
viewport_to_document = (x, y, doc=window?.document) -> # {{{
until doc == window.document
# We are in a frame
frame = doc.defaultView.frameElement
rect = frame.getBoundingClientRect()
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
###
@ -33,7 +47,7 @@ class BookIndexing
constructor: () ->
this.cache = {}
this.body_height_at_last_check = null
this.last_check = [null, null]
cache_valid: (anchors) ->
for a in anchors
@ -45,7 +59,9 @@ class BookIndexing
return true
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
ans = {}
@ -56,19 +72,24 @@ class BookIndexing
try
result = document.evaluate(
".//*[local-name() = 'a' and @name='#{ anchor }']",
document.body, null,
body, null,
XPathResult.FIRST_ORDERED_NODE_TYPE, null)
elem = result.singleNodeValue
catch error
# The anchor had a ' or other invalid char
elem = null
if elem == null
pos = body_height() + 10000
pos = [body.scrollWidth+1000, body.scrollHeight+1000]
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
this.cache = ans
this.body_height_at_last_check = body_height()
this.last_check = [body.scrollWidth, body.scrollHeight]
return ans
if window?

View File

@ -170,9 +170,7 @@ class PagedDisplay
if this.is_full_screen_layout
window.scrollTo(0, 0)
return
pos = 0
until (pos <= xpos < pos + this.page_width)
pos += this.page_width
pos = Math.floor(xpos/this.page_width) * this.page_width
limit = document.body.scrollWidth - this.screen_width
pos = limit if pos > limit
if animated
@ -180,6 +178,16 @@ class PagedDisplay
else
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) ->
# Scroll the window to X-position pos in an animated fashion over
# duration milliseconds. If notify is true, py_bridge.animated_scroll_done is
@ -217,10 +225,7 @@ class PagedDisplay
if this.is_full_screen_layout
return 0
x = window.pageXOffset + Math.max(10, this.current_margin_side)
edge = Math.floor(x/this.page_width) * this.page_width
while edge < x
edge += this.page_width
return edge - this.page_width
return Math.floor(x/this.page_width) * this.page_width
next_screen_location: () ->
# The position to scroll to for the next screen (which could contain
@ -354,7 +359,6 @@ if window?
window.paged_display = new PagedDisplay()
# TODO:
# Indexing
# Resizing of images
# Full screen mode
# Highlight on jump_to_anchor

View File

@ -202,7 +202,7 @@ class Document(QWebPage): # {{{
if not isinstance(self.anchor_positions, dict):
# Some weird javascript error happened
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):
if onresize and not self.loaded_javascript:
@ -217,6 +217,13 @@ class Document(QWebPage): # {{{
sz.setWidth(sz.width()+side_margin)
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):
if self.in_paged_mode:
self.setPreferredContentsSize(QSize())
@ -558,6 +565,18 @@ class DocumentView(QWebView): # {{{
return (self.document.ypos, self.document.ypos +
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):
link, text = unicode(link), unicode(text)
if link:

View File

@ -683,7 +683,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
if hasattr(self, 'current_index'):
entry = self.toc_model.next_entry(self.current_index,
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:
self.pending_goto_next_section = (
self.toc_model.currently_viewed_entry, entry, False)
@ -693,7 +693,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
if hasattr(self, 'current_index'):
entry = self.toc_model.next_entry(self.current_index,
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:
self.pending_goto_next_section = (
self.toc_model.currently_viewed_entry, entry, True)
@ -705,7 +706,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
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.scroll_pos, anchor_positions)
self.view.viewport_rect, anchor_positions,
self.view.document.in_paged_mode)
if items:
self.toc.scrollTo(items[-1].index())
if pgns is not None:
@ -714,7 +716,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
if pgns[0] is self.toc_model.currently_viewed_entry:
entry = self.toc_model.next_entry(self.current_index,
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])
if entry is not None:
self.pending_goto_next_section = (

View File

@ -93,9 +93,19 @@ class TOCItem(QStandardItem):
def type(cls):
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
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
# 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
@ -103,6 +113,9 @@ class TOCItem(QStandardItem):
# be larger than 25, but that's a decent compromise. Also we dont want
# 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:
# The position at which this anchor is present in the document
start_pos = anchor_map.get(self.start_anchor, 0)
@ -115,7 +128,7 @@ class TOCItem(QStandardItem):
# ancestors of this entry.
psp = [anchor_map.get(x, 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 >= 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
# of the window +1
end_pos = min(psp) if psp else (bottom+1 if self.ends_at >=
@ -141,6 +154,51 @@ class TOCItem(QStandardItem):
if changed:
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):
return 'TOC Item: %s %s#%s'%(self.title, self.abspath, self.fragment)
@ -183,20 +241,26 @@ class TOC(QStandardItemModel):
self.currently_viewed_entry = t
return items_being_viewed
def next_entry(self, spine_pos, anchor_map, scroll_pos, backwards=False,
current_entry=None):
def next_entry(self, spine_pos, anchor_map, viewport_rect, in_paged_mode,
backwards=False, current_entry=None):
current_entry = (self.currently_viewed_entry if current_entry is None
else current_entry)
if current_entry is None: return
items = reversed(self.all_items) if backwards else self.all_items
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:
if found:
start_pos = anchor_map.get(item.start_anchor, 0)
if backwards and item.is_being_viewed and start_pos >= top:
# Going to this item will either not move the scroll
# position or cause to to *increase* instead of descresing
if backwards and item.is_being_viewed and start_pos >= start:
# This item will not cause any scrolling
continue
if item.starts_at != spine_pos or item.start_anchor:
return item