Implement keyboard and wheel scrolling in flow mode

This commit is contained in:
Kovid Goyal 2016-04-07 15:18:27 +05:30
parent 8926f82472
commit 04929a943d
5 changed files with 185 additions and 71 deletions

54
src/pyj/keycodes.pyj Normal file
View File

@ -0,0 +1,54 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
KEYCODE_MAP = K = Object.create(None)
K[8] = 'backspace'
K[9] = 'tab'
K[13] = 'enter'
K[16] = 'shift'
K[17] = 'ctrl'
K[18] = 'alt'
K[19] = 'pause'
K[20] = 'capslock'
K[27] = 'escape'
K[32] = 'space'
K[33] = 'pageup'
K[34] = 'pagedown'
K[35] = 'end'
K[36] = 'home'
K[37] = 'left'
K[38] = 'up'
K[39] = 'right'
K[40] = 'down'
K[45] = 'insert'
K[46] = 'delete'
K[91] = 'meta_l'
K[92] = 'meta_r'
K[93] = 'select'
K[106] = 'numpad*'
K[107] = 'numpad+'
K[109] = 'numpad-'
K[111] = 'numpad/'
K[144] = 'numlock'
K[145] = 'scrolllock'
K[186] = ';'
K[190] = '.'
K[191] = '/'
K[192] = '`'
K[219] = '['
K[220] = '\\'
K[221] = ']'
K[222] = "'"
for i in range(10):
KEYCODE_MAP[48 + i] = i + ''
KEYCODE_MAP[96 + i] = 'numpad' + i
for i, c in enumerate(str.ascii_lowercase):
KEYCODE_MAP[65 + i] = c
for i in range(1, 13):
KEYCODE_MAP[111 + i] = 'f' + 1
def get_key(key_event):
return KEYCODE_MAP[key_event.keyCode]

View File

