2
# vim:fileencoding=utf-8
3
from __future__ import (unicode_literals, division, absolute_import,
7
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
9
import time, ctypes, sys
10
from functools import partial
11
from PyQt4.Qt import (
12
QObject, QPointF, pyqtSignal, QEvent, QApplication, QMouseEvent, Qt,
13
QContextMenuEvent, QDialog, QDialogButtonBox, QLabel, QVBoxLayout)
15
from calibre.constants import iswindows
17
touch_supported = False
18
if iswindows and sys.getwindowsversion()[:2] >= (6, 2): # At least windows 7
19
from ctypes import wintypes
21
RegisterTouchWindow = ctypes.windll.user32.RegisterTouchWindow
22
RegisterTouchWindow.argtypes = (wintypes.HWND, wintypes.ULONG)
23
RegisterTouchWindow.restype = wintypes.BOOL
24
touch_supported = True
28
SWIPE_HOLD_INTERVAL = 0.5 # seconds
29
HOLD_THRESHOLD = 1.0 # seconds
30
TAP_THRESHOLD = 50 # manhattan pixels
31
SWIPE_DISTANCE = 100 # manhattan pixels
32
PINCH_CENTER_THRESHOLD = 150 # manhattan pixels
33
PINCH_SQUEEZE_FACTOR = 2.5 # smaller length must be less that larger length / squeeze factor
35
Tap, TapAndHold, Pinch, Swipe, SwipeAndHold = 'Tap', 'TapAndHold', 'Pinch', 'Swipe', 'SwipeAndHold'
36
Left, Right, Up, Down = 'Left', 'Right', 'Up', 'Down'
39
class Help(QDialog): # {{{
41
def __init__(self, parent=None):
42
QDialog.__init__(self, parent=parent)
43
self.l = l = QVBoxLayout(self)
46
self.la = la = QLabel(
49
h2 { text-align: center }
50
dt { font-weight: bold }
51
dd { margin-bottom: 1.5em }
56
<h2>The list of available gestures</h2>
59
<dd>A single tap on the right two thirds of the page will turn to the next page
60
and on the left one-third of the page will turn to the previous page. Single tapping
61
on a link will activate the link.</dd>
64
<dd>Swipe to the left to go to the next page and to the right to go to the previous page.
65
This mimics turning pages in a paper book.</dd>
68
<dd>Pinch in or out to decrease or increase the font size</dd>
70
<dt>Swipe and hold</dt>
71
<dd>If you swipe and the hold your finger down instead of lifting it, pages will be turned
72
rapidly allowing for quickly scanning through large numbers of pages.</dd>
75
<dd>Bring up the context (right-click) menu</dd>
79
la.setAlignment(Qt.AlignTop | Qt.AlignLeft)
81
l.addWidget(la, Qt.AlignTop|Qt.AlignLeft)
82
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Close)
83
bb.accepted.connect(self.accept)
84
bb.rejected.connect(self.reject)
89
class TouchPoint(object):
91
def __init__(self, tp):
92
self.creation_time = self.last_update_time = self.time_of_last_move = time.time()
93
self.start_screen_position = self.current_screen_position = self.previous_screen_position = QPointF(tp.screenPos())
94
self.time_since_last_update = -1
95
self.total_movement = 0
99
self.time_since_last_update = now - self.last_update_time
100
self.last_update_time = now
101
self.previous_screen_position, self.current_screen_position = self.current_screen_position, QPointF(tp.screenPos())
102
movement = (self.current_screen_position - self.previous_screen_position).manhattanLength()
103
self.total_movement += movement
105
self.time_of_last_move = now
108
def swipe_type(self):
109
x_movement = self.current_screen_position.x() - self.start_screen_position.x()
110
y_movement = self.current_screen_position.y() - self.start_screen_position.y()
111
xabs, yabs = map(abs, (x_movement, y_movement))
112
if max(xabs, yabs) < SWIPE_DISTANCE or min(xabs/max(yabs, 0.01), yabs/max(xabs, 0.01)) > 0.3:
114
d = x_movement if xabs > yabs else y_movement
115
axis = (Left, Right) if xabs > yabs else (Up, Down)
116
return axis[0 if d < 0 else 1]
118
def get_pinch(p1, p2):
119
starts = [p1.start_screen_position, p2.start_screen_position]
120
ends = [p1.current_screen_position, p2.current_screen_position]
121
start_center = (starts[0] + starts[1]) / 2.0
122
end_center = (ends[0] + ends[1]) / 2.0
123
if (start_center - end_center).manhattanLength() > PINCH_CENTER_THRESHOLD:
125
start_length = (starts[0] - starts[1]).manhattanLength()
126
end_length = (ends[0] - ends[1]).manhattanLength()
127
if min(start_length, end_length) > max(start_length, end_length) / PINCH_SQUEEZE_FACTOR:
129
return In if start_length > end_length else Out
131
class State(QObject):
133
tapped = pyqtSignal(object)
134
swiped = pyqtSignal(object)
135
pinched = pyqtSignal(object)
136
tap_hold_started = pyqtSignal(object)
137
tap_hold_updated = pyqtSignal(object)
138
swipe_hold_started = pyqtSignal(object)
139
swipe_hold_updated = pyqtSignal(object)
140
tap_hold_finished = pyqtSignal(object)
141
swipe_hold_finished = pyqtSignal(object)
144
QObject.__init__(self)
148
self.possible_gestures = set()
149
self.touch_points = {}
150
self.hold_started = False
151
self.hold_data = None
155
self.possible_gestures = {Tap, TapAndHold, Swipe, Pinch, SwipeAndHold}
157
def update(self, ev, boundary='update'):
158
if boundary == 'start':
161
for tp in ev.touchPoints():
163
if tpid not in self.touch_points:
164
self.touch_points[tpid] = TouchPoint(tp)
166
self.touch_points[tpid].update(tp)
168
if len(self.touch_points) > 2:
169
self.possible_gestures.clear()
170
elif len(self.touch_points) > 1:
171
self.possible_gestures &= {Pinch}
173
if boundary == 'end':
177
self.check_for_holds()
179
def check_for_holds(self):
180
if not {SwipeAndHold, TapAndHold} & self.possible_gestures:
183
tp = next(self.touch_points.itervalues())
184
if now - tp.time_of_last_move < HOLD_THRESHOLD:
186
if self.hold_started:
187
if TapAndHold in self.possible_gestures:
188
self.tap_hold_updated.emit(tp)
189
if SwipeAndHold in self.possible_gestures:
190
self.swipe_hold_updated.emit(self.hold_data[1])
192
self.possible_gestures &= {TapAndHold, SwipeAndHold}
193
if tp.total_movement > TAP_THRESHOLD:
196
self.possible_gestures.clear()
198
self.hold_started = True
199
self.possible_gestures = {SwipeAndHold}
200
self.hold_data = (now, st)
201
self.swipe_hold_started.emit(st)
203
self.possible_gestures = {TapAndHold}
204
self.hold_started = True
206
self.tap_hold_started.emit(tp)
209
if Tap in self.possible_gestures:
210
tp = next(self.touch_points.itervalues())
211
if tp.total_movement <= TAP_THRESHOLD:
215
if Swipe in self.possible_gestures:
216
tp = next(self.touch_points.itervalues())
222
if Pinch in self.possible_gestures:
223
points = tuple(self.touch_points.itervalues())
225
pinch_dir = get_pinch(*points)
226
if pinch_dir is not None:
227
self.pinched.emit(pinch_dir)
229
if not self.hold_started:
232
if TapAndHold in self.possible_gestures:
233
tp = next(self.touch_points.itervalues())
234
self.tap_hold_finished.emit(tp)
237
if SwipeAndHold in self.possible_gestures:
238
self.swipe_hold_finished.emit(self.hold_data[1])
242
class GestureHandler(QObject):
244
def __init__(self, view):
245
QObject.__init__(self, view)
247
self.last_swipe_hold_update = None
248
self.state.swiped.connect(self.handle_swipe)
249
self.state.tapped.connect(self.handle_tap)
250
self.state.tap_hold_started.connect(partial(self.handle_tap_hold, 'start'))
251
self.state.tap_hold_updated.connect(partial(self.handle_tap_hold, 'update'))
252
self.state.tap_hold_finished.connect(partial(self.handle_tap_hold, 'end'))
253
self.state.swipe_hold_started.connect(partial(self.handle_swipe_hold, 'start'))
254
self.state.swipe_hold_updated.connect(partial(self.handle_swipe_hold, 'update'))
255
self.state.swipe_hold_finished.connect(partial(self.handle_swipe_hold, 'end'))
256
self.state.pinched.connect(self.handle_pinch)
257
self.evmap = {QEvent.TouchBegin: 'start', QEvent.TouchUpdate: 'update', QEvent.TouchEnd: 'end'}
259
# Ignore fake mouse events generated by the window system from touch
260
# events. At least on windows, we know how to identify these fake
261
# events. See http://msdn.microsoft.com/en-us/library/windows/desktop/ms703320(v=vs.85).aspx
262
self.is_fake_mouse_event = lambda : False
263
if touch_supported and iswindows:
264
MI_WP_SIGNATURE = 0xFF515700
265
SIGNATURE_MASK = 0xFFFFFF00
267
f = ctypes.windll.user32.GetMessageExtraInfo
268
f.restype = wintypes.LPARAM
269
def is_fake_mouse_event():
271
ans = (val & SIGNATURE_MASK) == MI_WP_SIGNATURE
273
self.is_fake_mouse_event = is_fake_mouse_event
274
QApplication.instance().focusChanged.connect(self.register_for_wm_touch)
277
traceback.print_exc()
279
def register_for_wm_touch(self, *args):
280
if touch_supported and iswindows:
281
# For some reason performing certain actions like toggling the ToC
282
# view causes windows to stop sending WM_TOUCH events. This works
284
# This might need to be changed for Qt 5 and effectivewinid returns
285
# a different kind of object.
286
hwnd = int(self.parent().effectiveWinId())
287
RegisterTouchWindow(hwnd, 0)
289
def __call__(self, ev):
290
if not touch_supported:
294
QEvent.MouseMove, QEvent.MouseButtonPress,
295
QEvent.MouseButtonRelease, QEvent.MouseButtonDblClick,
296
QEvent.ContextMenu) and self.is_fake_mouse_event():
297
# swallow fake mouse events that the windowing system generates from touch events
300
boundary = self.evmap.get(etype, None)
303
self.state.update(ev, boundary=boundary)
307
def close_open_menu(self):
308
m = getattr(self.parent(), 'context_menu', None)
309
if m is not None and m.isVisible():
313
def handle_swipe(self, direction):
314
if self.close_open_menu():
317
func = {Left:'next_page', Right: 'previous_page', Up:'goto_previous_section', Down:'goto_next_section'}[direction]
318
getattr(view, func)()
320
def current_position(self, tp):
321
return self.parent().mapFromGlobal(tp.current_screen_position.toPoint())
323
def handle_tap(self, tp):
324
if self.close_open_menu():
327
mf = view.document.mainFrame()
328
r = mf.hitTestContent(self.current_position(tp))
329
if r.linkElement().isNull():
330
threshold = view.width() / 3.0
331
attr = 'previous' if self.current_position(tp).x() <= threshold else 'next'
332
getattr(view, '%s_page'%attr)()
334
for etype in (QEvent.MouseButtonPress, QEvent.MouseButtonRelease):
335
ev = QMouseEvent(etype, self.current_position(tp), tp.current_screen_position.toPoint(), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
336
QApplication.sendEvent(view, ev)
338
def handle_tap_hold(self, action, tp):
339
etype = {'start':QEvent.MouseButtonPress, 'update':QEvent.MouseMove, 'end':QEvent.MouseButtonRelease}[action]
340
ev = QMouseEvent(etype, self.current_position(tp), tp.current_screen_position.toPoint(), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
341
QApplication.sendEvent(self.parent(), ev)
343
ev = QContextMenuEvent(QContextMenuEvent.Other, self.current_position(tp), tp.current_screen_position.toPoint())
344
# We have to use post event otherwise the popup remains an alien widget and does not receive events
345
QApplication.postEvent(self.parent(), ev)
347
def handle_swipe_hold(self, action, direction):
349
if action == 'start':
350
self.last_swipe_hold_update = time.time()
352
self.handle_swipe(direction)
354
view.is_auto_repeat_event = False
355
elif action == 'update' and self.last_swipe_hold_update is not None and time.time() - self.last_swipe_hold_update > SWIPE_HOLD_INTERVAL:
356
view.is_auto_repeat_event = True
357
self.last_swipe_hold_update = time.time()
359
self.handle_swipe(direction)
361
view.is_auto_repeat_event = False
362
elif action == 'end':
363
self.last_swipe_hold_update = None
365
def handle_pinch(self, direction):
366
attr = 'magnify' if direction is Out else 'shrink'
367
getattr(self.parent(), '%s_fonts' % attr)()
370
Help(self.parent()).exec_()
372
if __name__ == '__main__':
373
app = QApplication([])