src/video/cocoa/SDL_cocoamousetap.m
author Alex Szpakowski <slime73@gmail.com>
Sat, 21 May 2016 12:09:23 -0300
changeset 10177 faa36f2de933
parent 10159 d41acf6379f6
child 10653 f87d76304c76
permissions -rw-r--r--
Mac: Fix a crash when SDL is compiled with SDL_MAC_NO_SANDBOX enabled, by increasing the stack size of the mouse tap thread back to OS X' default of 512 KB.
     1 /*
     2   Simple DirectMedia Layer
     3   Copyright (C) 1997-2016 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_cocoavideo.h"
    36 #include "../../thread/SDL_systhread.h"
    37 
    38 #include "../../events/SDL_mouse_c.h"
    39 
    40 typedef struct {
    41     CFMachPortRef tap;
    42     CFRunLoopRef runloop;
    43     CFRunLoopSourceRef runloopSource;
    44     SDL_Thread *thread;
    45     SDL_sem *runloopStartedSemaphore;
    46 } SDL_MouseEventTapData;
    47 
    48 static const CGEventMask movementEventsMask =
    49       CGEventMaskBit(kCGEventLeftMouseDragged)
    50     | CGEventMaskBit(kCGEventRightMouseDragged)
    51     | CGEventMaskBit(kCGEventMouseMoved);
    52 
    53 static 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 
    60 static CGEventRef
    61 Cocoa_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 (!NSMouseInRect(NSPointFromCGPoint(eventLocation), windowRect, NO)) {
   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));
   116         }
   117 
   118         CGWarpMouseCursorPosition(newLocation);
   119         CGAssociateMouseAndMouseCursorPosition(YES);
   120 
   121         if ((CGEventMaskBit(type) & movementEventsMask) == 0) {
   122             /* For click events, we just constrain the event to the window, so
   123              * no other app receives the click event. We can't due the same to
   124              * movement events, since they mean that our warp cursor above
   125              * behaves strangely.
   126              */
   127             CGEventSetLocation(event, newLocation);
   128         }
   129     }
   130 
   131     return event;
   132 }
   133 
   134 static void
   135 SemaphorePostCallback(CFRunLoopTimerRef timer, void *info)
   136 {
   137     SDL_SemPost((SDL_sem*)info);
   138 }
   139 
   140 static int
   141 Cocoa_MouseTapThread(void *data)
   142 {
   143     SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)data;
   144 
   145     /* Create a tap. */
   146     CFMachPortRef eventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap,
   147                                               kCGEventTapOptionDefault, allGrabbedEventsMask,
   148                                               &Cocoa_MouseTapCallback, tapdata);
   149     if (eventTap) {
   150         /* Try to create a runloop source we can schedule. */
   151         CFRunLoopSourceRef runloopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
   152         if  (runloopSource) {
   153             tapdata->tap = eventTap;
   154             tapdata->runloopSource = runloopSource;
   155         } else {
   156             CFRelease(eventTap);
   157             SDL_SemPost(tapdata->runloopStartedSemaphore);
   158             /* TODO: Both here and in the return below, set some state in
   159              * tapdata to indicate that initialization failed, which we should
   160              * check in InitMouseEventTap, after we move the semaphore check
   161              * from Quit to Init.
   162              */
   163             return 1;
   164         }
   165     } else {
   166         SDL_SemPost(tapdata->runloopStartedSemaphore);
   167         return 1;
   168     }
   169 
   170     tapdata->runloop = CFRunLoopGetCurrent();
   171     CFRunLoopAddSource(tapdata->runloop, tapdata->runloopSource, kCFRunLoopCommonModes);
   172     CFRunLoopTimerContext context = {.info = tapdata->runloopStartedSemaphore};
   173     /* We signal the runloop started semaphore *after* the run loop has started, indicating it's safe to CFRunLoopStop it. */
   174     CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent(), 0, 0, 0, &SemaphorePostCallback, &context);
   175     CFRunLoopAddTimer(tapdata->runloop, timer, kCFRunLoopCommonModes);
   176     CFRelease(timer);
   177 
   178     /* Run the event loop to handle events in the event tap. */
   179     CFRunLoopRun();
   180     /* Make sure this is signaled so that SDL_QuitMouseEventTap knows it can safely SDL_WaitThread for us. */
   181     if (SDL_SemValue(tapdata->runloopStartedSemaphore) < 1) {
   182         SDL_SemPost(tapdata->runloopStartedSemaphore);
   183     }
   184     CFRunLoopRemoveSource(tapdata->runloop, tapdata->runloopSource, kCFRunLoopCommonModes);
   185 
   186     /* Clean up. */
   187     CGEventTapEnable(tapdata->tap, false);
   188     CFRelease(tapdata->runloopSource);
   189     CFRelease(tapdata->tap);
   190     tapdata->runloopSource = NULL;
   191     tapdata->tap = NULL;
   192 
   193     return 0;
   194 }
   195 
   196 void
   197 Cocoa_InitMouseEventTap(SDL_MouseData* driverdata)
   198 {
   199     SDL_MouseEventTapData *tapdata;
   200     driverdata->tapdata = SDL_calloc(1, sizeof(SDL_MouseEventTapData));
   201     tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
   202 
   203     tapdata->runloopStartedSemaphore = SDL_CreateSemaphore(0);
   204     if (tapdata->runloopStartedSemaphore) {
   205         tapdata->thread = SDL_CreateThreadInternal(&Cocoa_MouseTapThread, "Event Tap Loop", 512 * 1024, tapdata);
   206         if (!tapdata->thread) {
   207             SDL_DestroySemaphore(tapdata->runloopStartedSemaphore);
   208         }
   209     }
   210 
   211     if (!tapdata->thread) {
   212         SDL_free(driverdata->tapdata);
   213         driverdata->tapdata = NULL;
   214     }
   215 }
   216 
   217 void
   218 Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata)
   219 {
   220     SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
   221     int status;
   222 
   223     /* Ensure that the runloop has been started first.
   224      * TODO: Move this to InitMouseEventTap, check for error conditions that can
   225      * happen in Cocoa_MouseTapThread, and fall back to the non-EventTap way of
   226      * grabbing the mouse if it fails to Init.
   227      */
   228     status = SDL_SemWaitTimeout(tapdata->runloopStartedSemaphore, 5000);
   229     if (status > -1) {
   230         /* Then stop it, which will cause Cocoa_MouseTapThread to return. */
   231         CFRunLoopStop(tapdata->runloop);
   232         /* And then wait for Cocoa_MouseTapThread to finish cleaning up. It
   233          * releases some of the pointers in tapdata. */
   234         SDL_WaitThread(tapdata->thread, &status);
   235     }
   236 
   237     SDL_free(driverdata->tapdata);
   238     driverdata->tapdata = NULL;
   239 }
   240 
   241 #else /* SDL_MAC_NO_SANDBOX */
   242 
   243 void
   244 Cocoa_InitMouseEventTap(SDL_MouseData *unused)
   245 {
   246 }
   247 
   248 void
   249 Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata)
   250 {
   251 }
   252 
   253 #endif /* !SDL_MAC_NO_SANDBOX */
   254 
   255 #endif /* SDL_VIDEO_DRIVER_COCOA */
   256 
   257 /* vi: set ts=4 sw=4 expandtab: */