mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Implement keyboard and wheel scrolling in flow mode
This commit is contained in:
parent
8926f82472
commit
04929a943d
54
src/pyj/keycodes.pyj
Normal file
54
src/pyj/keycodes.pyj
Normal 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]
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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])))
|
||||
|
Loading…
x
Reference in New Issue
Block a user