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