calibre/src/pyj/read_book/touch.pyj
Kovid Goyal d3f36abfac DRYer
2016-08-21 11:20:26 +05:30

216 lines
7.6 KiB
Plaintext

# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from __python__ import hash_literals, bound_methods
from read_book.globals import get_boss
HOLD_THRESHOLD = 750 # milliseconds
TAP_THRESHOLD = 5 # pixels
TAP_LINK_THRESHOLD = 5 # pixels
gesture_id = 0
def copy_touch(t):
now = window.performance.now()
return {
'identifier':t.identifier,
'page_x':v'[t.pageX]', 'page_y':v'[t.pageY]', 'viewport_x':v'[t.clientX]', 'viewport_y':v'[t.clientY]',
'active':True, 'mtime':now, 'ctime':now, 'is_held':False
}
def max_displacement(points):
ans = 0
first = points[0]
if not first?:
return ans
for p in points:
delta = abs(p - first)
if delta > ans:
ans = delta
return ans
def interpret_single_gesture(touch, gesture_id):
max_x_displacement = max_displacement(touch.viewport_x)
max_y_displacement = max_displacement(touch.viewport_y)
ans = {'active':touch.active, 'is_held':touch.is_held, 'id':gesture_id}
if max(max_x_displacement, max_y_displacement) < TAP_THRESHOLD:
ans.type = 'tap'
ans.viewport_x = touch.viewport_x[0]
ans.viewport_y = touch.viewport_y[0]
return ans
if touch.viewport_y.length < 2:
return ans
delta_x = abs(touch.viewport_x[-1] - touch.viewport_x[0])
delta_y = abs(touch.viewport_y[-1] - touch.viewport_y[0])
if max(delta_y, delta_x) > TAP_THRESHOLD and min(delta_x, delta_y)/max(delta_y, delta_x) < 0.35:
ans.type = 'swipe'
ans.axis = 'vertical' if delta_y > delta_x else 'horizontal'
ans.points = pts = touch.viewport_y if ans.axis is 'vertical' else touch.viewport_x
positive = pts[-1] > pts[0]
if ans.axis is 'vertical':
ans.direction = 'down' if positive else 'up'
else:
ans.direction = 'right' if positive else 'left'
return ans
return ans
def element_from_point(x, y):
# This does not currently support detecting links inside iframes, but since
# iframes are not common in books, I am going to ignore that for now
return document.elementFromPoint(x, y)
def find_link(x, y):
p = element_from_point(x, y)
while p:
if p.tagName and p.tagName.toLowerCase() is 'a' and p.hasAttribute('href'):
return p
p = p.parentNode
def tap_on_link(gesture):
for delta_x in [0, TAP_LINK_THRESHOLD, -TAP_LINK_THRESHOLD]:
for delta_y in [0, TAP_LINK_THRESHOLD, -TAP_LINK_THRESHOLD]:
x = gesture.viewport_x + delta_x
y = gesture.viewport_y + delta_y
link = find_link(x, y)
if link:
link.click()
return True
return False
class TouchHandler:
def __init__(self):
self.ongoing_touches = {}
self.gesture_id = None
self.hold_timer = None
self.handled_tap_hold = False
def prune_expired_touches(self):
now = window.performance.now()
expired = v'[]'
ans = False
for tid in self.ongoing_touches:
t = self.ongoing_touches[tid]
if t.active:
if now - t.mtime > 3000:
expired.push(t.identifier)
for tid in expired:
v'delete self.ongoing_touches[tid]'
@property
def has_active_touches(self):
for tid in self.ongoing_touches:
t = self.ongoing_touches[tid]
if t.active:
return True
return False
def start_hold_timer(self):
self.stop_hold_timer()
self.hold_timer = window.setTimeout(self.check_for_hold, 100)
def stop_hold_timer(self):
if self.hold_timer is not None:
window.clearTimeout(self.hold_timer)
self.hold_timer = None
def check_for_hold(self):
if len(self.ongoing_touches) > 0:
now = window.performance.now()
found_hold = False
for touchid in self.ongoing_touches:
touch = self.ongoing_touches[touchid]
if touch.active and now - touch.mtime > HOLD_THRESHOLD:
touch.is_held = True
found_hold = True
if found_hold:
self.dispatch_gesture()
self.start_hold_timer()
def handle_touchstart(self, ev):
ev.preventDefault(), ev.stopPropagation()
self.prune_expired_touches()
for touch in ev.changedTouches:
self.ongoing_touches[touch.identifier] = copy_touch(touch)
if self.gesture_id is None:
nonlocal gesture_id
gesture_id += 1
self.gesture_id = gesture_id
self.handled_tap_hold = False
if len(self.ongoing_touches) > 0:
self.start_hold_timer()
def update_touch(self, t, touch):
t.mtime = window.performance.now()
t.page_x.push(touch.pageX), t.page_y.push(touch.pageY)
t.viewport_x.push(touch.clientX), t.viewport_y.push(touch.clientY)
def handle_touchmove(self, ev):
ev.preventDefault(), ev.stopPropagation()
for touch in ev.changedTouches:
t = self.ongoing_touches[touch.identifier]
if t:
self.update_touch(t, touch)
self.dispatch_gesture()
def handle_touchend(self, ev):
ev.preventDefault(), ev.stopPropagation()
for touch in ev.changedTouches:
t = self.ongoing_touches[touch.identifier]
if t:
t.active = False
self.update_touch(t, touch)
self.prune_expired_touches()
if not self.has_active_touches:
self.dispatch_gesture()
self.ongoing_touches = {}
self.gesture_id = None
self.handled_tap_hold = False
def handle_touchcancel(self, ev):
ev.preventDefault(), ev.stopPropagation()
for touch in ev.changedTouches:
v'delete self.ongoing_touches[touch.identifier]'
self.gesture_id = None
self.handled_tap_hold = False
def dispatch_gesture(self):
touches = self.ongoing_touches
num = len(touches)
gesture = {}
if num is 1:
gesture = interpret_single_gesture(touches[Object.keys(touches)[0]], self.gesture_id)
if not gesture?.type:
return
if gesture.type is 'tap':
if gesture.is_held:
if not self.handled_tap_hold:
self.handled_tap_hold = True
fake_click = new MouseEvent('click', {
'clientX': gesture.viewport_x, 'clientY':gesture.viewport_y, 'buttons': 1,
'view':window.self, 'bubbles': True, 'cancelable': True,
})
elem = document.elementFromPoint(fake_click.clientX, fake_click.clientY)
if elem:
elem.dispatchEvent(fake_click)
return
if not tap_on_link(gesture):
# TODO: Convert the tap gesture into
# semantic gestures based on position of tap (next page/previous
# page/show ui) as these are common to both paged and flow mode.
pass
get_boss().handle_gesture(gesture)
touch_handler = TouchHandler()
def create_handlers():
window.addEventListener('touchstart', touch_handler.handle_touchstart, True)
window.addEventListener('touchmove', touch_handler.handle_touchmove, True)
window.addEventListener('touchend', touch_handler.handle_touchend, True)
window.addEventListener('touchcancel', touch_handler.handle_touchcancel, True)