Start work on touch screen gesture support for the viewer

This commit is contained in:
Kovid Goyal 2014-02-09 15:19:39 +05:30
parent a309d3ba13
commit 0852b99192
2 changed files with 209 additions and 18 deletions

View File

@ -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)

View File

@ -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 <kovid at kovidgoyal.net>'
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)()