forked from nvaccess/nvda
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtouchTracker.py
347 lines (328 loc) · 19 KB
/
touchTracker.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
#touchTracker.py
#A part of NonVisual Desktop Access (NVDA)
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.
#Copyright (C) 2012 NV Access Limited
import threading
import time
from collections import OrderedDict
from logHandler import log
#Possible actions (single trackers)
action_tap="tap"
action_hold="hold"
action_tapAndHold="tapandhold"
action_flickUp="flickup"
action_flickDown="flickdown"
action_flickLeft="flickleft"
action_flickRight="flickright"
action_hoverDown="hoverdown"
action_hover="hover"
action_hoverUp="hoverup"
action_unknown="unknown"
hoverActions=(action_hoverDown,action_hover,action_hoverUp)
#timeout for detection of flicks and plural trackers
multitouchTimeout=0.25
#The distance a finger must travel to be treeted as a flick
minFlickDistance=50
#How far a finger is allowed to drift purpandicular to a flick direction to make the flick impossible
maxAccidentalDrift=10
actionLabels={
# Translators: a very quick touch and release of a finger on a touch screen
action_tap:pgettext("touch action","tap"),
# Translators: a very quick touch and release, then another touch with no release, on a touch screen
action_tapAndHold:pgettext("touch action","tap and hold"),
# Translators: a touch with no release, on a touch screen.
action_hold:pgettext("touch action","hold"),
# Translators: a quick swipe of a finger in an up direction, on a touch screen.
action_flickUp:pgettext("touch action","flick up"),
# Translators: a quick swipe of a finger in an down direction, on a touch screen.
action_flickDown:pgettext("touch action","flick down"),
# Translators: a quick swipe of a finger in a left direction, on a touch screen.
action_flickLeft:pgettext("touch action","flick left"),
# Translators: a quick swipe of a finger in a right direction, on a touch screen.
action_flickRight:pgettext("touch action","flick right"),
# Translators: a finger has been held on the touch screen long enough to be considered as hovering
action_hoverDown:pgettext("touch action","hover down"),
# Translators: A finger is still touching the touch screen and is moving around with out breaking contact.
action_hover:pgettext("touch action","hover"),
# Translators: a finger that was hovering (touching the touch screen for a long time) has been released
action_hoverUp:pgettext("touch action","hover up"),
}
class SingleTouchTracker(object):
"""
Represents the lifetime of one single finger while its in contact with the touch device, tracking start and current coordinates, start and end times, and whether its complete (broken contact yet).
It also calculates what kind of single action (tap, flick, hover) this finger is performing, once it has enough data.
@ivar ID: the ID this finger has been assigned by the Operating System.
@type ID: int
@ivar x: The last known x screen coordinate of this finger
@type x: int
@ivar y: The last known y screen coordinate of this finger
@type y: int
@ivar startX: The x screen coordinate where the finger first made contact
@type startX: int
@ivar startY: The y screen coordinate where the finger first made contact
@type startY: int
@ivar startTime: the time at which the finger first made contact
@type startTime: float
@ivar endTime: the time at which the finger broke contact. Before breaking contact the value is -1
@type endTime: float
@ivar maxAbsDeltaX: the maximum distance this finger has traveled on the x access while making contact
@type maxAbsDeltaX: int
@ivar maxAbsDeltaY: the maximum distance this finger has traveled on the y access while making contact
@type maxAbsDeltaY: int
@ivar action: the action this finger has performed (one of the action_* constants,E.g. tap, flickRight, hover etc). If not enough data has been collected yet the action will be unknown.
@type action: string
@ivar complete: If true then this finger has broken contact
@type complete: bool
"""
__slots__=['ID','x','y','startX','startY','startTime','endTime','maxAbsDeltaX','maxAbsDeltaY','action','complete']
def __init__(self,ID,x,y):
self.ID=ID
self.x=self.startX=x
self.y=self.startY=y
self.startTime=time.time()
self.endTime=-1
self.maxAbsDeltaX=0
self.maxAbsDeltaY=0
self.action=action_unknown
self.complete=False
def update(self,x,y,complete=False):
"""Called to alert this single tracker that the finger has moved or broken contact."""
self.x=x
self.y=y
deltaX=x-self.startX
deltaY=y-self.startY
absDeltaX=abs(deltaX)
absDeltaY=abs(deltaY)
self.maxAbsDeltaX=max(absDeltaX,self.maxAbsDeltaX)
self.maxAbsDeltaY=max(absDeltaY,self.maxAbsDeltaY)
curTime=time.time()
deltaTime=curTime-self.startTime
if deltaTime<multitouchTimeout: #not timed out yet
if complete and self.maxAbsDeltaX<maxAccidentalDrift and self.maxAbsDeltaY<maxAccidentalDrift:
#The completed quick touch never drifted too far from its initial contact point therefore its a tap
self.action=action_tap
elif complete and self.maxAbsDeltaX>=minFlickDistance and self.maxAbsDeltaX>self.maxAbsDeltaY:
#The completed quick touch traveled far enough horizontally to be a flick and was also not off by more than 45 degrees.
if deltaX>0: #Traveling to the right
self.action=action_flickRight
else: #traveling to the left
self.action=action_flickLeft
elif complete and self.maxAbsDeltaY>=minFlickDistance and self.maxAbsDeltaY>self.maxAbsDeltaX:
#The completed quick touch traveled far enough vertically to be a flick and was also not off by more than 45 degrees.
if deltaY>0: #traveling down
self.action=action_flickDown
else: #traveling up
self.action=action_flickUp
else: #timeout exceeded, must be a kind of hover
self.action=action_hover
self.complete=complete
if complete:
self.endTime=curTime
class MultiTouchTracker(object):
"""Represents an action jointly performed by 1 or more fingers.
@ivar action: the action this finger has performed (one of the action_* constants,E.g. tap, flickRight, hover etc).
@type action: string
@ivar x: the x screen coordinate where the action was performed. For multi-finger actions it is the average position of each of the fingers. For plural actions it is based on the first occurence
@type x: int
@ivar y: the y screen coordinate where the action was performed. For multi-finger actions it is the average position of each of the fingers. For plural actions it is based on the first occurence
@type y: int
@ivar startTime: the time the action began
@type startTime: float
@ivar endTime: the time the action was complete
@type endTime: float
@ivar numFingers: the number of fingers that performed this action
@type numFingers: int
@ivar actionCount: the number of times this action was performed in quick succession (E.g. 2 for a double tap)
@ivar childTrackers: a list of L{MultiTouchTracker} objects which represent the direct sub-actions of this action. E.g. a 2-finger tripple tap's childTrackers will contain 3 2-finger taps. Each of the 2-finger taps' childTrackers will contain 2 taps.
@type childTrackers: list of L{MultiTouchTracker} objeccts
@ivar rawSingleTouchTracker: if this tracker represents a 1-fingered non-plural action then this will be the L{SingleTouchTracker} object for that 1 finger. If not then it is None.
@type rawSingleTouchTracker: L{SingleTouchTracker}
@ivar pluralTimeout: the time at which this tracker could no longer possibly be merged with another to be pluralized, thus it is aloud to be emitted
@type pluralTimeout: float
"""
__slots__=['action','x','y','startTime','endTime','numFingers','actionCount','childTrackers','rawSingleTouchTracker','pluralTimeout']
def __init__(self,action,x,y,startTime,endTime,numFingers=1,actionCount=1,rawSingleTouchTracker=None,pluralTimeout=None):
self.action=action
self.x=x
self.y=y
self.startTime=startTime
self.endTime=endTime
self.numFingers=numFingers
self.actionCount=actionCount
self.childTrackers=[]
self.rawSingleTouchTracker=rawSingleTouchTracker
# We only allow pluralizing of taps, no other action.
if pluralTimeout is None and action==action_tap:
pluralTimeout=startTime+multitouchTimeout
self.pluralTimeout=pluralTimeout
def iterAllRawSingleTouchTrackers(self):
if self.rawSingleTouchTracker: yield self.rawSingleTouchTracker
for child in self.childTrackers:
for i in child.iterAllRawSingleTouchTrackers():
yield i
def __repr__(self):
return "<MultiTouchTracker {numFingers}finger {action} {actionCount} times at position {x},{y}>".format(action=self.action,x=self.x,y=self.y,numFingers=self.numFingers,actionCount=self.actionCount)
def getDevInfoString(self):
msg="%s\n"%self
if self.childTrackers:
msg+="--- made of ---\n"
for t in self.childTrackers:
msg+=t.getDevInfoString()
msg+="--- end ---\n"
return msg
class TrackerManager(object):
"""
Tracks touch input by managing L{SingleTouchTracker} instances and emitting L{MultiTouchTracker} instances representing high-level multiFingered plural trackers.
"""
def __init__(self):
self.singleTouchTrackersByID=OrderedDict()
self.multiTouchTrackers=[]
self.curHoverStack=[]
self.numUnknownTrackers=0
self._lock=threading.Lock()
def makePreheldTrackerFromSingleTouchTrackers(self,trackers):
childTrackers=[MultiTouchTracker(action_hold,tracker.x,tracker.y,tracker.startTime,time.time()) for tracker in trackers if tracker.action==action_hover]
numFingers=len(childTrackers)
if numFingers==0: return
if numFingers==1: return childTrackers[0]
avgX=sum(t.x for t in childTrackers)/numFingers
avgY=sum(t.y for t in childTrackers)/numFingers
tracker=MultiTouchTracker(action_hold,avgX,avgY,childTrackers[0].startTime,time.time(),numFingers)
tracker.childTrackers=childTrackers
return tracker
def makePreheldTrackerForTracker(self,tracker):
curHoverSet={x for x in self.singleTouchTrackersByID.itervalues() if x.action==action_hover}
excludeHoverSet={x for x in tracker.iterAllRawSingleTouchTrackers() if x.action==action_hover}
return self.makePreheldTrackerFromSingleTouchTrackers(curHoverSet-excludeHoverSet)
def update(self,ID,x,y,complete=False):
"""
Called to Alert the multiTouch tracker of a new, moved or completed contact (finger touch).
It creates new single trackers or updates existing ones, and queues/processes multi trackers for later emition.
"""
with self._lock:
#See if we know about this finger
tracker=self.singleTouchTrackersByID.get(ID)
if not tracker:
if not complete:
#This is a new contact (finger) so start tracking it
self.singleTouchTrackersByID[ID]=SingleTouchTracker(ID,x,y)
self.numUnknownTrackers+=1
return
#We already know about this finger
#Update its position and completion status
#But also find out its action before and after the update to decide what to do with it
oldAction=tracker.action
tracker.update(x,y,complete)
newAction=tracker.action
if (oldAction==action_unknown and newAction!=action_unknown):
self.numUnknownTrackers-=1
if complete: #This finger has broken contact
#Forget about this finger
del self.singleTouchTrackersByID[ID]
if tracker.action==action_unknown:
self.numUnknownTrackers-=1
#if the action changed and its not unknown, then we will be queuing it
if newAction!=oldAction and newAction!=action_unknown:
if newAction==action_hover:
#New hovers must be queued as holds
newAction=action_hold
#for most gestures the start coordinates are what we want to emit with trackers
#But hovers should always use their current coordinates
x,y=(tracker.x,tracker.y) if newAction in hoverActions else (tracker.startX,tracker.startY)
#Queue the tracker for processing or emition
self.processAndQueueMultiTouchTracker(MultiTouchTracker(newAction,x,y,tracker.startTime,time.time(),rawSingleTouchTracker=tracker))
def makeMergedTrackerIfPossible(self,oldTracker,newTracker):
if newTracker.action==oldTracker.action and newTracker.startTime<oldTracker.endTime and oldTracker.startTime<newTracker.endTime and oldTracker.actionCount==newTracker.actionCount==1:
#The old and new tracker are the same kind of action, they are not themselves plural actions, and their start and end times overlap
#Therefore they should be treeted as one multiFingered action
childTrackers=[]
childTrackers.extend(oldTracker.childTrackers) if oldTracker.numFingers>1 else childTrackers.append(oldTracker)
childTrackers.extend(newTracker.childTrackers) if newTracker.numFingers>1 else childTrackers.append(newTracker)
numFingers=oldTracker.numFingers+newTracker.numFingers
avgX=sum(t.x for t in childTrackers)/numFingers
avgY=sum(t.y for t in childTrackers)/numFingers
mergedTracker=MultiTouchTracker(newTracker.action,avgX,avgY,oldTracker.startTime,newTracker.endTime,numFingers,newTracker.actionCount,pluralTimeout=newTracker.pluralTimeout)
mergedTracker.childTrackers=childTrackers
elif self.numUnknownTrackers==0 and newTracker.pluralTimeout is not None and newTracker.startTime>=oldTracker.endTime and newTracker.startTime<oldTracker.pluralTimeout and newTracker.action==oldTracker.action and oldTracker.numFingers==newTracker.numFingers:
#The new and old action are the same and allow pluralising and have the same number of fingers and there are no other unknown trackers left and they do not overlap in time
#Therefore they should be treeted as 1 plural action (e.g. double tap)
mergedTracker=MultiTouchTracker(newTracker.action,oldTracker.x,oldTracker.y,oldTracker.startTime,newTracker.endTime,newTracker.numFingers,oldTracker.actionCount+newTracker.actionCount,pluralTimeout=newTracker.pluralTimeout)
mergedTracker.childTrackers.extend(oldTracker.childTrackers) if oldTracker.actionCount>1 else mergedTracker.childTrackers.append(oldTracker)
mergedTracker.childTrackers.extend(newTracker.childTrackers) if newTracker.actionCount>1 else mergedTracker.childTrackers.append(newTracker)
elif self.numUnknownTrackers==0 and newTracker.action==action_hold and oldTracker.action==action_tap and newTracker.numFingers==oldTracker.numFingers and newTracker.startTime>oldTracker.endTime:
#A tap and then a hover down is a tapAndHold
mergedTracker=MultiTouchTracker(action_tapAndHold,oldTracker.x,oldTracker.y,oldTracker.startTime,newTracker.endTime,newTracker.numFingers,oldTracker.actionCount,pluralTimeout=newTracker.pluralTimeout)
mergedTracker.childTrackers.append(oldTracker)
mergedTracker.childTrackers.append(newTracker)
else: #They don't match, go to the next one
return
return mergedTracker
def processAndQueueMultiTouchTracker(self,tracker):
"""Queues the given tracker, replacing old trackers with a multiFingered plural action where possible"""
#Reverse iterate through the existing queued trackers comparing the given tracker to each of them
#as L{emitTrackers} constantly dequeues, the queue only contains trackers newer than multiTouchTimeout, though may contain more if there are still unknown singleTouchTrackers around.
for index in xrange(len(self.multiTouchTrackers)):
index=len(self.multiTouchTrackers)-1-index
delayedTracker=self.multiTouchTrackers[index]
mergedTracker=self.makeMergedTrackerIfPossible(delayedTracker,tracker)
if mergedTracker:
# The trackers were successfully merged
# remove the old one from the queue, and queue the merged one for possible further matching
del self.multiTouchTrackers[index]
self.processAndQueueMultiTouchTracker(mergedTracker)
return
else:
self.multiTouchTrackers.append(tracker)
pendingEmitInterval=None #: If set: how long to wait before calling emitTrackers again as trackers are still in the queue
def emitTrackers(self):
"""
Yields queued trackers that have existed in the queue for long enough to not be connected with other trackers.
A part from a timeout, trackers are also not emitted if there are other fingers touching that still have an unknown action.
If there are no queued trackers to yield but there is a hover tracker, a hover action is yielded instead.
"""
with self._lock:
self.pendingEmitInterval=None
t=time.time()
# yield hover ups for complete hovers from most recent backwards
for singleTouchTracker in list(self.curHoverStack):
if singleTouchTracker.complete:
self.curHoverStack.remove(singleTouchTracker)
tracker=MultiTouchTracker(action_hoverUp,singleTouchTracker.x,singleTouchTracker.y,singleTouchTracker.startTime,time.time())
preheldTracker=self.makePreheldTrackerFromSingleTouchTrackers(self.curHoverStack)
yield preheldTracker,tracker
#Only emit trackers if there are not unknown actions
hasUnknownTrackers=self.numUnknownTrackers
if not hasUnknownTrackers:
for tracker in list(self.multiTouchTrackers):
# isolated holds can be dropped as we only care when they are tapAndHolds (and preheld is handled later)
#All trackers can be emitted with no delay except for tap which must wait for the timeout (to detect plural taps)
trackerTimeout=tracker.pluralTimeout-t if tracker.pluralTimeout is not None else 0
if trackerTimeout<=0:
self.multiTouchTrackers.remove(tracker)
# isolated holds should not be emitted as they are covered by hover downs later
if tracker.action==action_hold:
continue
preheldTracker=self.makePreheldTrackerFromSingleTouchTrackers(self.curHoverStack)
# If this tracker was made up of any new hovers (e.g. a tapAndHold) they should be quietly added to the current hover stack so that hover downs are not produced
for singleTouchTracker in tracker.iterAllRawSingleTouchTrackers():
if singleTouchTracker.action==action_hover and singleTouchTracker not in self.curHoverStack:
self.curHoverStack.append(singleTouchTracker)
yield preheldTracker,tracker
else:
self.pendingEmitInterval=min(self.pendingEmitInterval,trackerTimeout) if self.pendingEmitInterval else trackerTimeout
# yield hover downs for any new hovers
# But only once there are no more trackers in the queue waiting to timeout (E.g. a hold for a tapAndHold)
if len(self.multiTouchTrackers)==0:
for singleTouchTracker in self.singleTouchTrackersByID.itervalues():
if singleTouchTracker.action==action_hover and singleTouchTracker not in self.curHoverStack:
self.curHoverStack.append(singleTouchTracker)
tracker=MultiTouchTracker(action_hoverDown,singleTouchTracker.x,singleTouchTracker.y,singleTouchTracker.startTime,time.time())
preheldTracker=self.makePreheldTrackerFromSingleTouchTrackers(self.curHoverStack[:-1])
yield preheldTracker,tracker
# yield a hover for the most recent hover
if len(self.curHoverStack)>0:
singleTouchTracker=self.curHoverStack[-1]
tracker=MultiTouchTracker(action_hover,singleTouchTracker.x,singleTouchTracker.y,singleTouchTracker.startTime,time.time())
preheldTracker=self.makePreheldTrackerFromSingleTouchTrackers(self.curHoverStack[:-1])
yield preheldTracker,tracker