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