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