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