src/video/cocoa/SDL_cocoamousetap.m
author Sam Lantinga <slouken@libsdl.org>
Sat, 26 Nov 2016 10:26:26 -0800
changeset 10654 a9713e5c7788
parent 10653 f87d76304c76
child 10655 a303ec46889b
permissions -rw-r--r--
tap should default to disabled, not enabled
     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     /* Tap was created on main thread but we own it now. */
   146     CFMachPortRef eventTap = tapdata->tap;
   147     if (eventTap) {
   148         /* Try to create a runloop source we can schedule. */
   149         CFRunLoopSourceRef runloopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
   150         if  (runloopSource) {
   151             tapdata->runloopSource = runloopSource;
   152         } else {
   153             CFRelease(eventTap);
   154             SDL_SemPost(tapdata->runloopStartedSemaphore);
   155             /* TODO: Both here and in the return below, set some state in
   156              * tapdata to indicate that initialization failed, which we should
   157              * check in InitMouseEventTap, after we move the semaphore check
   158              * from Quit to Init.
   159              */
   160             return 1;
   161         }
   162     } else {
   163         SDL_SemPost(tapdata->runloopStartedSemaphore);
   164         return 1;
   165     }
   166 
   167     tapdata->runloop = CFRunLoopGetCurrent();
   168     CFRunLoopAddSource(tapdata->runloop, tapdata->runloopSource, kCFRunLoopCommonModes);
   169     CFRunLoopTimerContext context = {.info = tapdata->runloopStartedSemaphore};
   170     /* We signal the runloop started semaphore *after* the run loop has started, indicating it's safe to CFRunLoopStop it. */
   171     CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent(), 0, 0, 0, &SemaphorePostCallback, &context);
   172     CFRunLoopAddTimer(tapdata->runloop, timer, kCFRunLoopCommonModes);
   173     CFRelease(timer);
   174 
   175     /* Run the event loop to handle events in the event tap. */
   176     CFRunLoopRun();
   177     /* Make sure this is signaled so that SDL_QuitMouseEventTap knows it can safely SDL_WaitThread for us. */
   178     if (SDL_SemValue(tapdata->runloopStartedSemaphore) < 1) {
   179         SDL_SemPost(tapdata->runloopStartedSemaphore);
   180     }
   181     CFRunLoopRemoveSource(tapdata->runloop, tapdata->runloopSource, kCFRunLoopCommonModes);
   182 
   183     /* Clean up. */
   184     CGEventTapEnable(tapdata->tap, false);
   185     CFRelease(tapdata->runloopSource);
   186     CFRelease(tapdata->tap);
   187     tapdata->runloopSource = NULL;
   188     tapdata->tap = NULL;
   189 
   190     return 0;
   191 }
   192 
   193 void
   194 Cocoa_InitMouseEventTap(SDL_MouseData* driverdata)
   195 {
   196     SDL_MouseEventTapData *tapdata;
   197     driverdata->tapdata = SDL_calloc(1, sizeof(SDL_MouseEventTapData));
   198     tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
   199 
   200     tapdata->runloopStartedSemaphore = SDL_CreateSemaphore(0);
   201     if (tapdata->runloopStartedSemaphore) {
   202         tapdata->tap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap,
   203                                         kCGEventTapOptionDefault, allGrabbedEventsMask,
   204                                         &Cocoa_MouseTapCallback, tapdata);
   205         if (tapdata->tap) {
   206             /* Tap starts disabled, until app requests mouse grab */
   207             CGEventTapEnable(tapdata->tap, false);
   208             tapdata->thread = SDL_CreateThreadInternal(&Cocoa_MouseTapThread, "Event Tap Loop", 512 * 1024, tapdata);
   209             if (tapdata->thread) {
   210                 /* Success - early out. Ownership transferred to thread. */
   211             	return;
   212             }
   213             CFRelease(tapdata->tap);
   214         }
   215         SDL_DestroySemaphore(tapdata->runloopStartedSemaphore);
   216     }
   217     SDL_free(driverdata->tapdata);
   218     driverdata->tapdata = NULL;
   219 }
   220 
   221 void
   222 Cocoa_EnableMouseEventTap(SDL_MouseData *driverdata, SDL_bool enabled)
   223 {
   224     SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
   225     if (tapdata && tapdata->tap)
   226     {
   227         CGEventTapEnable(tapdata->tap, !!enabled);
   228     }
   229 }
   230 
   231 void
   232 Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata)
   233 {
   234     SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
   235     int status;
   236 
   237     /* Ensure that the runloop has been started first.
   238      * TODO: Move this to InitMouseEventTap, check for error conditions that can
   239      * happen in Cocoa_MouseTapThread, and fall back to the non-EventTap way of
   240      * grabbing the mouse if it fails to Init.
   241      */
   242     status = SDL_SemWaitTimeout(tapdata->runloopStartedSemaphore, 5000);
   243     if (status > -1) {
   244         /* Then stop it, which will cause Cocoa_MouseTapThread to return. */
   245         CFRunLoopStop(tapdata->runloop);
   246         /* And then wait for Cocoa_MouseTapThread to finish cleaning up. It
   247          * releases some of the pointers in tapdata. */
   248         SDL_WaitThread(tapdata->thread, &status);
   249     }
   250 
   251     SDL_free(driverdata->tapdata);
   252     driverdata->tapdata = NULL;
   253 }
   254 
   255 #else /* SDL_MAC_NO_SANDBOX */
   256 
   257 void
   258 Cocoa_InitMouseEventTap(SDL_MouseData *unused)
   259 {
   260 }
   261 
   262 void
   263 Cocoa_EnableMouseEventTap(SDL_MouseData *driverdata, SDL_bool enabled)
   264 {
   265 }
   266 
   267 void
   268 Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata)
   269 {
   270 }
   271 
   272 #endif /* !SDL_MAC_NO_SANDBOX */
   273 
   274 #endif /* SDL_VIDEO_DRIVER_COCOA */
   275 
   276 /* vi: set ts=4 sw=4 expandtab: */