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,
|
from PyQt4.Qt import (QSize, QSizePolicy, QUrl, SIGNAL, Qt, pyqtProperty,
|
||||||
QPainter, QPalette, QBrush, QDialog, QColor, QPoint, QImage, QRegion,
|
QPainter, QPalette, QBrush, QDialog, QColor, QPoint, QImage, QRegion,
|
||||||
QIcon, pyqtSignature, QAction, QMenu, QString, pyqtSignal,
|
QIcon, pyqtSignature, QAction, QMenu, QString, pyqtSignal,
|
||||||
QSwipeGesture, QApplication, pyqtSlot)
|
QApplication, pyqtSlot)
|
||||||
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings, QWebElement
|
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings, QWebElement
|
||||||
|
|
||||||
from calibre.gui2.viewer.flip import SlideFlip
|
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.image_popup import ImagePopup
|
||||||
from calibre.gui2.viewer.table_popup import TablePopup
|
from calibre.gui2.viewer.table_popup import TablePopup
|
||||||
from calibre.gui2.viewer.inspector import WebInspector
|
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.ebooks.oeb.display.webview import load_html
|
||||||
from calibre.constants import isxp, iswindows
|
from calibre.constants import isxp, iswindows
|
||||||
# }}}
|
# }}}
|
||||||
@ -482,10 +483,12 @@ class DocumentView(QWebView): # {{{
|
|||||||
|
|
||||||
magnification_changed = pyqtSignal(object)
|
magnification_changed = pyqtSignal(object)
|
||||||
DISABLED_BRUSH = QBrush(Qt.lightGray, Qt.Dense5Pattern)
|
DISABLED_BRUSH = QBrush(Qt.lightGray, Qt.Dense5Pattern)
|
||||||
|
gesture_handler = lambda s, e: False
|
||||||
|
|
||||||
def initialize_view(self, debug_javascript=False):
|
def initialize_view(self, debug_javascript=False):
|
||||||
self.setRenderHints(QPainter.Antialiasing|QPainter.TextAntialiasing|QPainter.SmoothPixmapTransform)
|
self.setRenderHints(QPainter.Antialiasing|QPainter.TextAntialiasing|QPainter.SmoothPixmapTransform)
|
||||||
self.flipper = SlideFlip(self)
|
self.flipper = SlideFlip(self)
|
||||||
|
self.gesture_handler = GestureHandler(self)
|
||||||
self.is_auto_repeat_event = False
|
self.is_auto_repeat_event = False
|
||||||
self.debug_javascript = debug_javascript
|
self.debug_javascript = debug_javascript
|
||||||
self.shortcuts = Shortcuts(SHORTCUTS, 'shortcuts/viewer')
|
self.shortcuts = Shortcuts(SHORTCUTS, 'shortcuts/viewer')
|
||||||
@ -561,7 +564,6 @@ class DocumentView(QWebView): # {{{
|
|||||||
else:
|
else:
|
||||||
m.addAction(name, a[key], self.shortcuts.get_sequences(key)[0])
|
m.addAction(name, a[key], self.shortcuts.get_sequences(key)[0])
|
||||||
self.goto_location_action.setMenu(self.goto_location_menu)
|
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 = QAction(_('Default font size'), self)
|
||||||
self.restore_fonts_action.setCheckable(True)
|
self.restore_fonts_action.setCheckable(True)
|
||||||
@ -1258,24 +1260,10 @@ class DocumentView(QWebView): # {{{
|
|||||||
return QWebView.resizeEvent(self, event)
|
return QWebView.resizeEvent(self, event)
|
||||||
|
|
||||||
def event(self, ev):
|
def event(self, ev):
|
||||||
if ev.type() == ev.Gesture:
|
if self.gesture_handler(ev):
|
||||||
swipe = ev.gesture(Qt.SwipeGesture)
|
return True
|
||||||
if swipe is not None:
|
|
||||||
self.handle_swipe(swipe)
|
|
||||||
return True
|
|
||||||
return QWebView.event(self, ev)
|
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):
|
def mouseReleaseEvent(self, ev):
|
||||||
opos = self.document.ypos
|
opos = self.document.ypos
|
||||||
ret = QWebView.mouseReleaseEvent(self, ev)
|
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