src/video/cocoa/SDL_cocoamousetap.m
author Jørgen P. Tjernø <jorgen@valvesoftware.com>
Wed, 07 Aug 2013 16:29:15 -0700
changeset 7593 20298a0d8631
child 7607 7753a6f8cda8
permissions -rw-r--r--
Mac: Better mouse-grab if you define SDL_MAC_NO_SANDBOX.

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