SDL_cocoamousetap.m (9004B)
1/* 2 Simple DirectMedia Layer 3 Copyright (C) 1997-2014 Sam Lantinga <slouken@libsdl.org> 4 5 This software is provided 'as-is', without any express or implied 6 warranty. In no event will the authors be held liable for any damages 7 arising from the use of this software. 8 9 Permission is granted to anyone to use this software for any purpose, 10 including commercial applications, and to alter it and redistribute it 11 freely, subject to the following restrictions: 12 13 1. The origin of this software must not be misrepresented; you must not 14 claim that you wrote the original software. If you use this software 15 in a product, an acknowledgment in the product documentation would be 16 appreciated but is not required. 17 2. Altered source versions must be plainly marked as such, and must not be 18 misrepresented as being the original software. 19 3. This notice may not be removed or altered from any source distribution. 20*/ 21#include "../../SDL_internal.h" 22 23#if SDL_VIDEO_DRIVER_COCOA 24 25#include "SDL_cocoamousetap.h" 26 27/* Event taps are forbidden in the Mac App Store, so we can only enable this 28 * code if your app doesn't need to ship through the app store. 29 * This code makes it so that a grabbed cursor cannot "leak" a mouse click 30 * past the edge of the window if moving the cursor too fast. 31 */ 32#if SDL_MAC_NO_SANDBOX 33 34#include "SDL_keyboard.h" 35#include "SDL_thread.h" 36#include "SDL_cocoavideo.h" 37 38#include "../../events/SDL_mouse_c.h" 39 40typedef struct { 41 CFMachPortRef tap; 42 CFRunLoopRef runloop; 43 CFRunLoopSourceRef runloopSource; 44 SDL_Thread *thread; 45 SDL_sem *runloopStartedSemaphore; 46} SDL_MouseEventTapData; 47 48static const CGEventMask movementEventsMask = 49 CGEventMaskBit(kCGEventLeftMouseDragged) 50 | CGEventMaskBit(kCGEventRightMouseDragged) 51 | CGEventMaskBit(kCGEventMouseMoved); 52 53static const CGEventMask allGrabbedEventsMask = 54 CGEventMaskBit(kCGEventLeftMouseDown) | CGEventMaskBit(kCGEventLeftMouseUp) 55 | CGEventMaskBit(kCGEventRightMouseDown) | CGEventMaskBit(kCGEventRightMouseUp) 56 | CGEventMaskBit(kCGEventOtherMouseDown) | CGEventMaskBit(kCGEventOtherMouseUp) 57 | CGEventMaskBit(kCGEventLeftMouseDragged) | CGEventMaskBit(kCGEventRightMouseDragged) 58 | CGEventMaskBit(kCGEventMouseMoved); 59 60static CGEventRef 61Cocoa_MouseTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) 62{ 63 SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)refcon; 64 SDL_Mouse *mouse = SDL_GetMouse(); 65 SDL_Window *window = SDL_GetKeyboardFocus(); 66 NSWindow *nswindow; 67 NSRect windowRect; 68 CGPoint eventLocation; 69 70 switch (type) { 71 case kCGEventTapDisabledByTimeout: 72 case kCGEventTapDisabledByUserInput: 73 { 74 CGEventTapEnable(tapdata->tap, true); 75 return NULL; 76 } 77 default: 78 break; 79 } 80 81 82 if (!window || !mouse) { 83 return event; 84 } 85 86 if (mouse->relative_mode) { 87 return event; 88 } 89 90 if (!(window->flags & SDL_WINDOW_INPUT_GRABBED)) { 91 return event; 92 } 93 94 /* This is the same coordinate system as Cocoa uses. */ 95 nswindow = ((SDL_WindowData *) window->driverdata)->nswindow; 96 eventLocation = CGEventGetUnflippedLocation(event); 97 windowRect = [nswindow contentRectForFrameRect:[nswindow frame]]; 98 99 if (!NSPointInRect(NSPointFromCGPoint(eventLocation), windowRect)) { 100 101 /* This is in CGs global screenspace coordinate system, which has a 102 * flipped Y. 103 */ 104 CGPoint newLocation = CGEventGetLocation(event); 105 106 if (eventLocation.x < NSMinX(windowRect)) { 107 newLocation.x = NSMinX(windowRect); 108 } else if (eventLocation.x >= NSMaxX(windowRect)) { 109 newLocation.x = NSMaxX(windowRect) - 1.0; 110 } 111 112 if (eventLocation.y < NSMinY(windowRect)) { 113 newLocation.y -= (NSMinY(windowRect) - eventLocation.y + 1); 114 } else if (eventLocation.y >= NSMaxY(windowRect)) { 115 newLocation.y += (eventLocation.y - NSMaxY(windowRect) + 1); 116 } 117 118 CGSetLocalEventsSuppressionInterval(0); 119 CGWarpMouseCursorPosition(newLocation); 120 CGSetLocalEventsSuppressionInterval(0.25); 121 122 if ((CGEventMaskBit(type) & movementEventsMask) == 0) { 123 /* For click events, we just constrain the event to the window, so 124 * no other app receives the click event. We can't due the same to 125 * movement events, since they mean that our warp cursor above 126 * behaves strangely. 127 */ 128 CGEventSetLocation(event, newLocation); 129 } 130 } 131 132 return event; 133} 134 135static void 136SemaphorePostCallback(CFRunLoopTimerRef timer, void *info) 137{ 138 SDL_SemPost((SDL_sem*)info); 139} 140 141static int 142Cocoa_MouseTapThread(void *data) 143{ 144 SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)data; 145 146 /* Create a tap. */ 147 CFMachPortRef eventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, 148 kCGEventTapOptionDefault, allGrabbedEventsMask, 149 &Cocoa_MouseTapCallback, tapdata); 150 if (eventTap) { 151 /* Try to create a runloop source we can schedule. */ 152 CFRunLoopSourceRef runloopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0); 153 if (runloopSource) { 154 tapdata->tap = eventTap; 155 tapdata->runloopSource = runloopSource; 156 } else { 157 CFRelease(eventTap); 158 SDL_SemPost(tapdata->runloopStartedSemaphore); 159 /* TODO: Both here and in the return below, set some state in 160 * tapdata to indicate that initialization failed, which we should 161 * check in InitMouseEventTap, after we move the semaphore check 162 * from Quit to Init. 163 */ 164 return 1; 165 } 166 } else { 167 SDL_SemPost(tapdata->runloopStartedSemaphore); 168 return 1; 169 } 170 171 tapdata->runloop = CFRunLoopGetCurrent(); 172 CFRunLoopAddSource(tapdata->runloop, tapdata->runloopSource, kCFRunLoopCommonModes); 173 CFRunLoopTimerContext context = {.info = tapdata->runloopStartedSemaphore}; 174 /* We signal the runloop started semaphore *after* the run loop has started, indicating it's safe to CFRunLoopStop it. */ 175 CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent(), 0, 0, 0, &SemaphorePostCallback, &context); 176 CFRunLoopAddTimer(tapdata->runloop, timer, kCFRunLoopCommonModes); 177 CFRelease(timer); 178 179 /* Run the event loop to handle events in the event tap. */ 180 CFRunLoopRun(); 181 /* Make sure this is signaled so that SDL_QuitMouseEventTap knows it can safely SDL_WaitThread for us. */ 182 if (SDL_SemValue(tapdata->runloopStartedSemaphore) < 1) { 183 SDL_SemPost(tapdata->runloopStartedSemaphore); 184 } 185 CFRunLoopRemoveSource(tapdata->runloop, tapdata->runloopSource, kCFRunLoopCommonModes); 186 187 /* Clean up. */ 188 CGEventTapEnable(tapdata->tap, false); 189 CFRelease(tapdata->runloopSource); 190 CFRelease(tapdata->tap); 191 tapdata->runloopSource = NULL; 192 tapdata->tap = NULL; 193 194 return 0; 195} 196 197void 198Cocoa_InitMouseEventTap(SDL_MouseData* driverdata) 199{ 200 SDL_MouseEventTapData *tapdata; 201 driverdata->tapdata = SDL_calloc(1, sizeof(SDL_MouseEventTapData)); 202 tapdata = (SDL_MouseEventTapData*)driverdata->tapdata; 203 204 tapdata->runloopStartedSemaphore = SDL_CreateSemaphore(0); 205 if (tapdata->runloopStartedSemaphore) { 206 tapdata->thread = SDL_CreateThread(&Cocoa_MouseTapThread, "Event Tap Loop", tapdata); 207 if (!tapdata->thread) { 208 SDL_DestroySemaphore(tapdata->runloopStartedSemaphore); 209 } 210 } 211 212 if (!tapdata->thread) { 213 SDL_free(driverdata->tapdata); 214 driverdata->tapdata = NULL; 215 } 216} 217 218void 219Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata) 220{ 221 SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)driverdata->tapdata; 222 int status; 223 224 /* Ensure that the runloop has been started first. 225 * TODO: Move this to InitMouseEventTap, check for error conditions that can 226 * happen in Cocoa_MouseTapThread, and fall back to the non-EventTap way of 227 * grabbing the mouse if it fails to Init. 228 */ 229 status = SDL_SemWaitTimeout(tapdata->runloopStartedSemaphore, 5000); 230 if (status > -1) { 231 /* Then stop it, which will cause Cocoa_MouseTapThread to return. */ 232 CFRunLoopStop(tapdata->runloop); 233 /* And then wait for Cocoa_MouseTapThread to finish cleaning up. It 234 * releases some of the pointers in tapdata. */ 235 SDL_WaitThread(tapdata->thread, &status); 236 } 237 238 SDL_free(driverdata->tapdata); 239 driverdata->tapdata = NULL; 240} 241 242#else /* SDL_MAC_NO_SANDBOX */ 243 244void 245Cocoa_InitMouseEventTap(SDL_MouseData *unused) 246{ 247} 248 249void 250Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata) 251{ 252} 253 254#endif /* !SDL_MAC_NO_SANDBOX */ 255 256#endif /* SDL_VIDEO_DRIVER_COCOA */ 257 258/* vi: set ts=4 sw=4 expandtab: */