diff --git a/src/pyj/read_book/create_annotation.pyj b/src/pyj/read_book/create_annotation.pyj index daaff5f288..94bbd076ea 100644 --- a/src/pyj/read_book/create_annotation.pyj +++ b/src/pyj/read_book/create_annotation.pyj @@ -177,6 +177,7 @@ def map_to_iframe_coords(point): BAR_SIZE = 32 +DRAG_SCROLL_ZONE_MIN_HEIGHT = 10 def create_bar(): @@ -194,6 +195,8 @@ class CreateAnnotation: def __init__(self, view): self.view = view self.active_touch = None + self.drag_scroll_timer = None + self.last_drag_scroll_at = -100000 self.editing_annot_uuid = None self.current_notes = '' self.annotations_manager = self.view.annotations_manager @@ -227,7 +230,7 @@ class CreateAnnotation: tb = create_bar() container.appendChild(tb) button(tb, 'close', _('Cancel creation of highlight'), self.hide) - button(tb, 'chevron-up', _('Scroll up'), self.scroll_up) + button(tb, 'chevron-up', _('Scroll up'), self.button_scroll.bind(None, True)) tb.appendChild(E.span(style=f'height: {tb.style.height}')) button(tb.lastChild, 'trash', _('Remove this highlight'), self.delete_highlight) tb.lastChild.appendChild(E.span('\xa0\xa0\xa0')) @@ -243,7 +246,7 @@ class CreateAnnotation: bb = create_bar() container.appendChild(bb) button(bb, 'fg', _('Change highlight color'), self.choose_color) - button(bb, 'chevron-down', _('Scroll down'), self.scroll_down) + button(bb, 'chevron-down', _('Scroll down'), self.button_scroll) button(bb, 'pencil', _('Add a note'), self.add_notes) sd = get_session_data() @@ -272,12 +275,6 @@ class CreateAnnotation: def copy_to_clipboard(self): self.view.iframe_wrapper.send_message('copy_selection') - def scroll_up(self): - self.send_message('scroll', backwards=True) - - def scroll_down(self): - self.send_message('scroll', backwards=False) - @property def middle(self): return document.getElementById(self.middle_id) @@ -455,12 +452,16 @@ class CreateAnnotation: if sc_name is 'show_chrome': self.hide() elif sc_name in ('up', 'down', 'pageup', 'pagedown'): - self.send_message('scroll', backwards=bool('up' in sc_name)) + backwards = 'up' in sc_name + if 'page' in sc_name or not self.in_flow_mode: + self.paged_scroll(backwards) + else: + self.send_drag_scroll_message(backwards, 'left' if backwards else 'right', False) elif sc_name in ('left', 'right'): if self.in_flow_mode: self.send_message('perp-scroll', backwards=bool(sc_name is 'left')) else: - self.send_message('scroll', backwards=bool(sc_name is 'left')) + self.paged_scroll(sc_name is 'left') def container_clicked(self, ev): ev.stopPropagation(), ev.preventDefault() @@ -495,9 +496,46 @@ class CreateAnnotation: self.start_handle_drag(touch, ev.currentTarget.id) break + def button_scroll(self, backwards): + if self.in_flow_mode: + self.send_drag_scroll_message(backwards, 'left' if backwards else 'right', False) + else: + self.paged_scroll(backwards) + + def paged_scroll(self, backwards): + self.send_message('paged-scroll', backwards=backwards) + + def run_drag_scroll(self, mouse_y, top, bottom): + backwards = mouse_y <= top + self.do_one_drag_scroll(backwards, top - mouse_y if backwards else mouse_y - bottom) + + def do_one_drag_scroll(self, backwards, distance_from_boundary): + window.clearTimeout(self.drag_scroll_timer) + self.drag_scroll_timer = None + if self.state not in (DRAGGING_RIGHT, DRAGGING_LEFT): + return + interval = 100 if self.in_flow_mode else 1200 + self.drag_scroll_timer = window.setTimeout(self.do_one_drag_scroll.bind(None, backwards, distance_from_boundary), interval) + now = window.performance.now() + if now - self.last_drag_scroll_at > interval: + self.send_drag_scroll_message(backwards, 'left' if self.state is DRAGGING_LEFT else 'right', True) + self.last_drag_scroll_at = now + + def send_drag_scroll_message(self, backwards, handle, extend_selection): + self.send_message( + 'drag-scroll', backwards=backwards, handle=handle, extents=self.current_handle_position, + extend_selection=extend_selection) + + def end_drag_scroll(self): + if self.drag_scroll_timer is not None: + window.clearTimeout(self.drag_scroll_timer) + self.drag_scroll_timer = None + self.last_drag_scroll_at = -10000 + def mouseup_on_container(self, ev): if self.state in (DRAGGING_RIGHT, DRAGGING_LEFT): self.state = WAITING_FOR_DRAG + self.end_drag_scroll() ev.preventDefault(), ev.stopPropagation() def touchend_on_container(self, ev): @@ -507,6 +545,7 @@ class CreateAnnotation: if touch.identifier is self.active_touch: self.active_touch = None self.state = WAITING_FOR_DRAG + self.end_drag_scroll() return def handle_moved(self, ev): @@ -518,6 +557,16 @@ class CreateAnnotation: pos.start = map_to_iframe_coords(pos.start) pos.end = map_to_iframe_coords(pos.end) self.send_message('set-selection', extents=pos) + c = self.container + rect = c.getBoundingClientRect() + t = document.getElementById('book-top-margin').offsetHeight + top = rect.top + max(t, DRAG_SCROLL_ZONE_MIN_HEIGHT) + t = document.getElementById('book-bottom-margin').offsetHeight + bottom = rect.bottom - max(t, DRAG_SCROLL_ZONE_MIN_HEIGHT) + if ev.clientY < top or ev.clientY > bottom: + self.run_drag_scroll(ev.clientY, top, bottom) + else: + self.end_drag_scroll() def mousemove_on_container(self, ev): if self.state not in (DRAGGING_RIGHT, DRAGGING_LEFT): @@ -626,17 +675,10 @@ class CreateAnnotation: self.editing_annot_uuid = msg.existing or None if self.editing_annot_uuid: self.current_notes = self.annotations_manager.notes_for_highlight(self.editing_annot_uuid) or '' + elif msg.type is 'scrolled': + self.place_handles_after_scroll(msg.extents, msg.handle, msg.extended) elif msg.type is 'update-handles': self.place_handles(msg.extents) - if msg.from_scroll and not msg.selection_extended: - middle = map_from_iframe_coords({ - 'x': msg.page_rect.left + msg.page_rect.width // 2, - 'y': msg.page_rect.top + msg.page_rect.height // 2 - }) - handle = self.left_handle if msg.backwards else self.right_handle - handle.style.display = 'block' - handle.style.left = f'{middle.x}px' - handle.style.top = f'{middle.y}px' elif msg.type is 'highlight-applied': if not msg.ok: return error_dialog( @@ -653,30 +695,42 @@ class CreateAnnotation: self.left_handle.style.display = 'none' self.right_handle.style.display = 'none' + def place_single_handle(self, handle, data): + map_from_iframe_coords(data) + s = handle.style + s.display = 'block' if data.onscreen else 'none' + height = data.height * 3 + width = data.height * 2 + s.width = f'{width}px' + s.height = f'{height}px' + bottom = data.y + data.height + top = bottom - height + s.top = f'{top}px' + if handle.id is self.left_handle_id: + s.left = (data.x - width) + 'px' + else: + s.left = data.x + 'px' + def place_handles(self, extents): - lh, rh = self.left_handle, self.right_handle - - def do_it(handle, data): - map_from_iframe_coords(data) - s = handle.style - s.display = 'block' if data.onscreen else 'none' - height = data.height * 3 - width = data.height * 2 - s.width = f'{width}px' - s.height = f'{height}px' - bottom = data.y + data.height - top = bottom - height - s.top = f'{top}px' - return s, width - - style, width = do_it(lh, extents.start) - style.left = (extents.start.x - width) + 'px' - style, width = do_it(rh, extents.end) - style.left = extents.end.x + 'px' + self.place_single_handle(self.left_handle, extents.start) + self.place_single_handle(self.right_handle, extents.end) self.state = WAITING_FOR_DRAG self.left_line_height = extents.start.height self.right_line_height = extents.end.height + def place_handles_after_scroll(self, extents, handle, extended): + if extended: + if handle is 'right': + h = self.left_handle + data = extents.start + else: + h = self.right_handle + data = extents.end + self.place_single_handle(h, data) + else: + self.place_single_handle(self.left_handle, extents.start) + self.place_single_handle(self.right_handle, extents.end) + class ViewAnnotation: diff --git a/src/pyj/read_book/flow_mode.pyj b/src/pyj/read_book/flow_mode.pyj index 05fa8402c9..3dbdda5cb9 100644 --- a/src/pyj/read_book/flow_mode.pyj +++ b/src/pyj/read_book/flow_mode.pyj @@ -123,13 +123,15 @@ def scroll_by_page(direction): h = scroll_viewport.height() - 10 window.scrollBy(0, h * direction) -def scroll_to_extend_annotation(backward, horizontal): +def scroll_to_extend_annotation(backward, horizontal, by_page): direction = -1 if backward else 1 + h = line_height() + if by_page: + h = (window.innerWidth if horizontal else window.innerHeight) - h if horizontal: before = window.pageXOffset - window.scrollBy(15 * direction, 0) + window.scrollBy(h * direction, 0) return window.pageXOffset is not before - h = scroll_viewport.height() - 10 before = window.pageYOffset window.scrollBy(0, h * direction) return window.pageYOffset is not before diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index 55ee6c1254..215f5108b3 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -5,7 +5,7 @@ from __python__ import bound_methods, hash_literals import traceback from gettext import gettext as _ from select import ( - extend_selection_after_scroll, selection_extents, selection_extents_at_point, + extend_selection_to_limit, selection_extents, selection_extents_at_point, set_selections_extents_to, word_at_point ) @@ -38,11 +38,11 @@ from read_book.mathjax import apply_mathjax from read_book.paged_mode import ( anchor_funcs as paged_anchor_funcs, auto_scroll_action as paged_auto_scroll_action, calc_columns_per_screen, - current_cfi, current_page_width, get_columns_per_screen_data, - handle_gesture as paged_handle_gesture, handle_shortcut as paged_handle_shortcut, - jump_to_cfi as paged_jump_to_cfi, layout as paged_layout, - onwheel as paged_onwheel, prepare_for_resize as paged_prepare_for_resize, - progress_frac, reset_paged_mode_globals, resize_done as paged_resize_done, + current_cfi, get_columns_per_screen_data, handle_gesture as paged_handle_gesture, + handle_shortcut as paged_handle_shortcut, jump_to_cfi as paged_jump_to_cfi, + layout as paged_layout, onwheel as paged_onwheel, + prepare_for_resize as paged_prepare_for_resize, progress_frac, + reset_paged_mode_globals, resize_done as paged_resize_done, scroll_by_page as paged_scroll_by_page, scroll_to_elem, scroll_to_extend_annotation as paged_annotation_scroll, scroll_to_fraction as paged_scroll_to_fraction, snap_to_selection, @@ -707,13 +707,21 @@ class IframeBoss: extents = selection_extents(in_flow_mode) self.send_message( 'annotations', type='position-handles', extents=extents, existing=annot_id_uuid_map[annot_id]) - elif data.type is 'scroll': + elif data.type is 'drag-scroll': if self.scroll_to_extend_annotation(data.backwards): - page_rect = {'width': current_page_width()} - extended = extend_selection_after_scroll(data.backwards, in_flow_mode, page_rect) + if data.extend_selection: + extend_selection_to_limit(data.handle is 'left', data.backwards) + extents = selection_extents(in_flow_mode) self.send_message( - 'annotations', type='update-handles', extents=selection_extents(in_flow_mode), - backwards=data.backwards, from_scroll=True, selection_extended=extended, page_rect=page_rect + 'annotations', type='scrolled', + backwards=data.backwards, handle=data.handle, extents=extents, extended=data.extend_selection + ) + elif data.type is 'paged-scroll': + if self.scroll_to_extend_annotation(data.backwards, False, True): + extents = selection_extents(in_flow_mode) + self.send_message( + 'annotations', type='scrolled', backwards=data.backwards, handle=None, + extents=extents, extended=False ) elif data.type is 'perp-scroll': if in_flow_mode and flow_annotation_scroll(data.backwards, True): diff --git a/src/pyj/select.pyj b/src/pyj/select.pyj index 448a4047ee..edd02177e5 100644 --- a/src/pyj/select.pyj +++ b/src/pyj/select.pyj @@ -98,53 +98,32 @@ def selection_extents_at_point(x, y, in_flow_mode): return ans -def extend_selection_after_scroll(backwards, in_flow_mode, page_rect): - page_rect.top = page_rect.left = 0 - page_rect.bottom = window.innerHeight - page_rect.right = page_rect.width - if not in_flow_mode and not backwards: - page_rect.right = window.innerWidth - page_rect.left = window.innerWidth - page_rect.width - page_rect.width = page_rect.right - page_rect.left - page_rect.height = page_rect.bottom - page_rect.top +def range_at_limit(invert_x, invert_y): + step = 10 + for y in range(0, window.innerHeight + step, step): + if invert_y: + y = max(0, window.innerHeight - y) + for x in range(0, window.innerWidth + step, step): + if invert_x: + x = max(0, window.innerWidth - x) + r = range_from_point(x, y) + if r: + return r + +def extend_selection_to_limit(left, top): sel = window.getSelection() - if not sel.rangeCount: + if not sel or not sel.rangeCount or sel.isCollapsed: + return False + new_limit = range_at_limit(not left, not top) + if not new_limit: return False - if in_flow_mode: - page_rect.width = window.innerWidth r = sel.getRangeAt(0) - q = r.cloneRange() - q.collapse(backwards) - rects = q.getClientRects() - if not rects.length: - return False - rect = rects[0] - - in_page_already = rect.left >= page_rect.left and rect.top >= page_rect.top and rect.bottom <= page_rect.bottom and rect.right <= page_rect.right - if in_page_already: - return True - dx = page_rect.width // 10 - dy = page_rect.height // 10 - middle_x = page_rect.left + page_rect.width // 2 - for yi in range(1, 10): - if backwards: - y = page_rect.bottom - dy * yi - else: - y = page_rect.top + dy * yi - for xi in range(4): - xvals = v'[-1, 1]' if xi else v'[1]' - for xw in xvals: - x = middle_x + xw * dx - p = range_from_point(x, y) - if p: - if backwards: - r.setStart(p.startContainer, p.startOffset) - else: - r.setEnd(p.startContainer, p.startOffset) - return True - # could not find any content on page - return False + if left: + r.setStart(new_limit.startContainer, new_limit.startOffset) + else: + r.setEnd(new_limit.startContainer, new_limit.startOffset) + return True def set_selections_extents_to(extents):