From 93a370ef8cbd049566ad2c873111c2b4b30df690 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 26 Jun 2012 16:25:08 +0530 Subject: [PATCH] Paged display: Indexing now (mostly) works --- .../ebooks/oeb/display/indexing.coffee | 59 ++++++++----- src/calibre/ebooks/oeb/display/paged.coffee | 20 +++-- src/calibre/gui2/viewer/documentview.py | 21 ++++- src/calibre/gui2/viewer/main.py | 11 ++- src/calibre/gui2/viewer/toc.py | 82 +++++++++++++++++-- 5 files changed, 152 insertions(+), 41 deletions(-) diff --git a/src/calibre/ebooks/oeb/display/indexing.coffee b/src/calibre/ebooks/oeb/display/indexing.coffee index 11f73c1504..48f0697506 100644 --- a/src/calibre/ebooks/oeb/display/indexing.coffee +++ b/src/calibre/ebooks/oeb/display/indexing.coffee @@ -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? diff --git a/src/calibre/ebooks/oeb/display/paged.coffee b/src/calibre/ebooks/oeb/display/paged.coffee index 17049fb99a..ff750ff3ba 100644 --- a/src/calibre/ebooks/oeb/display/paged.coffee +++ b/src/calibre/ebooks/oeb/display/paged.coffee @@ -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 diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index dc7b557f3c..a6a8616307 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -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: diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index 8200169025..08ab731e51 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -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 = ( diff --git a/src/calibre/gui2/viewer/toc.py b/src/calibre/gui2/viewer/toc.py index ae03b3ed26..b0e97bea65 100644 --- a/src/calibre/gui2/viewer/toc.py +++ b/src/calibre/gui2/viewer/toc.py @@ -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