1 /*
2 * Copyright (c) 2012, 2020, 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 static BOOL useEventTap = NO;
46
47
48 @interface GlassTouches (hidden)
49
50 - (void)releaseTouches;
51
52 - (void)terminateImpl;
53
54 - (void)enableTouchInputEventTap;
55
56 - (void)sendJavaTouchEvent:(NSEvent *)theEvent;
57 - (void)notifyTouch:(JNIEnv*)env identity:(const id)identity
58 phase:(NSUInteger)phase
59 pos:(const NSPoint*)pos;
60 @end
61
62
63 static jint getTouchStateFromPhase(NSUInteger phase)
64 {
65 switch (phase)
66 {
67 case NSTouchPhaseBegan:
68 return com_sun_glass_events_TouchEvent_TOUCH_PRESSED;
69 case NSTouchPhaseMoved:
70 return com_sun_glass_events_TouchEvent_TOUCH_MOVED;
71 case NSTouchPhaseStationary:
72 return com_sun_glass_events_TouchEvent_TOUCH_STILL;
73 case NSTouchPhaseEnded:
74 case NSTouchPhaseCancelled:
75 return com_sun_glass_events_TouchEvent_TOUCH_RELEASED;
76 }
77 return 0;
78 }
79
80
81 static BOOL isTouchEnded(NSUInteger phase)
82 {
83 return phase == NSTouchPhaseEnded || phase == NSTouchPhaseCancelled;
84 }
85
86
87 static BOOL hasTouchWithIdentity(const id identity, const NSSet* touchPoints)
88 {
89 for (const NSTouch* touch in touchPoints)
90 {
91 if ([identity isEqual:touch.identity])
92 {
93 return YES;
94 }
95 }
96 return NO;
97 }
98
99
100 typedef struct
101 {
102 jlong touchId;
103 jfloat x;
104 jfloat y;
105 } TouchPoint;
106
107
108 static CGEventRef listenTouchEvents(CGEventTapProxy proxy, CGEventType type,
109 CGEventRef event, void* refcon)
110 {
111 if (type == kCGEventTapDisabledByTimeout ||
112 type == kCGEventTapDisabledByUserInput)
113 {
114 // OS may disable event tap if it handles events too slowly
115 // or for some other reason based on user input.
116 // This is undesirable, so enable event tap after such a reset.
117 [glassTouches enableTouchInputEventTap];
118 LOG("TOUCHES: listenTouchEvents: re-enable event tap, type = %d\n", type);
119 return event;
120 }
121
122 if (type == NSEventTypeGesture)
123 {
124 LOG("TOUCHES: listenTouchEvents: process NSEventTypeGesture\n");
125 NSEvent* theEvent = [NSEvent eventWithCGEvent:event];
126 if (theEvent)
127 {
128 if (glassTouches)
129 {
130 [glassTouches sendJavaTouchEvent:theEvent];
131 }
132 }
133 } else {
134 LOG("TOUCHES: listenTouchEvents: unknown event ignored, type = %d\n", type);
135 }
136
137 return event;
138 }
139
140
141 @implementation GlassTouches
142
143 + (void)startTracking:(GlassViewDelegate *)delegate
144 {
145 if (!glassTouches)
146 {
147 glassTouches = [[GlassTouches alloc] init];
148 }
149
150 if (glassTouches)
151 {
152 glassTouches->curConsumer = delegate;
153 }
154
155 LOG("TOUCHES: startTracking: delegate=%p\n", glassTouches->curConsumer);
156 }
157
158 + (void)stopTracking:(GlassViewDelegate *)delegate
159 {
160 if (!glassTouches || glassTouches->curConsumer != delegate)
161 {
162 return;
163 }
164
165 // Keep updating java touch point counter, just have no view to notify.
166 glassTouches->curConsumer = nil;
167
168 LOG("TOUCHES: stopTracking: delegate=%p\n", glassTouches->curConsumer);
169 }
170
171 + (void)terminate
172 {
173 // Should be called right after Application's run loop terminate
174 [glassTouches terminateImpl];
175 glassTouches = nil;
176 }
177
178 - (id)init
179 {
180 useEventTap = YES;
181 if (@available(macOS 10.15, *)) {
182 useEventTap = NO;
183 }
184
185 self = [super init];
186 if (self != nil)
187 {
188 self->curConsumer = nil;
189 self->eventTap = nil;
190 self->runLoopSource = nil;
191 self->touches = nil;
192 self->lastTouchId = 0;
193
194 if (useEventTap) {
195 //
196 // Notes after fixing RT-23199:
197 //
198 // Don't use NSMachPort and NSRunLoop to integrate CFMachPortRef
199 // instance into run loop.
200 //
201 // Ignoring the above "don't"s results into performance degradation
202 // referenced in the bug.
203 //
204
205 self->eventTap = CGEventTapCreate(kCGHIDEventTap,
206 kCGHeadInsertEventTap,
207 kCGEventTapOptionListenOnly,
208 CGEventMaskBit(NSEventTypeGesture),
209 listenTouchEvents, nil);
210
211 LOG("TOUCHES: eventTap=%p\n", self->eventTap);
212
213 if (self->eventTap)
214 { // Create a run loop source.
215 self->runLoopSource = CFMachPortCreateRunLoopSource(
216 kCFAllocatorDefault,
217 self->eventTap, 0);
218
219 LOG("TOUCHES: runLoopSource=%p\n", self->runLoopSource);
220
221 // Add to the current run loop.
222 CFRunLoopAddSource(CFRunLoopGetCurrent(), self->runLoopSource,
223 kCFRunLoopCommonModes);
224 }
225 }
226 }
227 return self;
228 }
229
230 @end
231
232
233 @implementation GlassTouches (hidden)
234 - (void)terminateImpl
235 {
236 if (useEventTap) {
237 LOG("TOUCHES: terminateImpl eventTap=%p runLoopSource=%p\n", self->eventTap,
238 self->runLoopSource);
239
240 if (self->runLoopSource)
241 {
242 CFRunLoopRemoveSource(CFRunLoopGetCurrent(), self->runLoopSource,
243 kCFRunLoopCommonModes);
244 CFRelease(self->runLoopSource);
245 self->runLoopSource = nil;
246 }
247
248 if (self->eventTap)
249 {
250 CFRelease(self->eventTap);
251 self->eventTap = nil;
252 }
253 }
254 [self releaseTouches];
255 }
256
257 - (void)enableTouchInputEventTap
258 {
259 if (useEventTap) {
260 CGEventTapEnable(self->eventTap, true);
261 }
262 }
263
264 - (void)sendJavaTouchEvent:(NSEvent *)theEvent
265 {
266 jint modifiers = GetJavaModifiers(theEvent);
267
268 const NSSet* touchPoints =
269 [theEvent touchesMatchingPhase:NSTouchPhaseAny inView:nil];
270
271 //
272 // Known issues with OSX touch input:
273 // - multiple 'NSTouchPhaseBegan' for the same touch point;
274 // - missing 'NSTouchPhaseEnded' for released touch points
275 // (RT-20139, RT-20375);
276 //
277
278 //
279 // Find just released touch points that are not in the cache already.
280 // Don't send TouchEvent#TOUCH_RELEASED for these touch points.
281 //
282 jint noReleaseTouchPointCount = 0;
283 for (NSTouch* touch in touchPoints)
284 {
285 NSUInteger phase = touch.phase;
286 BOOL isPhaseEnded = isTouchEnded(phase);
287
288 if (!isPhaseEnded)
289 {
290 continue;
291 }
292
293 if (self->touches == nil ||
294 [self->touches objectForKey:touch.identity] == nil)
295 {
296 ++noReleaseTouchPointCount;
297 }
298 }
299
300 //
301 // Find cached touch points that are not in the curent set of touch points.
302 // Should send TouchEvent#TOUCH_RELEASED for these touch points.
303 //
304 NSMutableArray* releaseTouchIds = nil;
305 if (self->touches != nil)
306 {
307 for (id identity in self->touches)
308 {
309 if (!hasTouchWithIdentity(identity, touchPoints))
310 {
311 if (!releaseTouchIds)
312 {
313 releaseTouchIds = [NSMutableArray array];
314 }
315 [releaseTouchIds addObject:identity];
316 }
317 }
318 }
319
320 const jint touchPointCount =
321 (jint)touchPoints.count
322 - (jint)noReleaseTouchPointCount + (jint)(releaseTouchIds == nil ? 0 : releaseTouchIds.count);
323 if (!touchPointCount)
324 {
325 return;
326 }
327
328 GET_MAIN_JENV;
329 const jclass jGestureSupportClass = [GlassHelper ClassForName:"com.sun.glass.ui.mac.MacGestureSupport"
330 withEnv:env];
331 if (jGestureSupportClass)
332 {
333 (*env)->CallStaticVoidMethod(env, jGestureSupportClass,
334 javaIDs.GestureSupport.notifyBeginTouchEvent,
335 [self->curConsumer jView], modifiers,
336 touchPointCount);
337 }
338 GLASS_CHECK_EXCEPTION(env);
339
340 if (self->touches == nil && touchPointCount)
341 {
342 self->touches = [[NSMutableDictionary alloc] init];
343 }
344
345 if (releaseTouchIds != nil)
346 {
347 for (id identity in releaseTouchIds)
348 {
349 [self notifyTouch:env
350 identity:identity
351 phase:NSTouchPhaseEnded
352 pos:nil];
353 }
354 }
355
356 for (NSTouch* touch in touchPoints)
357 {
358 if (![touch respondsToSelector:@selector(type)]
359 || (NSInteger) [touch performSelector:@selector(type)] == 1 /* NSTouchTypeIndirect */) {
360
361 const NSPoint pos = touch.normalizedPosition;
362 [self notifyTouch:env
363 identity:touch.identity
364 phase:touch.phase
365 pos:&pos];
366 }
367 }
368
369 if (jGestureSupportClass)
370 {
371 (*env)->CallStaticVoidMethod(env, jGestureSupportClass,
372 javaIDs.GestureSupport.notifyEndTouchEvent,
373 [self->curConsumer jView]);
374 }
375 GLASS_CHECK_EXCEPTION(env);
376
377 if ([self->touches count] == 0)
378 {
379 [self releaseTouches];
380 self->lastTouchId = 0;
381 }
382 }
383
384 - (void)notifyTouch:(JNIEnv*)env identity:(const id)identity phase:(NSUInteger)phase
385 pos:(const NSPoint*)pos;
386 {
387 const BOOL isPhaseEnded = isTouchEnded(phase);
388
389 TouchPoint tp;
390 NSValue* ctnr = [self->touches objectForKey:identity];
391 if (ctnr == nil)
392 {
393 if (isPhaseEnded)
394 {
395 return;
396 }
397 tp.touchId = ++(self->lastTouchId);
398
399 if (phase != NSTouchPhaseBegan)
400 { // Adjust 'phase'. By some reason OS X sometimes doesn't send
401 // 'NSTouchPhaseBegan' for the just appeared touch point.
402 phase = NSTouchPhaseBegan;
403 }
404 }
405 else
406 {
407 [ctnr getValue:&tp];
408
409 if (phase == NSTouchPhaseBegan)
410 { // Adjust 'phase'. This is needed as OS X sometimes sends
411 // multiple 'NSTouchPhaseBegan' for the same touch point.
412 phase = NSTouchPhaseStationary;
413 }
414 }
415
416 if (pos)
417 { // update stored position
418 tp.x = (jfloat)pos->x;
419 tp.y = (jfloat)pos->y;
420 }
421
422 if (isPhaseEnded)
423 {
424 [self->touches removeObjectForKey:identity];
425 }
426 else
427 {
428 ctnr = [NSValue valueWithBytes:&tp objCType:@encode(TouchPoint)];
429 [self->touches setObject:ctnr forKey:identity];
430 }
431
432 const jclass jGestureSupportClass = [GlassHelper ClassForName:"com.sun.glass.ui.mac.MacGestureSupport"
433 withEnv:env];
434 if (jGestureSupportClass)
435 {
436 (*env)->CallStaticVoidMethod(env, jGestureSupportClass,
437 javaIDs.GestureSupport.notifyNextTouchEvent,
438 [self->curConsumer jView],
439 getTouchStateFromPhase(phase),
440 tp.touchId, tp.x, tp.y);
441 }
442 GLASS_CHECK_EXCEPTION(env);
443 }
444
445 - (void)releaseTouches
446 {
447 [self->touches release];
448 self->touches = nil;
449 }
450
451 @end