src/video/cocoa/SDL_cocoamousetap.m
author Jørgen P. Tjernø <jorgen@valvesoftware.com>
Thu, 08 Aug 2013 12:48:37 -0700
changeset 7607 7753a6f8cda8
parent 7593 20298a0d8631
child 7610 b043c5c726c1
permissions -rw-r--r--
Mac: Don't enable SDL_MAC_NO_SANDBOX by default.

To get this functionality, you need to use -DSDL_MAC_NO_SANDBOX=1 in
your CFLAGS.
     1 /*
     2   Simple DirectMedia Layer
     3   Copyright (C) 1997-2013 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_config.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 
    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_MouseData *driverdata = (SDL_MouseData*)refcon;
    64     SDL_Mouse *mouse = SDL_GetMouse();
    65     SDL_Window *window = SDL_GetKeyboardFocus();
    66     NSRect windowRect;
    67     CGPoint eventLocation;
    68 
    69     switch (type)
    70     {
    71         case kCGEventTapDisabledByTimeout:
    72         case kCGEventTapDisabledByUserInput:
    73             {
    74                 CGEventTapEnable(((SDL_MouseEventTapData*)(driverdata->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     eventLocation = CGEventGetUnflippedLocation(event);
    96     windowRect = [((SDL_WindowData *) window->driverdata)->nswindow frame];
    97 
    98     if (!NSPointInRect(NSPointFromCGPoint(eventLocation), windowRect)) {
    99 
   100         /* This is in CGs global screenspace coordinate system, which has a
   101          * flipped Y.
   102          */
   103         CGPoint newLocation = CGEventGetLocation(event);
   104 
   105         if (eventLocation.x < NSMinX(windowRect)) {
   106             newLocation.x = NSMinX(windowRect);
   107         } else if (eventLocation.x >= NSMaxX(windowRect)) {
   108             newLocation.x = NSMaxX(windowRect) - 1.0;
   109         }
   110 
   111         if (eventLocation.y < NSMinY(windowRect)) {
   112             newLocation.y -= (NSMinY(windowRect) - eventLocation.y + 1);
   113         } else if (eventLocation.y >= NSMaxY(windowRect)) {
   114             newLocation.y += (eventLocation.y - NSMaxY(windowRect) + 1);
   115         }
   116 
   117         CGSetLocalEventsSuppressionInterval(0);
   118         CGWarpMouseCursorPosition(newLocation);
   119         CGSetLocalEventsSuppressionInterval(0.25);
   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 int
   135 Cocoa_MouseTapThread(void *data)
   136 {
   137     SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)data;
   138 
   139     /* Create a tap. */
   140     CFMachPortRef eventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap,
   141                                               kCGEventTapOptionDefault, allGrabbedEventsMask,
   142                                               &Cocoa_MouseTapCallback, tapdata);
   143     if (eventTap) {
   144         /* Try to create a runloop source we can schedule. */
   145         CFRunLoopSourceRef runloopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
   146         if  (runloopSource) {
   147             tapdata->tap = eventTap;
   148             tapdata->runloopSource = runloopSource;
   149         } else {
   150             CFRelease(eventTap);
   151             SDL_SemPost(tapdata->runloopStartedSemaphore);
   152             /* TODO: Both here and in the return below, set some state in
   153              * tapdata to indicate that initialization failed, which we should
   154              * check in InitMouseEventTap, after we move the semaphore check
   155              * from Quit to Init.
   156              */
   157             return 1;
   158         }
   159     } else {
   160         SDL_SemPost(tapdata->runloopStartedSemaphore);
   161         return 1;
   162     }
   163 
   164     tapdata->runloop = CFRunLoopGetCurrent();
   165     CFRunLoopAddSource(tapdata->runloop, tapdata->runloopSource, kCFRunLoopCommonModes);
   166     CFRunLoopPerformBlock(tapdata->runloop, kCFRunLoopCommonModes, ^{
   167         /* We signal this *after* the run loop has started, indicating it's safe to CFRunLoopStop it. */
   168         SDL_SemPost(tapdata->runloopStartedSemaphore);
   169     });
   170 
   171     /* Run the event loop to handle events in the event tap. */
   172     CFRunLoopRun();
   173     /* Make sure this is signaled so that SDL_QuitMouseEventTap knows it can safely SDL_WaitThread for us. */
   174     if (SDL_SemValue(tapdata->runloopStartedSemaphore) < 1) {
   175         SDL_SemPost(tapdata->runloopStartedSemaphore);
   176     }
   177     CFRunLoopRemoveSource(tapdata->runloop, tapdata->runloopSource, kCFRunLoopCommonModes);
   178 
   179     /* Clean up. */
   180     CGEventTapEnable(tapdata->tap, false);
   181     CFRelease(tapdata->runloopSource);
   182     CFRelease(tapdata->tap);
   183     tapdata->runloopSource = NULL;
   184     tapdata->tap = NULL;
   185 
   186     return 0;
   187 }
   188 
   189 void
   190 Cocoa_InitMouseEventTap(SDL_MouseData* driverdata)
   191 {
   192     SDL_MouseEventTapData *tapdata;
   193     driverdata->tapdata = SDL_calloc(1, sizeof(SDL_MouseEventTapData));
   194     tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
   195 
   196     tapdata->runloopStartedSemaphore = SDL_CreateSemaphore(0);
   197     if (tapdata->runloopStartedSemaphore) {
   198         tapdata->thread = SDL_CreateThread(&Cocoa_MouseTapThread, "Event Tap Loop", tapdata);
   199         if (!tapdata->thread) {
   200             SDL_DestroySemaphore(tapdata->runloopStartedSemaphore);
   201         }
   202     }
   203 
   204     if (!tapdata->thread) {
   205         SDL_free(driverdata->tapdata);
   206         driverdata->tapdata = NULL;
   207     }
   208 }
   209 
   210 void
   211 Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata)
   212 {
   213     SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
   214     int status;
   215 
   216     /* Ensure that the runloop has been started first.
   217      * TODO: Move this to InitMouseEventTap, check for error conditions that can
   218      * happen in Cocoa_MouseTapThread, and fall back to the non-EventTap way of
   219      * grabbing the mouse if it fails to Init.
   220      */
   221     status = SDL_SemWaitTimeout(tapdata->runloopStartedSemaphore, 5000);
   222     if (status > -1) {
   223         /* Then stop it, which will cause Cocoa_MouseTapThread to return. */
   224         CFRunLoopStop(tapdata->runloop);
   225         /* And then wait for Cocoa_MouseTapThread to finish cleaning up. It
   226          * releases some of the pointers in tapdata. */
   227         SDL_WaitThread(tapdata->thread, &status);
   228     }
   229 
   230     SDL_free(driverdata->tapdata);
   231     driverdata->tapdata = NULL;
   232 }
   233 
   234 #else /* SDL_MAC_NO_SANDBOX */
   235 
   236 void
   237 Cocoa_InitMouseEventTap(SDL_MouseData *unused)
   238 {
   239 }
   240 
   241 void
   242 Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata)
   243 {
   244 }
   245 
   246 #endif /* !SDL_MAC_NO_SANDBOX */
   247 
   248 #endif /* SDL_VIDEO_DRIVER_COCOA */
   249 
   250 /* vi: set ts=4 sw=4 expandtab: */