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