mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Start work on touch screen gesture support for the viewer
This commit is contained in:
parent
a309d3ba13
commit
0852b99192
@ -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)
|
||||
|
203
src/calibre/gui2/viewer/gestures.py
Normal file
203
src/calibre/gui2/viewer/gestures.py
Normal 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)()
|
||||
|
Loading…
x
Reference in New Issue
Block a user