1 /*
2 * Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26 #import "common.h"
27 #import "com_sun_glass_events_TouchEvent.h"
28
29 #import "GlassMacros.h"
30 #import "GlassTouches.h"
31 #import "GlassKey.h"
32 #import "GlassHelper.h"
33 #import "GlassStatics.h"
34
35
36 //#define VERBOSE
37 #ifndef VERBOSE
38 #define LOG(MSG, ...)
39 #else
40 #define LOG(MSG, ...) GLASS_LOG(MSG, ## __VA_ARGS__);
41 #endif
42
43
44 static GlassTouches* glassTouches = nil;
45
46
47 @interface GlassTouches (hidden)
48
49 - (void)releaseTouches;
50
51 - (void)terminateImpl;
52
53 - (void)enableTouchInputEventTap;
54
55 - (void)sendJavaTouchEvent:(NSEvent *)theEvent;
56 - (void)notifyTouch:(JNIEnv*)env identity:(const id)identity
57 phase:(NSUInteger)phase
58 pos:(const NSPoint*)pos;
59 @end
60
61
62 static jint getTouchStateFromPhase(NSUInteger phase)
63 {
64 switch (phase)
65 {
66 case NSTouchPhaseBegan:
67 return com_sun_glass_events_TouchEvent_TOUCH_PRESSED;
68 case NSTouchPhaseMoved:
69 return com_sun_glass_events_TouchEvent_TOUCH_MOVED;
70 case NSTouchPhaseStationary:
71 return com_sun_glass_events_TouchEvent_TOUCH_STILL;
72 case NSTouchPhaseEnded:
73 case NSTouchPhaseCancelled:
74 return com_sun_glass_events_TouchEvent_TOUCH_RELEASED;
75 }
76 return 0;
77 }
78
79
80 static BOOL isTouchEnded(NSUInteger phase)
81 {
82 return phase == NSTouchPhaseEnded || phase == NSTouchPhaseCancelled;
83 }
84
85
86 static BOOL hasTouchWithIdentity(const id identity, const NSSet* touchPoints)
87 {
88 for (const NSTouch* touch in touchPoints)
89 {
90 if ([identity isEqual:touch.identity])
91 {
92 return YES;
93 }
94 }
95 return NO;
96 }
97
98
99 typedef struct
100 {
101 jlong touchId;
102 jfloat x;
103 jfloat y;
104 } TouchPoint;
105
106
107 static CGEventRef listenTouchEvents(CGEventTapProxy proxy, CGEventType type,
108 CGEventRef event, void* refcon)
109 {
110 if (type == kCGEventTapDisabledByTimeout ||
111 type == kCGEventTapDisabledByUserInput)
112 {
113 // OS may disable event tap if it handles events too slowly
114 // or for some other reason based on user input.
115 // This is undesirable, so enable event tap after such a reset.
116 [glassTouches enableTouchInputEventTap];
117 LOG("TOUCHES: listenTouchEvents: re-enable event tap, type = %d\n", type);
118 return event;
119 }
120
121 if (type == NSEventTypeGesture)
122 {
123 LOG("TOUCHES: listenTouchEvents: process NSEventTypeGesture\n");
124 NSEvent* theEvent = [NSEvent eventWithCGEvent:event];
125 if (theEvent)
126 {
127 if (glassTouches)
128 {
129 [glassTouches sendJavaTouchEvent:theEvent];
130 }
131 }
132 } else {
133 LOG("TOUCHES: listenTouchEvents: unknown event ignored, type = %d\n", type);
134 }
135
136 return event;
137 }
138
139
140 @implementation GlassTouches
141
142 + (void)startTracking:(GlassViewDelegate *)delegate
143 {
144 if (!glassTouches)
145 {
146 glassTouches = [[GlassTouches alloc] init];
147 }
148
149 if (glassTouches)
150 {
151 glassTouches->curConsumer = delegate;
152 }
153
154 LOG("TOUCHES: startTracking: delegate=%p\n", glassTouches->curConsumer);
155 }
156
157 + (void)stopTracking:(GlassViewDelegate *)delegate
158 {
159 if (!glassTouches || glassTouches->curConsumer != delegate)
160 {
161 return;
162 }
163
164 // Keep updating java touch point counter, just have no view to notify.
165 glassTouches->curConsumer = nil;
166
167 LOG("TOUCHES: stopTracking: delegate=%p\n", glassTouches->curConsumer);
168 }
169
170 + (void)terminate
171 {
172 // Should be called right after Application's run loop terminate
173 [glassTouches terminateImpl];
174 glassTouches = nil;
175 }
176
177 - (id)init
178 {
179 self = [super init];
180 if (self != nil)
181 {
182 self->curConsumer = nil;
183 self->eventTap = nil;
184 self->runLoopSource = nil;
185 self->touches = nil;
186 self->lastTouchId = 0;
187
188 //
189 // Notes after fixing RT-23199:
190 //
191 // Don't use NSMachPort and NSRunLoop to integrate CFMachPortRef
192 // instance into run loop.
193 //
194 // Ignoring the above "don't"s results into performance degradation
195 // referenced in the bug.
196 //
197
198 self->eventTap = CGEventTapCreate(kCGHIDEventTap,
199 kCGHeadInsertEventTap,
200 kCGEventTapOptionListenOnly,
201 CGEventMaskBit(NSEventTypeGesture),
202 listenTouchEvents, nil);
203
204 LOG("TOUCHES: eventTap=%p\n", self->eventTap);
205
206 if (self->eventTap)
207 { // Create a run loop source.
208 self->runLoopSource = CFMachPortCreateRunLoopSource(
209 kCFAllocatorDefault,
210 self->eventTap, 0);
211
212 LOG("TOUCHES: runLoopSource=%p\n", self->runLoopSource);
213
214 // Add to the current run loop.
215 CFRunLoopAddSource(CFRunLoopGetCurrent(), self->runLoopSource,
216 kCFRunLoopCommonModes);
217 }
218 }
219 return self;
220 }
221
222 @end
223
224
225 @implementation GlassTouches (hidden)
226 - (void)terminateImpl
227 {
228 LOG("TOUCHES: terminateImpl eventTap=%p runLoopSource=%p\n", self->eventTap,
229 self->runLoopSource);
230
231 if (self->runLoopSource)
232 {
233 CFRunLoopRemoveSource(CFRunLoopGetCurrent(), self->runLoopSource,
234 kCFRunLoopCommonModes);
235 CFRelease(self->runLoopSource);
236 self->runLoopSource = nil;
237 }
238
239 if (self->eventTap)
240 {
241 CFRelease(self->eventTap);
242 self->eventTap = nil;
243 }
244
245 [self releaseTouches];
246 }
247
248 - (void)enableTouchInputEventTap
249 {
250 CGEventTapEnable(self->eventTap, true);
251 }
252
253 - (void)sendJavaTouchEvent:(NSEvent *)theEvent
254 {
255 jint modifiers = GetJavaModifiers(theEvent);
256
257 const NSSet* touchPoints =
258 [theEvent touchesMatchingPhase:NSTouchPhaseAny inView:nil];
259
260 //
261 // Known issues with OSX touch input:
262 // - multiple 'NSTouchPhaseBegan' for the same touch point;
263 // - missing 'NSTouchPhaseEnded' for released touch points
264 // (RT-20139, RT-20375);
265 //
266
267 //
268 // Find just released touch points that are not in the cache already.
269 // Don't send TouchEvent#TOUCH_RELEASED for these touch points.
270 //
271 jint noReleaseTouchPointCount = 0;
272 for (NSTouch* touch in touchPoints)
273 {
274 NSUInteger phase = touch.phase;
275 BOOL isPhaseEnded = isTouchEnded(phase);
276
277 if (!isPhaseEnded)
278 {
279 continue;
280 }
281
282 if (self->touches == nil ||
283 [self->touches objectForKey:touch.identity] == nil)
284 {
285 ++noReleaseTouchPointCount;
286 }
287 }
288
289 //
290 // Find cached touch points that are not in the curent set of touch points.
291 // Should send TouchEvent#TOUCH_RELEASED for these touch points.
292 //
293 NSMutableArray* releaseTouchIds = nil;
294 if (self->touches != nil)
295 {
296 for (id identity in self->touches)
297 {
298 if (!hasTouchWithIdentity(identity, touchPoints))
299 {
300 if (!releaseTouchIds)
301 {
302 releaseTouchIds = [NSMutableArray array];
303 }
304 [releaseTouchIds addObject:identity];
305 }
306 }
307 }
308
309 const jint touchPointCount =
310 (jint)touchPoints.count
311 - (jint)noReleaseTouchPointCount + (jint)(releaseTouchIds == nil ? 0 : releaseTouchIds.count);
312 if (!touchPointCount)
313 {
314 return;
315 }
316
317 GET_MAIN_JENV;
318 const jclass jGestureSupportClass = [GlassHelper ClassForName:"com.sun.glass.ui.mac.MacGestureSupport"
319 withEnv:env];
320 if (jGestureSupportClass)
321 {
322 (*env)->CallStaticVoidMethod(env, jGestureSupportClass,
323 javaIDs.GestureSupport.notifyBeginTouchEvent,
324 [self->curConsumer jView], modifiers,
325 touchPointCount);
326 }
327 GLASS_CHECK_EXCEPTION(env);
328
329 if (self->touches == nil && touchPointCount)
330 {
331 self->touches = [[NSMutableDictionary alloc] init];
332 }
333
334 if (releaseTouchIds != nil)
335 {
336 for (id identity in releaseTouchIds)
337 {
338 [self notifyTouch:env
339 identity:identity
340 phase:NSTouchPhaseEnded
341 pos:nil];
342 }
343 }
344
345 for (NSTouch* touch in touchPoints)
346 {
347 if (![touch respondsToSelector:@selector(type)]
348 || (NSInteger) [touch performSelector:@selector(type)] == 1 /* NSTouchTypeIndirect */) {
349
350 const NSPoint pos = touch.normalizedPosition;
351 [self notifyTouch:env
352 identity:touch.identity
353 phase:touch.phase
354 pos:&pos];
355 }
356 }
357
358 if (jGestureSupportClass)
359 {
360 (*env)->CallStaticVoidMethod(env, jGestureSupportClass,
361 javaIDs.GestureSupport.notifyEndTouchEvent,
362 [self->curConsumer jView]);
363 }
364 GLASS_CHECK_EXCEPTION(env);
365
366 if ([self->touches count] == 0)
367 {
368 [self releaseTouches];
369 self->lastTouchId = 0;
370 }
371 }
372
373 - (void)notifyTouch:(JNIEnv*)env identity:(const id)identity phase:(NSUInteger)phase
374 pos:(const NSPoint*)pos;
375 {
376 const BOOL isPhaseEnded = isTouchEnded(phase);
377
378 TouchPoint tp;
379 NSValue* ctnr = [self->touches objectForKey:identity];
380 if (ctnr == nil)
381 {
382 if (isPhaseEnded)
383 {
384 return;
385 }
386 tp.touchId = ++(self->lastTouchId);
387
388 if (phase != NSTouchPhaseBegan)
389 { // Adjust 'phase'. By some reason OS X sometimes doesn't send
390 // 'NSTouchPhaseBegan' for the just appeared touch point.
391 phase = NSTouchPhaseBegan;
392 }
393 }
394 else
395 {
396 [ctnr getValue:&tp];
397
398 if (phase == NSTouchPhaseBegan)
399 { // Adjust 'phase'. This is needed as OS X sometimes sends
400 // multiple 'NSTouchPhaseBegan' for the same touch point.
401 phase = NSTouchPhaseStationary;
402 }
403 }
404
405 if (pos)
406 { // update stored position
407 tp.x = (jfloat)pos->x;
408 tp.y = (jfloat)pos->y;
409 }
410
411 if (isPhaseEnded)
412 {
413 [self->touches removeObjectForKey:identity];
414 }
415 else
416 {
417 ctnr = [NSValue valueWithBytes:&tp objCType:@encode(TouchPoint)];
418 [self->touches setObject:ctnr forKey:identity];
419 }
420
421 const jclass jGestureSupportClass = [GlassHelper ClassForName:"com.sun.glass.ui.mac.MacGestureSupport"
422 withEnv:env];
423 if (jGestureSupportClass)
424 {
425 (*env)->CallStaticVoidMethod(env, jGestureSupportClass,
426 javaIDs.GestureSupport.notifyNextTouchEvent,
427 [self->curConsumer jView],
428 getTouchStateFromPhase(phase),
429 tp.touchId, tp.x, tp.y);
430 }
431 GLASS_CHECK_EXCEPTION(env);
432 }
433
434 - (void)releaseTouches
435 {
436 [self->touches release];
437 self->touches = nil;
438 }
439
440 @end