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
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 BOOL useEventTap = YES;
180 if (@available(macOS 10.15, *)) {
181 useEventTap = NO;
182 }
183
184 self = [super init];
185 if (self != nil)
186 {
187 self->curConsumer = nil;
188 self->eventTap = nil;
189 self->runLoopSource = nil;
190 self->touches = nil;
191 self->lastTouchId = 0;
192
193 if (useEventTap) {
194 //
195 // Notes after fixing RT-23199:
196 //
197 // Don't use NSMachPort and NSRunLoop to integrate CFMachPortRef
198 // instance into run loop.
199 //
200 // Ignoring the above "don't"s results into performance degradation
201 // referenced in the bug.
202 //
203
204 self->eventTap = CGEventTapCreate(kCGHIDEventTap,
205 kCGHeadInsertEventTap,
206 kCGEventTapOptionListenOnly,
207 CGEventMaskBit(NSEventTypeGesture),
208 listenTouchEvents, nil);
209
210 LOG("TOUCHES: eventTap=%p\n", self->eventTap);
211
212 if (self->eventTap)
213 { // Create a run loop source.
214 self->runLoopSource = CFMachPortCreateRunLoopSource(
215 kCFAllocatorDefault,
216 self->eventTap, 0);
217
218 LOG("TOUCHES: runLoopSource=%p\n", self->runLoopSource);
219
220 // Add to the current run loop.
221 CFRunLoopAddSource(CFRunLoopGetCurrent(), self->runLoopSource,
222 kCFRunLoopCommonModes);
223 }
224 }
225 }
226 return self;
227 }
228
229 @end
230
231
232 @implementation GlassTouches (hidden)
233 - (void)terminateImpl
234 {
235 BOOL useEventTap = YES;
236 if (@available(macOS 10.15, *)) {
237 useEventTap = NO;
238 }
239
240 if (useEventTap) {
241 LOG("TOUCHES: terminateImpl eventTap=%p runLoopSource=%p\n", self->eventTap,
242 self->runLoopSource);
243
244 if (self->runLoopSource)
245 {
246 CFRunLoopRemoveSource(CFRunLoopGetCurrent(), self->runLoopSource,
247 kCFRunLoopCommonModes);
248 CFRelease(self->runLoopSource);
249 self->runLoopSource = nil;
250 }
251
252 if (self->eventTap)
253 {
254 CFRelease(self->eventTap);
255 self->eventTap = nil;
256 }
257 }
258 [self releaseTouches];
259 }
260
261 - (void)enableTouchInputEventTap
262 {
263 BOOL useEventTap = YES;
264 if (@available(macOS 10.15, *)) {
265 useEventTap = NO;
266 }
267
268 if (useEventTap) {
269 CGEventTapEnable(self->eventTap, true);
270 }
271 }
272
273 - (void)sendJavaTouchEvent:(NSEvent *)theEvent
274 {
275 jint modifiers = GetJavaModifiers(theEvent);
276
277 const NSSet* touchPoints =
278 [theEvent touchesMatchingPhase:NSTouchPhaseAny inView:nil];
279
280 //
281 // Known issues with OSX touch input:
282 // - multiple 'NSTouchPhaseBegan' for the same touch point;
283 // - missing 'NSTouchPhaseEnded' for released touch points
284 // (RT-20139, RT-20375);
285 //
286
287 //
288 // Find just released touch points that are not in the cache already.
289 // Don't send TouchEvent#TOUCH_RELEASED for these touch points.
290 //
291 jint noReleaseTouchPointCount = 0;
292 for (NSTouch* touch in touchPoints)
293 {
294 NSUInteger phase = touch.phase;
295 BOOL isPhaseEnded = isTouchEnded(phase);
296
297 if (!isPhaseEnded)
298 {
299 continue;
300 }
301
302 if (self->touches == nil ||
303 [self->touches objectForKey:touch.identity] == nil)
304 {
305 ++noReleaseTouchPointCount;
306 }
307 }
308
309 //
310 // Find cached touch points that are not in the curent set of touch points.
311 // Should send TouchEvent#TOUCH_RELEASED for these touch points.
312 //
313 NSMutableArray* releaseTouchIds = nil;
314 if (self->touches != nil)
315 {
316 for (id identity in self->touches)
317 {
318 if (!hasTouchWithIdentity(identity, touchPoints))
319 {
320 if (!releaseTouchIds)
321 {
322 releaseTouchIds = [NSMutableArray array];
323 }
324 [releaseTouchIds addObject:identity];
325 }
326 }
327 }
328
329 const jint touchPointCount =
330 (jint)touchPoints.count
331 - (jint)noReleaseTouchPointCount + (jint)(releaseTouchIds == nil ? 0 : releaseTouchIds.count);
332 if (!touchPointCount)
333 {
334 return;
335 }
336
337 GET_MAIN_JENV;
338 const jclass jGestureSupportClass = [GlassHelper ClassForName:"com.sun.glass.ui.mac.MacGestureSupport"
339 withEnv:env];
340 if (jGestureSupportClass)
341 {
342 (*env)->CallStaticVoidMethod(env, jGestureSupportClass,
343 javaIDs.GestureSupport.notifyBeginTouchEvent,
344 [self->curConsumer jView], modifiers,
345 touchPointCount);
346 }
347 GLASS_CHECK_EXCEPTION(env);
348
349 if (self->touches == nil && touchPointCount)
350 {
351 self->touches = [[NSMutableDictionary alloc] init];
352 }
353
354 if (releaseTouchIds != nil)
355 {
356 for (id identity in releaseTouchIds)
357 {
358 [self notifyTouch:env
359 identity:identity
360 phase:NSTouchPhaseEnded
361 pos:nil];
362 }
363 }
364
365 for (NSTouch* touch in touchPoints)
366 {
367 if (![touch respondsToSelector:@selector(type)]
368 || (NSInteger) [touch performSelector:@selector(type)] == 1 /* NSTouchTypeIndirect */) {
369
370 const NSPoint pos = touch.normalizedPosition;
371 [self notifyTouch:env
372 identity:touch.identity
373 phase:touch.phase
374 pos:&pos];
375 }
376 }
377
378 if (jGestureSupportClass)
379 {
380 (*env)->CallStaticVoidMethod(env, jGestureSupportClass,
381 javaIDs.GestureSupport.notifyEndTouchEvent,
382 [self->curConsumer jView]);
383 }
384 GLASS_CHECK_EXCEPTION(env);
385
386 if ([self->touches count] == 0)
387 {
388 [self releaseTouches];
389 self->lastTouchId = 0;
390 }
391 }
392
393 - (void)notifyTouch:(JNIEnv*)env identity:(const id)identity phase:(NSUInteger)phase
394 pos:(const NSPoint*)pos;
395 {
396 const BOOL isPhaseEnded = isTouchEnded(phase);
397
398 TouchPoint tp;
399 NSValue* ctnr = [self->touches objectForKey:identity];
400 if (ctnr == nil)
401 {
402 if (isPhaseEnded)
403 {
404 return;
405 }
406 tp.touchId = ++(self->lastTouchId);
407
408 if (phase != NSTouchPhaseBegan)
409 { // Adjust 'phase'. By some reason OS X sometimes doesn't send
410 // 'NSTouchPhaseBegan' for the just appeared touch point.
411 phase = NSTouchPhaseBegan;
412 }
413 }
414 else
415 {
416 [ctnr getValue:&tp];
417
418 if (phase == NSTouchPhaseBegan)
419 { // Adjust 'phase'. This is needed as OS X sometimes sends
420 // multiple 'NSTouchPhaseBegan' for the same touch point.
421 phase = NSTouchPhaseStationary;
422 }
423 }
424
425 if (pos)
426 { // update stored position
427 tp.x = (jfloat)pos->x;
428 tp.y = (jfloat)pos->y;
429 }
430
431 if (isPhaseEnded)
432 {
433 [self->touches removeObjectForKey:identity];
434 }
435 else
436 {
437 ctnr = [NSValue valueWithBytes:&tp objCType:@encode(TouchPoint)];
438 [self->touches setObject:ctnr forKey:identity];
439 }
440
441 const jclass jGestureSupportClass = [GlassHelper ClassForName:"com.sun.glass.ui.mac.MacGestureSupport"
442 withEnv:env];
443 if (jGestureSupportClass)
444 {
445 (*env)->CallStaticVoidMethod(env, jGestureSupportClass,
446 javaIDs.GestureSupport.notifyNextTouchEvent,
447 [self->curConsumer jView],
448 getTouchStateFromPhase(phase),
449 tp.touchId, tp.x, tp.y);
450 }
451 GLASS_CHECK_EXCEPTION(env);
452 }
453
454 - (void)releaseTouches
455 {
456 [self->touches release];
457 self->touches = nil;
458 }
459
460 @end