src/video/cocoa/SDL_cocoamousetap.m
author Ryan C. Gordon <icculus@icculus.org>
Sun, 24 Nov 2013 23:56:17 -0500
changeset 8093 b43765095a6f
parent 8054 2a38ef3eeabb
child 8149 681eb46b8ac4
permissions -rw-r--r--
Make internal SDL sources include SDL_internal.h instead of SDL_config.h

The new header will include SDL_config.h, but allows for other global stuff.
     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_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 
    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     {
    72         case kCGEventTapDisabledByTimeout:
    73         case kCGEventTapDisabledByUserInput:
    74             {
    75                 CGEventTapEnable(tapdata->tap, true);
    76                 return NULL;
    77             }
    78         default:
    79             break;
    80     }
    81 
    82 
    83     if (!window || !mouse) {
    84         return event;
    85     }
    86 
    87     if (mouse->relative_mode) {
    88         return event;
    89     }
    90 
    91     if (!(window->flags & SDL_WINDOW_INPUT_GRABBED)) {
    92         return event;
    93     }
    94 
    95     /* This is the same coordinate system as Cocoa uses. */
    96     nswindow = ((SDL_WindowData *) window->driverdata)->nswindow;
    97     eventLocation = CGEventGetUnflippedLocation(event);
    98     windowRect = [nswindow contentRectForFrameRect:[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 void
   137 SemaphorePostCallback(CFRunLoopTimerRef timer, void *info)
   138 {
   139     SDL_SemPost((SDL_sem*)info);
   140 }
   141 
   142 static int
   143 Cocoa_MouseTapThread(void *data)
   144 {
   145     SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)data;
   146 
   147     /* Create a tap. */
   148     CFMachPortRef eventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap,
   149                                               kCGEventTapOptionDefault, allGrabbedEventsMask,
   150                                               &Cocoa_MouseTapCallback, tapdata);
   151     if (eventTap) {
   152         /* Try to create a runloop source we can schedule. */
   153         CFRunLoopSourceRef runloopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
   154         if  (runloopSource) {
   155             tapdata->tap = eventTap;
   156             tapdata->runloopSource = runloopSource;
   157         } else {
   158             CFRelease(eventTap);
   159             SDL_SemPost(tapdata->runloopStartedSemaphore);
   160             /* TODO: Both here and in the return below, set some state in
   161              * tapdata to indicate that initialization failed, which we should
   162              * check in InitMouseEventTap, after we move the semaphore check
   163              * from Quit to Init.
   164              */
   165             return 1;
   166         }
   167     } else {
   168         SDL_SemPost(tapdata->runloopStartedSemaphore);
   169         return 1;
   170     }
   171 
   172     tapdata->runloop = CFRunLoopGetCurrent();
   173     CFRunLoopAddSource(tapdata->runloop, tapdata->runloopSource, kCFRunLoopCommonModes);
   174     CFRunLoopTimerContext context = {.info = tapdata->runloopStartedSemaphore};
   175     /* We signal the runloop started semaphore *after* the run loop has started, indicating it's safe to CFRunLoopStop it. */
   176     CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent(), 0, 0, 0, &SemaphorePostCallback, &context);
   177     CFRunLoopAddTimer(tapdata->runloop, timer, kCFRunLoopCommonModes);
   178     CFRelease(timer);
   179 
   180     /* Run the event loop to handle events in the event tap. */
   181     CFRunLoopRun();
   182     /* Make sure this is signaled so that SDL_QuitMouseEventTap knows it can safely SDL_WaitThread for us. */
   183     if (SDL_SemValue(tapdata->runloopStartedSemaphore) < 1) {
   184         SDL_SemPost(tapdata->runloopStartedSemaphore);
   185     }
   186     CFRunLoopRemoveSource(tapdata->runloop, tapdata->runloopSource, kCFRunLoopCommonModes);
   187 
   188     /* Clean up. */
   189     CGEventTapEnable(tapdata->tap, false);
   190     CFRelease(tapdata->runloopSource);
   191     CFRelease(tapdata->tap);
   192     tapdata->runloopSource = NULL;
   193     tapdata->tap = NULL;
   194 
   195     return 0;
   196 }
   197 
   198 void
   199 Cocoa_InitMouseEventTap(SDL_MouseData* driverdata)
   200 {
   201     SDL_MouseEventTapData *tapdata;
   202     driverdata->tapdata = SDL_calloc(1, sizeof(SDL_MouseEventTapData));
   203     tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
   204 
   205     tapdata->runloopStartedSemaphore = SDL_CreateSemaphore(0);
   206     if (tapdata->runloopStartedSemaphore) {
   207         tapdata->thread = SDL_CreateThread(&Cocoa_MouseTapThread, "Event Tap Loop", tapdata);
   208         if (!tapdata->thread) {
   209             SDL_DestroySemaphore(tapdata->runloopStartedSemaphore);
   210         }
   211     }
   212 
   213     if (!tapdata->thread) {
   214         SDL_free(driverdata->tapdata);
   215         driverdata->tapdata = NULL;
   216     }
   217 }
   218 
   219 void
   220 Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata)
   221 {
   222     SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
   223     int status;
   224 
   225     /* Ensure that the runloop has been started first.
   226      * TODO: Move this to InitMouseEventTap, check for error conditions that can
   227      * happen in Cocoa_MouseTapThread, and fall back to the non-EventTap way of
   228      * grabbing the mouse if it fails to Init.
   229      */
   230     status = SDL_SemWaitTimeout(tapdata->runloopStartedSemaphore, 5000);
   231     if (status > -1) {
   232         /* Then stop it, which will cause Cocoa_MouseTapThread to return. */
   233         CFRunLoopStop(tapdata->runloop);
   234         /* And then wait for Cocoa_MouseTapThread to finish cleaning up. It
   235          * releases some of the pointers in tapdata. */
   236         SDL_WaitThread(tapdata->thread, &status);
   237     }
   238 
   239     SDL_free(driverdata->tapdata);
   240     driverdata->tapdata = NULL;
   241 }
   242 
   243 #else /* SDL_MAC_NO_SANDBOX */
   244 
   245 void
   246 Cocoa_InitMouseEventTap(SDL_MouseData *unused)
   247 {
   248 }
   249 
   250 void
   251 Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata)
   252 {
   253 }
   254 
   255 #endif /* !SDL_MAC_NO_SANDBOX */
   256 
   257 #endif /* SDL_VIDEO_DRIVER_COCOA */
   258 
   259 /* vi: set ts=4 sw=4 expandtab: */