mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-12-07 13:45:11 -05:00
286 lines
11 KiB
Plaintext
286 lines
11 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, window_width, window_height
|
|
from book_list.globals import get_read_ui
|
|
|
|
HOLD_THRESHOLD = 750 # milliseconds
|
|
TAP_THRESHOLD = 5 # pixels
|
|
TAP_LINK_THRESHOLD = 5 # pixels
|
|
PINCH_THRESHOLD = 10 # 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, 'mtimes':v'[now]', 'ctime':now, 'is_held':False, 'x_velocity': 0, 'y_velocity': 0
|
|
}
|
|
|
|
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
|
|
ans.times = touch.mtimes
|
|
positive = pts[-1] > pts[0]
|
|
if ans.axis is 'vertical':
|
|
ans.direction = 'down' if positive else 'up'
|
|
ans.velocity = touch.y_velocity
|
|
else:
|
|
ans.direction = 'right' if positive else 'left'
|
|
ans.velocity = touch.x_velocity
|
|
return ans
|
|
return ans
|
|
|
|
def interpret_double_gesture(touch1, touch2, gesture_id):
|
|
ans = {'active':touch1.active or touch2.active, 'is_held':touch1.is_held or touch2.is_held, 'id':gesture_id}
|
|
max_x_displacement1 = max_displacement(touch1.viewport_x)
|
|
max_x_displacement2 = max_displacement(touch2.viewport_x)
|
|
max_y_displacement1 = max_displacement(touch1.viewport_y)
|
|
max_y_displacement2 = max_displacement(touch2.viewport_y)
|
|
if max(max_x_displacement1, max_y_displacement1) < TAP_THRESHOLD and max(max_x_displacement2, max_y_displacement2) < TAP_THRESHOLD:
|
|
ans.type = 'double-tap'
|
|
ans.viewport_x1 = touch1.viewport_x[0]
|
|
ans.viewport_y1 = touch1.viewport_y[0]
|
|
ans.viewport_x2 = touch2.viewport_x[0]
|
|
ans.viewport_y2 = touch2.viewport_y[0]
|
|
return ans
|
|
initial_distance = Math.sqrt((touch1.viewport_x[0] - touch2.viewport_x[0])**2 + (touch1.viewport_y[0] - touch2.viewport_y[0])**2)
|
|
final_distance = Math.sqrt((touch1.viewport_x[-1] - touch2.viewport_x[-1])**2 + (touch1.viewport_y[-1] - touch2.viewport_y[-1])**2)
|
|
distance = abs(final_distance - initial_distance)
|
|
if distance > PINCH_THRESHOLD:
|
|
ans.type = 'pinch'
|
|
ans.direction = 'in' if final_distance < initial_distance else 'out'
|
|
ans.distance = distance
|
|
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'[]'
|
|
for tid in self.ongoing_touches:
|
|
t = self.ongoing_touches[tid]
|
|
if t.active:
|
|
if now - t.mtimes[-1] > 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.mtimes[-1] > 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):
|
|
now = window.performance.now()
|
|
t.mtimes.push(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)
|
|
elif num is 2:
|
|
t = Object.keys(touches)
|
|
gesture = interpret_double_gesture(touches[t[0]], touches[t[1]], self.gesture_id)
|
|
if not gesture?.type:
|
|
return
|
|
self.handle_gesture(gesture)
|
|
|
|
class BookTouchHandler(TouchHandler):
|
|
|
|
def __init__(self, for_side_margin=None):
|
|
self.for_side_margin = for_side_margin
|
|
TouchHandler.__init__(self)
|
|
|
|
def handle_gesture(self, gesture):
|
|
gesture.from_side_margin = self.for_side_margin
|
|
if gesture.type is 'tap':
|
|
if gesture.is_held:
|
|
if not self.for_side_margin and 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 gesture.active:
|
|
if self.for_side_margin or not tap_on_link(gesture):
|
|
if gesture.viewport_y < min(100, window_height() / 4):
|
|
gesture.type = 'show-chrome'
|
|
else:
|
|
if gesture.viewport_x < min(100, window_width() / 4):
|
|
gesture.type = 'prev-page'
|
|
else:
|
|
gesture.type = 'next-page'
|
|
if gesture.type is 'pinch':
|
|
if gesture.active:
|
|
return
|
|
if gesture.type is 'double-tap':
|
|
if gesture.active:
|
|
return
|
|
gesture.type = 'show-chrome'
|
|
if self.for_side_margin:
|
|
get_read_ui().view.forward_gesture(gesture)
|
|
else:
|
|
get_boss().handle_gesture(gesture)
|
|
|
|
def __repr__(self):
|
|
return 'BookTouchHandler:for_side_margin:' + self.for_side_margin
|
|
|
|
main_touch_handler = BookTouchHandler()
|
|
left_margin_handler = BookTouchHandler('left')
|
|
right_margin_handler = BookTouchHandler('right')
|
|
|
|
def install_handlers(elem, handler):
|
|
options = {'capture': True, 'passive': False}
|
|
elem.addEventListener('touchstart', handler.handle_touchstart, options)
|
|
elem.addEventListener('touchmove', handler.handle_touchmove, options)
|
|
elem.addEventListener('touchend', handler.handle_touchend, options)
|
|
elem.addEventListener('touchcancel', handler.handle_touchcancel, options)
|
|
|
|
def create_handlers():
|
|
# Safari does not work if we register the handler
|
|
# on window instead of document
|
|
install_handlers(document, main_touch_handler)
|
|
|
|
def set_left_margin_handler(elem):
|
|
install_handlers(elem, left_margin_handler)
|
|
|
|
def set_right_margin_handler(elem):
|
|
install_handlers(elem, right_margin_handler)
|