From 0ce941104dd419c72c138cdc973ae66237d76cfd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 4 Jun 2020 07:32:29 +0530 Subject: [PATCH] Viewer: In flow mode, implement drag scrolling. Fixes #1880707 [there is no auto scroll when to try text select](https://bugs.launchpad.net/calibre/+bug/1880707) --- src/pyj/read_book/flow_mode.pyj | 69 +++++++++++++++++++++++++++++++++ src/pyj/read_book/iframe.pyj | 29 ++++++++++++-- src/pyj/read_book/settings.pyj | 2 + src/pyj/read_book/view.pyj | 2 + 4 files changed, 98 insertions(+), 4 deletions(-) diff --git a/src/pyj/read_book/flow_mode.pyj b/src/pyj/read_book/flow_mode.pyj index c12e2b86e4..c625b910d7 100644 --- a/src/pyj/read_book/flow_mode.pyj +++ b/src/pyj/read_book/flow_mode.pyj @@ -254,6 +254,7 @@ class ScrollAnimator: return self.is_running() and (self.auto or self.auto_timer is not None) def start(self, direction, auto): + cancel_drag_scroll() if self.wait: return @@ -274,6 +275,9 @@ class ScrollAnimator: def smooth_scroll(self, ts): duration = self.end_time - self.start_time + if duration <= 0: + self.animation_id = None + return progress = max(0, min(1, (ts - self.start_time) / duration)) # max/min to account for jitter scroll_target = self.start_offset scroll_target += Math.trunc(self.direction * progress * duration * line_height() * opts.lines_per_sec_smooth) / 1000 @@ -374,6 +378,7 @@ class FlickAnimator: self.animation_id = None def start(self, gesture): + cancel_drag_scroll() self.vertical = gesture.axis is 'vertical' now = window.performance.now() points = times = None @@ -411,6 +416,70 @@ class FlickAnimator: flick_animator = FlickAnimator() + +class DragScroller: + + DURATION = 100 # milliseconds + + def __init__(self): + self.animation_id = None + self.direction = 1 + self.speed_factor = 1 + self.start_time = self.end_time = 0 + self.start_offset = 0 + + def is_running(self): + return self.animation_id is not None + + def smooth_scroll(self, ts): + duration = self.end_time - self.start_time + if duration <= 0: + self.animation_id = None + self.start(self.direction, self.speed_factor) + return + progress = max(0, min(1, (ts - self.start_time) / duration)) # max/min to account for jitter + scroll_target = self.start_offset + scroll_target += Math.trunc(self.direction * progress * duration * line_height() * opts.lines_per_sec_smooth * self.speed_factor) / 1000 + window.scrollTo(0, scroll_target) + + if progress < 1: + self.animation_id = window.requestAnimationFrame(self.smooth_scroll) + else: + self.animation_id = None + self.start(self.direction, self.speed_factor) + + def start(self, direction, speed_factor): + now = window.performance.now() + self.end_time = now + self.DURATION + if not self.is_running() or direction is not self.direction or speed_factor is not self.speed_factor: + self.stop() + self.direction = direction + self.speed_factor = speed_factor + self.start_time = now + self.start_offset = window.pageYOffset + self.animation_id = window.requestAnimationFrame(self.smooth_scroll) + + def stop(self): + if self.animation_id is not None: + window.cancelAnimationFrame(self.animation_id) + self.animation_id = None + + +drag_scroller = DragScroller() + + +def cancel_drag_scroll(): + drag_scroller.stop() + + +def start_drag_scroll(delta): + limit = opts.margin_top if delta < 0 else opts.margin_bottom + direction = 1 if delta >= 0 else -1 + speed_factor = min(abs(delta), limit) / limit + speed_factor *= (2 - speed_factor) # QuadOut Easing curve + drag_scroller.start(direction, 2 * speed_factor) + + def handle_gesture(gesture): flick_animator.stop() if gesture.type is 'swipe': diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index c385e87431..0ccf0de307 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -22,10 +22,11 @@ from read_book.extract import get_elements from read_book.find import reset_find_caches, select_search_result from read_book.flow_mode import ( anchor_funcs as flow_anchor_funcs, auto_scroll_action as flow_auto_scroll_action, - ensure_selection_visible, flow_onwheel, flow_to_scroll_fraction, - handle_gesture as flow_handle_gesture, handle_shortcut as flow_handle_shortcut, - layout as flow_layout, scroll_by_page as flow_scroll_by_page, - scroll_to_extend_annotation as flow_annotation_scroll + cancel_drag_scroll, ensure_selection_visible, flow_onwheel, + flow_to_scroll_fraction, handle_gesture as flow_handle_gesture, + handle_shortcut as flow_handle_shortcut, layout as flow_layout, + scroll_by_page as flow_scroll_by_page, + scroll_to_extend_annotation as flow_annotation_scroll, start_drag_scroll ) from read_book.footnotes import is_footnote_link from read_book.globals import ( @@ -143,6 +144,7 @@ class IframeBoss: self.length_before = None def on_overlay_visibility_changed(self, data): + cancel_drag_scroll() if data.visible: self.forward_keypresses = True if self.auto_scroll_action: @@ -166,6 +168,8 @@ class IframeBoss: window.addEventListener('resize', debounce(self.onresize, 500)) window.addEventListener('wheel', self.onwheel, {'passive': False}) window.addEventListener('keydown', self.onkeydown, {'passive': False}) + window.addEventListener('mousemove', self.onmousemove, {'passive': True}) + window.addEventListener('mouseup', self.onmouseup, {'passive': True}) document.documentElement.addEventListener('contextmenu', self.oncontextmenu, {'passive': False}) document.addEventListener('selectionchange', self.onselectionchange) self.color_scheme = data.color_scheme @@ -201,6 +205,7 @@ class IframeBoss: (console.error or console.log)('There was an error in the JavaScript from within the book') def display(self, data): + cancel_drag_scroll() self.length_before = None self.content_ready = False clear_annot_id_uuid_map() @@ -535,6 +540,22 @@ class IframeBoss: else: self.handle_wheel(evt) + def onmousemove(self, evt): + if evt.buttons is not 1: + return + if 0 <= evt.clientY <= window.innerHeight or current_layout_mode() is not 'flow': + cancel_drag_scroll() + return + sel = window.getSelection() + if not sel: + cancel_drag_scroll() + return + delta = evt.clientY if evt.clientY < 0 else (evt.clientY - window.innerHeight) + start_drag_scroll(delta) + + def onmouseup(self, evt): + cancel_drag_scroll() + def onkeydown(self, evt): if current_layout_mode() is not 'flow' and evt.key is 'Tab': # Prevent the TAB key from shifting focus as it causes partial scrolling diff --git a/src/pyj/read_book/settings.pyj b/src/pyj/read_book/settings.pyj index 35526b39f3..60d1ca7cba 100644 --- a/src/pyj/read_book/settings.pyj +++ b/src/pyj/read_book/settings.pyj @@ -22,6 +22,8 @@ def update_settings(settings): opts.lines_per_sec_smooth = settings.lines_per_sec_smooth opts.margin_left = max(0, settings.margin_left) opts.margin_right = max(0, settings.margin_right) + opts.margin_top = max(0, settings.margin_top) + opts.margin_bottom = max(0, settings.margin_bottom) opts.override_book_colors = settings.override_book_colors opts.paged_wheel_scrolls_by_screen = v'!!settings.paged_wheel_scrolls_by_screen' opts.paged_taps_scroll_by_screen = v'!!settings.paged_taps_scroll_by_screen' diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index d513bb46ec..82d67f4825 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -840,6 +840,8 @@ class View: return { 'margin_left': 0 if name is self.book.manifest.title_page_name else sd.get('margin_left'), 'margin_right': 0 if name is self.book.manifest.title_page_name else sd.get('margin_right'), + 'margin_top': 0 if name is self.book.manifest.title_page_name else sd.get('margin_top'), + 'margin_bottom': 0 if name is self.book.manifest.title_page_name else sd.get('margin_bottom'), 'read_mode': sd.get('read_mode'), 'columns_per_screen': sd.get('columns_per_screen'), 'color_scheme': cs,