# vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal 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)