src/video/cocoa/SDL_cocoamousetap.m
author Sam Lantinga <slouken@libsdl.org>
Sat, 26 Nov 2016 10:26:22 -0800
changeset 10653 f87d76304c76
parent 10177 faa36f2de933
child 10654 a9713e5c7788
permissions -rw-r--r--
SDL for Mac - only enable global event tap when actually necessary (app has focus and has requested relative mouse mode or has asked for a mouse grab). in other situations the event tap impacts system performance and battery life with no benefit.
     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             tapdata->thread = SDL_CreateThreadInternal(&Cocoa_MouseTapThread, "Event Tap Loop", 512 * 1024, tapdata);
   207             if (tapdata->thread) {
   208                 /* Success - early out. Ownership transferred to thread. */
   209             	return;
   210             }
   211             CFRelease(tapdata->tap);
   212         }
   213         SDL_DestroySemaphore(tapdata->runloopStartedSemaphore);
   214     }
   215     SDL_free(driverdata->tapdata);
   216     driverdata->tapdata = NULL;
   217 }
   218 
   219 void
   220 Cocoa_EnableMouseEventTap(SDL_MouseData *driverdata, SDL_bool enabled)
   221 {
   222     SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
   223     if (tapdata && tapdata->tap)
   224     {
   225         CGEventTapEnable(tapdata->tap, enabled);
   226     }
   227 }
   228 
   229 void
   230 Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata)
   231 {
   232     SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
   233     int status;
   234 
   235     /* Ensure that the runloop has been started first.
   236      * TODO: Move this to InitMouseEventTap, check for error conditions that can
   237      * happen in Cocoa_MouseTapThread, and fall back to the non-EventTap way of
   238      * grabbing the mouse if it fails to Init.
   239      */
   240     status = SDL_SemWaitTimeout(tapdata->runloopStartedSemaphore, 5000);
   241     if (status > -1) {
   242         /* Then stop it, which will cause Cocoa_MouseTapThread to return. */
   243         CFRunLoopStop(tapdata->runloop);
   244         /* And then wait for Cocoa_MouseTapThread to finish cleaning up. It
   245          * releases some of the pointers in tapdata. */
   246         SDL_WaitThread(tapdata->thread, &status);
   247     }
   248 
   249     SDL_free(driverdata->tapdata);
   250     driverdata->tapdata = NULL;
   251 }
   252 
   253 #else /* SDL_MAC_NO_SANDBOX */
   254 
   255 void
   256 Cocoa_InitMouseEventTap(SDL_MouseData *unused)
   257 {
   258 }
   259 
   260 void
   261 Cocoa_EnableMouseEventTap(SDL_MouseData *driverdata, SDL_bool enabled)
   262 {
   263 }
   264 
   265 void
   266 Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata)
   267 {
   268 }
   269 
   270 #endif /* !SDL_MAC_NO_SANDBOX */
   271 
   272 #endif /* SDL_VIDEO_DRIVER_COCOA */
   273 
   274 /* vi: set ts=4 sw=4 expandtab: */