diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 7dc7f7b809..7343bb10f3 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -11,7 +11,7 @@ from functools import partial from PyQt4.Qt import (QSize, QSizePolicy, QUrl, SIGNAL, Qt, pyqtProperty, QPainter, QPalette, QBrush, QDialog, QColor, QPoint, QImage, QRegion, QIcon, pyqtSignature, QAction, QMenu, QString, pyqtSignal, - QSwipeGesture, QApplication, pyqtSlot) + QApplication, pyqtSlot) from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings, QWebElement from calibre.gui2.viewer.flip import SlideFlip @@ -26,6 +26,7 @@ from calibre.gui2.viewer.config import config, ConfigDialog, load_themes from calibre.gui2.viewer.image_popup import ImagePopup from calibre.gui2.viewer.table_popup import TablePopup from calibre.gui2.viewer.inspector import WebInspector +from calibre.gui2.viewer.gestures import GestureHandler from calibre.ebooks.oeb.display.webview import load_html from calibre.constants import isxp, iswindows # }}} @@ -482,10 +483,12 @@ class DocumentView(QWebView): # {{{ magnification_changed = pyqtSignal(object) DISABLED_BRUSH = QBrush(Qt.lightGray, Qt.Dense5Pattern) + gesture_handler = lambda s, e: False def initialize_view(self, debug_javascript=False): self.setRenderHints(QPainter.Antialiasing|QPainter.TextAntialiasing|QPainter.SmoothPixmapTransform) self.flipper = SlideFlip(self) + self.gesture_handler = GestureHandler(self) self.is_auto_repeat_event = False self.debug_javascript = debug_javascript self.shortcuts = Shortcuts(SHORTCUTS, 'shortcuts/viewer') @@ -561,7 +564,6 @@ class DocumentView(QWebView): # {{{ else: m.addAction(name, a[key], self.shortcuts.get_sequences(key)[0]) self.goto_location_action.setMenu(self.goto_location_menu) - self.grabGesture(Qt.SwipeGesture) self.restore_fonts_action = QAction(_('Default font size'), self) self.restore_fonts_action.setCheckable(True) @@ -1258,24 +1260,10 @@ class DocumentView(QWebView): # {{{ return QWebView.resizeEvent(self, event) def event(self, ev): - if ev.type() == ev.Gesture: - swipe = ev.gesture(Qt.SwipeGesture) - if swipe is not None: - self.handle_swipe(swipe) - return True + if self.gesture_handler(ev): + return True return QWebView.event(self, ev) - def handle_swipe(self, swipe): - if swipe.state() == Qt.GestureFinished: - if swipe.horizontalDirection() == QSwipeGesture.Left: - self.previous_page() - elif swipe.horizontalDirection() == QSwipeGesture.Right: - self.next_page() - elif swipe.verticalDirection() == QSwipeGesture.Up: - self.goto_previous_section() - elif swipe.horizontalDirection() == QSwipeGesture.Down: - self.goto_next_section() - def mouseReleaseEvent(self, ev): opos = self.document.ypos ret = QWebView.mouseReleaseEvent(self, ev) diff --git a/src/calibre/gui2/viewer/gestures.py b/src/calibre/gui2/viewer/gestures.py new file mode 100644 index 0000000000..e0e941b54f --- /dev/null +++ b/src/calibre/gui2/viewer/gestures.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2014, Kovid Goyal ' + +import time, ctypes +from PyQt4.Qt import QObject, QPointF, pyqtSignal, QEvent + +from calibre.constants import iswindows, isxp + +touch_supported = iswindows and not isxp + +HOLD_THRESHOLD = 1.0 # seconds +SWIPE_DISTANCE = 50 # manhattan pixels + +Tap, TapAndHold, Pinch, Swipe, SwipeAndHold = 'Tap', 'TapAndHold', 'Pinch', 'Swipe', 'SwipeAndHold' +Left, Right, Up, Down = 'Left', 'Right', 'Up', 'Down' + +class TouchPoint(object): + + def __init__(self, tp): + self.creation_time = self.last_update_time = self.time_of_last_move = time.time() + self.start_position = self.current_position = self.previous_position = QPointF(tp.pos()) + self.start_screen_position = self.current_screen_position = self.previous_screen_position = QPointF(tp.screenPos()) + self.time_since_last_update = -1 + self.total_movement = 0 + + def update(self, tp): + now = time.time() + self.time_since_last_update = now - self.last_update_time + self.last_update_time = now + self.previous_position, self.previous_screen_position = self.current_position, self.current_screen_position + self.current_position = QPointF(tp.pos()) + self.current_screen_position = QPointF(tp.screenPos()) + movement = (self.current_position - self.previous_position).manhattanLength() + self.total_movement += movement + if movement > 5: + self.time_of_last_move = now + + @property + def swipe_type(self): + x_movement = self.current_position.x() - self.start_position.x() + y_movement = self.current_position.y() - self.start_position.y() + xabs, yabs = map(abs, (x_movement, y_movement)) + if max(xabs, yabs) < SWIPE_DISTANCE or min(xabs/max(yabs, 0.01), yabs/max(xabs, 0.01)) > 0.3: + return + d = x_movement if xabs > yabs else y_movement + axis = (Left, Right) if xabs > yabs else (Up, Down) + return axis[0 if d < 0 else 1] + +class State(QObject): + + tapped = pyqtSignal(object) + swiped = pyqtSignal(object) + tap_hold_updated = pyqtSignal(object) + swipe_hold_updated = pyqtSignal(object) + tap_and_hold_finished = pyqtSignal(object) + swipe_and_hold_finished = pyqtSignal(object) + + def __init__(self): + QObject.__init__(self) + self.clear() + + def clear(self): + self.possible_gestures = set() + self.touch_points = {} + self.hold_started = False + self.hold_data = None + + def start(self): + self.clear() + self.possible_gestures = {Tap, TapAndHold, Swipe, Pinch, SwipeAndHold} + + def update(self, ev, boundary='update'): + if boundary == 'start': + self.start() + + for tp in ev.touchPoints(): + tpid = tp.id() + if tpid not in self.touch_points: + self.touch_points[tpid] = TouchPoint(tp) + else: + self.touch_points[tpid].update(tp) + + if len(self.touch_points) > 2: + self.possible_gestures.clear() + elif len(self.touch_points) > 1: + self.possible_gestures &= {Pinch} + + if boundary == 'end': + self.finalize() + self.clear() + else: + self.check_for_holds() + + def check_for_holds(self): + if not {SwipeAndHold, TapAndHold} & self.possible_gestures: + return + now = time.time() + tp = next(self.touch_points.itervalues()) + if now - tp.time_of_last_move < HOLD_THRESHOLD: + return + if self.hold_started: + if TapAndHold in self.possible_gestures: + self.tap_hold_updated.emit(tp) + if SwipeAndHold in self.possible_gestures: + self.swipe_hold_updated.emit(tp) + else: + self.possible_gestures &= {TapAndHold, SwipeAndHold} + if tp.total_movement > SWIPE_DISTANCE: + st = tp.swipe_type + if st is None: + self.possible_gestures.clear() + else: + self.hold_started = True + self.possible_gestures = {SwipeAndHold} + self.hold_data = (now, st) + else: + self.possible_gestures = {TapAndHold} + self.hold_started = True + self.hold_data = now + + def finalize(self): + if Tap in self.possible_gestures: + tp = next(self.touch_points.itervalues()) + if tp.total_movement <= SWIPE_DISTANCE: + self.tapped.emit(tp) + return + + if Swipe in self.possible_gestures: + tp = next(self.touch_points.itervalues()) + st = tp.swipe_type + if st is not None: + self.swiped.emit(st) + return + + if Pinch in self.possible_gestures: + pass # TODO: Implement this + + if TapAndHold in self.possible_gestures: + tp = next(self.touch_points.itervalues()) + self.tap_and_hold_finished.emit(tp) + return + + if SwipeAndHold in self.possible_gestures: + self.swipe_and_hold_finished.emit(self.hold_data[1]) + return + + +class GestureHandler(QObject): + + def __init__(self, view): + QObject.__init__(self, view) + self.state = State() + self.state.swiped.connect(self.handle_swipe) + self.evmap = {QEvent.TouchBegin: 'start', QEvent.TouchUpdate: 'update', QEvent.TouchEnd: 'end'} + + # Ignore fake mouse events generated by the window system from touch + # events. At least on windows, we know how to identify these fake + # events. See http://msdn.microsoft.com/en-us/library/windows/desktop/ms703320(v=vs.85).aspx + self.is_fake_mouse_event = lambda : False + if touch_supported and iswindows: + LPARAM = ctypes.c_long if ctypes.sizeof(ctypes.c_long) == ctypes.sizeof(ctypes.c_void_p) else ctypes.c_longlong + MI_WP_SIGNATURE = 0xFF515700 + SIGNATURE_MASK = 0xFFFFFF00 + try: + f = ctypes.windll.user32.GetMessageExtraInfo + f.restype = LPARAM + def is_fake_mouse_event(): + val = f() + ans = (val & SIGNATURE_MASK) == MI_WP_SIGNATURE + return ans + self.is_fake_mouse_event = is_fake_mouse_event + except Exception: + import traceback + traceback.print_exc() + + def __call__(self, ev): + if not touch_supported: + return False + etype = ev.type() + if etype in ( + QEvent.MouseMove, QEvent.MouseButtonPress, + QEvent.MouseButtonRelease, QEvent.MouseButtonDblClick, + QEvent.ContextMenu) and self.is_fake_mouse_event(): + # swallow fake mouse events that the windowing system generates from touch events + ev.accept() + return True + boundary = self.evmap.get(etype, None) + if boundary is None: + return False + self.state.update(ev, boundary=boundary) + ev.accept() + return True + + def handle_swipe(self, direction): + view = self.parent() + func = {Left:'next_page', Right: 'previous_page', Up:'goto_previous_section', Down:'goto_next_section'}[direction] + getattr(view, func)() +