@ -1,60 +1,99 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from dom import build_rule
from elementmaker import E
from gettext import gettext as _
from read_book.globals import current_layout_mode, current_spine_item, uid, get_boss
flow_previous_indicator = flow_next_indicator = None
def document_height():
html = document.documentElement
return max(document.body.scrollHeight, document.body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight)
def setup_flow_indicators(style):
nonlocal flow_previous_indicator, flow_next_indicator
sel = 'flow-indicator-' + uid
flow_previous_indicator = E.div(
style='right:0; top:0;', '⬅ ' + _('prev page'), title=_('Go to the previous page'),
onclick=def():
get_boss().send_message('next_spine_item', previous=True)
)
flow_next_indicator = E.div(
style='right:0; bottom:0;', _('next page') + ' ➡', title=_('Go to the next page'),
onclick=def():
get_boss().send_message('next_spine_item', previous=False)
)
for c in [flow_next_indicator, flow_previous_indicator]:
c.setAttribute('class', sel)
document.body.appendChild(c)
document.body.appendChild(flow_previous_indicator)
document.body.appendChild(flow_next_indicator)
update_flow_mode_scroll_indicators()
style.push(build_rule(
'.' + sel, position='fixed', z_index='2147483647', padding='1ex 1em', margin='1em', border_radius='8px',
background_color='#eee', color='#0070FF', cursor='pointer', font_size='larger', font_family='sans-serif',
transition='opacity 0.5s ease-in', opacity='0'
))
style.push(build_rule('.' + sel + ':hover', color='red'))
def update_flow_mode_scroll_indicators():
if not flow_previous_indicator or current_layout_mode() is not 'flow':
return
near_top = window.pageYOffset < 25
near_bottom = abs(window.pageYOffset + window.innerHeight - document_height()) < 25
csi = current_spine_item()
p = near_top and not csi.is_first
n = near_bottom and not csi.is_last
flow_previous_indicator.style.visibility = 'visible' if p else 'hidden'
flow_next_indicator.style.visibility = 'visible' if n else 'hidden'
flow_previous_indicator.style.opacity = '1' if p else '0'
flow_next_indicator.style.opacity = '1' if n else '0'
def flow_change_mode():
if flow_previous_indicator:
d = 'block' if current_layout_mode() is 'flow' else 'none'
flow_previous_indicator.style.display = flow_next_indicator.style.display = d
from read_book.globals import get_boss
from keycodes import get_key
from utils import document_height, document_width
def flow_to_scroll_fraction(frac):
window.scrollTo(0, document_height() * frac)
def check_for_scroll_end(func):
return def ():
before = window.pageYOffset
func.apply(this, arguments)
if window.pageYOffset is before:
get_boss().send_message('next_spine_item', previous=window.pageYOffset < 5)
return False
return True
@check_for_scroll_end
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 = (window.innerHeight - 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 = (window.innerWidth - 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_start():
window.scrollTo(window.pageXOffset, 0)
@check_for_scroll_end
def goto_end():
window.scrollTo(window.pageXOffset, document_height())
@check_for_scroll_end
def scroll_by_page(up):
h = window.innerHeight - 10
window.scrollBy(0, -h if up 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_start() if key is 'up' else goto_end()
else:
smooth_y_scroll(key is 'up')
elif key is 'left' or key is 'right':
handled = True
if evt.ctrlKey:
window.scrollTo(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
if evt.ctrlKey:
get_boss().send_message('goto_doc_boundary', start=key is 'home')
else:
if key is 'home':
window.scrollTo(window.pageXOffset, 0)
else:
window.scrollTo(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()

View File

@ -3,11 +3,10 @@
from __python__ import bound_methods
from aes import GCM
from elementmaker import E
from gettext import install
from read_book.globals import set_boss, set_current_spine_item, current_layout_mode, current_spine_item
from read_book.resources import finalize_resources, unserialize_html
from read_book.flow_mode import setup_flow_indicators, update_flow_mode_scroll_indicators, flow_change_mode, flow_to_scroll_fraction
from read_book.flow_mode import flow_to_scroll_fraction, flow_onwheel, flow_onkeydown
from utils import debounce
class Boss:
@ -65,21 +64,30 @@ class Boss:
unserialize_html(root_data, self.content_loaded)
def content_loaded(self):
window.addEventListener('scroll', debounce(self.onscroll, 15))
style = v'[]'
setup_flow_indicators(style)
document.head.appendChild(E.style(type='text/css', style.join('\n')))
flow_change_mode()
document.documentElement.style.overflow = 'hidden'
window.addEventListener('scroll', debounce(self.update_cfi, 1000))
window.addEventListener('resize', debounce(self.onresize, 500))
window.addEventListener('wheel', self.onwheel)
window.addEventListener('keydown', self.onkeydown)
csi = current_spine_item()
if csi.initial_scroll_fraction:
if csi.initial_scroll_fraction is not None:
if current_layout_mode() is 'flow':
flow_to_scroll_fraction(csi.initial_scroll_fraction)
def onscroll(self, evt):
if evt.view and evt.view is not window.top:
return
self.last_window_ypos = window.pageYOffset
update_flow_mode_scroll_indicators()
def update_cfi(self):
pass # TODO: Update CFI
def onresize(self):
self.update_cfi()
def onwheel(self, evt):
evt.preventDefault()
if current_layout_mode() is 'flow':
flow_onwheel(evt)
def onkeydown(self, evt):
if current_layout_mode() is 'flow':
flow_onkeydown(evt)
def send_message(self, action, **data):
data.action = action

View File

@ -51,6 +51,7 @@ class View:
'ready': self.on_iframe_ready,
'error': self.on_iframe_error,
'next_spine_item': self.on_next_spine_item,
'goto_doc_boundary': self.goto_doc_boundary,
}
self.currently_showing = {'spine':0, 'cfi':None}
@ -117,22 +118,26 @@ class View:
# TODO: Check for last open position of book
self.show_name(book.manifest.spine[1])
def show_name(self, name, initial_scroll_fraction=0):
self.currently_showing = {'name':name, 'cfi':None, 'initial_scroll_fraction':initial_scroll_fraction}
def show_name(self, name, initial_scroll_fraction=None, cfi=None):
self.currently_showing = {'name':name, 'cfi':cfi, 'initial_scroll_fraction':initial_scroll_fraction}
load_resources(self.ui.db, self.book, name, self.loaded_resources, self.show_spine_item)
def goto_doc_boundary(self, data):
name = self.book.manifest.spine[0 if data.start else self.book.manifest.spine.length - 1]
self.show_name(name, initial_scroll_fraction=0 if data.start else 1)
def on_next_spine_item(self, data):
spine = self.book.manifest.spine
idx = spine.indexOf(self.currently_showing.name)
if data.previous:
if idx is 0:
return
idx = max(idx - 1, 0)
idx = min(spine.length - 1, max(idx - 1, 0))
self.show_name(spine[idx], initial_scroll_fraction=1)
else:
if idx is spine.length - 1:
return
idx = min(spine.length - 1, idx + 1)
idx = max(0, min(spine.length - 1, idx + 1))
self.show_name(spine[idx])
def show_spine_item(self, resource_data):

View File

@ -80,6 +80,14 @@ def human_readable(size, sep=' '):
size = size[:-2]
return size + sep + suffix
def document_height():
html = document.documentElement
return max(document.body.scrollHeight, document.body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight)
def document_width():
html = document.documentElement
return max(document.body.scrollWidth, document.body.offsetWidth, html.clientWidth, html.scrollWidth, html.offsetWidth)
if __name__ is '__main__':
print(fmt_sidx(10), fmt_sidx(1.2))
print(list(map(human_readable, [1, 1024.0, 1025, 1024*1024*2.3])))