calibre/src/pyj/read_book/flow_mode.pyj

262 lines
8.1 KiB
Plaintext

# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from __python__ import bound_methods, hash_literals
from select import word_at_point
from dom import set_css
from keycodes import get_key
from read_book.globals import get_boss
from read_book.viewport import scroll_viewport
from utils import document_height, document_width, viewport_to_document
def flow_to_scroll_fraction(frac):
scroll_viewport.scroll_to(0, document_height() * frac)
small_scroll_events = v'[]'
def clear_small_scrolls():
nonlocal small_scroll_events
small_scroll_events = v'[]'
def dispatch_small_scrolls():
if small_scroll_events.length:
now = window.performance.now()
if now - small_scroll_events[-1].time <= 2000:
window.setTimeout(dispatch_small_scrolls, 100)
return
amt = 0
for x in small_scroll_events:
amt += x.amt
clear_small_scrolls()
get_boss().report_human_scroll(amt / document_height())
def add_small_scroll(amt):
small_scroll_events.push({'amt': amt, 'time': window.performance.now()})
window.setTimeout(dispatch_small_scrolls, 100)
def report_human_scroll(amt):
if amt > 0:
h = scroll_viewport.height()
is_large_scroll = (amt / h) >= 0.5
if is_large_scroll:
clear_small_scrolls()
get_boss().report_human_scroll(amt / document_height())
else:
add_small_scroll(amt)
else:
clear_small_scrolls()
def _check_for_scroll_end(func, obj, args, report):
before = window.pageYOffset
func.apply(obj, args)
if window.pageYOffset is before:
get_boss().send_message('next_spine_item', previous=arguments[0] < 0)
return False
if report:
report_human_scroll(window.pageYOffset - before)
return True
def check_for_scroll_end(func):
return def ():
return _check_for_scroll_end(func, this, arguments, False)
def check_for_scroll_end_and_report(func):
return def ():
return _check_for_scroll_end(func, this, arguments, True)
@check_for_scroll_end_and_report
def scroll_by(y):
window.scrollBy(0, y)
def flow_onwheel(evt):
dx = dy = 0
if evt.deltaY:
if evt.deltaMode is evt.DOM_DELTA_PIXEL:
dy = evt.deltaY
elif evt.deltaMode is evt.DOM_DELTA_LINE:
dy = 15 * evt.deltaY
if evt.deltaMode is evt.DOM_DELTA_PAGE:
dy = (scroll_viewport.height() - 30) * evt.deltaY
if evt.deltaX:
if evt.deltaMode is evt.DOM_DELTA_PIXEL:
dx = evt.deltaX
elif evt.deltaMode is evt.DOM_DELTA_LINE:
dx = 15 * evt.deltaX
else:
dx = (scroll_viewport.width() - 30) * evt.deltaX
if dx:
window.scrollBy(dx, 0)
elif dy:
scroll_by(dy)
smooth_y_data = {'last_event_at':0, 'up': False, 'timer':None, 'source':'wheel', 'pixels_per_ms': 0.2, 'scroll_interval':10, 'stop_scrolling_after':100}
def do_y_scroll():
if scroll_by((-1 if smooth_y_data.up else 1) * smooth_y_data.pixels_per_ms * smooth_y_data.scroll_interval):
if Date.now() - smooth_y_data.last_event_at < smooth_y_data.stop_scrolling_after:
smooth_y_data.timer = setTimeout(do_y_scroll, smooth_y_data.scroll_interval)
def smooth_y_scroll(up):
clearTimeout(smooth_y_data.timer)
smooth_y_data.last_event_at = Date.now()
smooth_y_data.up = up
do_y_scroll()
@check_for_scroll_end
def goto_boundary(y):
scroll_viewport.scroll_to(window.pageXOffset, 0)
get_boss().report_human_scroll()
@check_for_scroll_end_and_report
def scroll_by_page(backward):
h = scroll_viewport.height() - 10
window.scrollBy(0, -h if backward else h)
def flow_onkeydown(evt):
handled = False
key = get_key(evt)
if key is 'up' or key is 'down':
handled = True
if evt.ctrlKey:
goto_boundary(-1 if key is 'up' else 1)
else:
smooth_y_scroll(key is 'up')
elif (key is 'left' or key is 'right') and not evt.altKey:
handled = True
if evt.ctrlKey:
scroll_viewport.scroll_to(0 if key is 'left' else document_width(), window.pageYOffset)
else:
window.scrollBy(-15 if key is 'left' else 15, 0)
elif key is 'home' or key is 'end':
handled = True
get_boss().report_human_scroll()
clear_small_scrolls()
if evt.ctrlKey:
get_boss().send_message('goto_doc_boundary', start=key is 'home')
else:
if key is 'home':
scroll_viewport.scroll_to(window.pageXOffset, 0)
else:
scroll_viewport.scroll_to(window.pageXOffset, document_height())
elif key is 'pageup' or key is 'pagedown' or key is 'space':
handled = True
scroll_by_page(key is 'pageup')
if handled:
evt.preventDefault()
def layout(is_single_page):
set_css(document.body, margin='0', border_width='0', padding='0')
class FlickAnimator:
SPEED_FACTOR = 0.04
DECEL_TIME_CONSTANT = 325 # milliseconds
VELOCITY_HISTORY = 300 # milliseconds
MIMUMUM_VELOCITY = 100 # pixels/sec
def __init__(self):
self.animation_id = None
def start(self, gesture):
self.vertical = gesture.axis is 'vertical'
now = window.performance.now()
points = times = None
for i, t in enumerate(gesture.times):
if now - t < self.VELOCITY_HISTORY:
points, times = gesture.points[i:], gesture.times[i:]
break
if times and times.length > 1:
elapsed = (times[-1] - times[0]) / 1000
if elapsed > 0 and points.length > 1:
delta = points[0] - points[-1]
velocity = delta / elapsed
if abs(velocity) > self.MIMUMUM_VELOCITY:
self.amplitude = self.SPEED_FACTOR * velocity
self.start_time = now
self.animation_id = window.requestAnimationFrame(self.auto_scroll)
def auto_scroll(self, ts):
if self.animation_id is None:
return
elapsed = window.performance.now() - self.start_time
delta = self.amplitude * Math.exp(-elapsed / self.DECEL_TIME_CONSTANT)
if abs(delta) >= 1:
delta = Math.round(delta)
if self.vertical:
window.scrollBy(0, delta)
else:
window.scrollBy(delta, 0)
self.animation_id = window.requestAnimationFrame(self.auto_scroll)
def stop(self):
if self.animation_id is not None:
window.cancelAnimationFrame(self.animation_id)
self.animation_id = None
flick_animator = FlickAnimator()
def handle_gesture(gesture):
flick_animator.stop()
if gesture.type is 'swipe':
if gesture.points.length > 1 and not gesture.is_held:
delta = gesture.points[-2] - gesture.points[-1]
if gesture.axis is 'vertical':
scroll_by(delta)
else:
window.scrollBy(delta, 0)
if not gesture.active and not gesture.is_held:
flick_animator.start(gesture)
elif gesture.type is 'prev-page':
scroll_by_page(True)
elif gesture.type is 'next-page':
scroll_by_page(False)
elif gesture.type is 'long-tap':
r = word_at_point(gesture.viewport_x, gesture.viewport_y)
if r:
s = document.getSelection()
s.removeAllRanges()
s.addRange(r)
get_boss().send_message('lookup_word', word=str(r))
anchor_funcs = {
'pos_for_elem': def pos_for_elem(elem):
if not elem:
return 0, 0
br = elem.getBoundingClientRect()
x, y = viewport_to_document(br.left, br.top, elem.ownerDocument)
return y, x
,
'visibility': def visibility(pos):
y, x = pos
if y < window.pageYOffset:
return -1
if y < window.pageYOffset + scroll_viewport.height():
if x < window.pageXOffset:
return -1
if x < window.pageXOffset + scroll_viewport.width():
return 0
return 1
,
'cmp': def cmp(a, b):
return (a[0] - b[0]) or (a[1] - b[1])
,
}