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