src/video/cocoa/SDL_cocoamousetap.m
author Sam Lantinga <slouken@libsdl.org>
Sat, 26 Nov 2016 10:26:36 -0800
changeset 10656 18f1e8b0737d
parent 10655 a303ec46889b
child 10737 3406a0f8b041
permissions -rw-r--r--
once more - iterating on this is annoying
     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             {
    73                 CGEventTapEnable(tapdata->tap, true);
    74                 return NULL;
    75             }
    76         case kCGEventTapDisabledByUserInput:
    77             {
    78                 return NULL;
    79             }
    80         default:
    81             break;
    82     }
    83 
    84 
    85     if (!window || !mouse) {
    86         return event;
    87     }
    88 
    89     if (mouse->relative_mode) {
    90         return event;
    91     }
    92 
    93     if (!(window->flags & SDL_WINDOW_INPUT_GRABBED)) {
    94         return event;
    95     }
    96 
    97     /* This is the same coordinate system as Cocoa uses. */
    98     nswindow = ((SDL_WindowData *) window->driverdata)->nswindow;
    99     eventLocation = CGEventGetUnflippedLocation(event);
   100     windowRect = [nswindow contentRectForFrameRect:[nswindow frame]];
   101 
   102     if (!NSMouseInRect(NSPointFromCGPoint(eventLocation), windowRect, NO)) {
   103 
   104         /* This is in CGs global screenspace coordinate system, which has a
   105          * flipped Y.
   106          */
   107         CGPoint newLocation = CGEventGetLocation(event);
   108 
   109         if (eventLocation.x < NSMinX(windowRect)) {
   110             newLocation.x = NSMinX(windowRect);
   111         } else if (eventLocation.x >= NSMaxX(windowRect)) {
   112             newLocation.x = NSMaxX(windowRect) - 1.0;
   113         }
   114 
   115         if (eventLocation.y <= NSMinY(windowRect)) {
   116             newLocation.y -= (NSMinY(windowRect) - eventLocation.y + 1);
   117         } else if (eventLocation.y > NSMaxY(windowRect)) {
   118             newLocation.y += (eventLocation.y - NSMaxY(windowRect));
   119         }
   120 
   121         CGWarpMouseCursorPosition(newLocation);
   122         CGAssociateMouseAndMouseCursorPosition(YES);
   123 
   124         if ((CGEventMaskBit(type) & movementEventsMask) == 0) {
   125             /* For click events, we just constrain the event to the window, so
   126              * no other app receives the click event. We can't due the same to
   127              * movement events, since they mean that our warp cursor above
   128              * behaves strangely.
   129              */
   130             CGEventSetLocation(event, newLocation);
   131         }
   132     }
   133 
   134     return event;
   135 }
   136 
   137 static void
   138 SemaphorePostCallback(CFRunLoopTimerRef timer, void *info)
   139 {
   140     SDL_SemPost((SDL_sem*)info);
   141 }
   142 
   143 static int
   144 Cocoa_MouseTapThread(void *data)
   145 {
   146     SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)data;
   147 
   148     /* Tap was created on main thread but we own it now. */
   149     CFMachPortRef eventTap = tapdata->tap;
   150     if (eventTap) {
   151         /* Try to create a runloop source we can schedule. */
   152         CFRunLoopSourceRef runloopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
   153         if  (runloopSource) {
   154             tapdata->runloopSource = runloopSource;
   155         } else {
   156             CFRelease(eventTap);
   157             SDL_SemPost(tapdata->runloopStartedSemaphore);
   158             /* TODO: Both here and in the return below, set some state in
   159              * tapdata to indicate that initialization failed, which we should
   160              * check in InitMouseEventTap, after we move the semaphore check
   161              * from Quit to Init.
   162              */
   163             return 1;
   164         }
   165     } else {
   166         SDL_SemPost(tapdata->runloopStartedSemaphore);
   167         return 1;
   168     }
   169 
   170     tapdata->runloop = CFRunLoopGetCurrent();
   171     CFRunLoopAddSource(tapdata->runloop, tapdata->runloopSource, kCFRunLoopCommonModes);
   172     CFRunLoopTimerContext context = {.info = tapdata->runloopStartedSemaphore};
   173     /* We signal the runloop started semaphore *after* the run loop has started, indicating it's safe to CFRunLoopStop it. */
   174     CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent(), 0, 0, 0, &SemaphorePostCallback, &context);
   175     CFRunLoopAddTimer(tapdata->runloop, timer, kCFRunLoopCommonModes);
   176     CFRelease(timer);
   177 
   178     /* Run the event loop to handle events in the event tap. */
   179     CFRunLoopRun();
   180     /* Make sure this is signaled so that SDL_QuitMouseEventTap knows it can safely SDL_WaitThread for us. */
   181     if (SDL_SemValue(tapdata->runloopStartedSemaphore) < 1) {
   182         SDL_SemPost(tapdata->runloopStartedSemaphore);
   183     }
   184     CFRunLoopRemoveSource(tapdata->runloop, tapdata->runloopSource, kCFRunLoopCommonModes);
   185 
   186     /* Clean up. */
   187     CGEventTapEnable(tapdata->tap, false);
   188     CFRelease(tapdata->runloopSource);
   189     CFRelease(tapdata->tap);
   190     tapdata->runloopSource = NULL;
   191     tapdata->tap = NULL;
   192 
   193     return 0;
   194 }
   195 
   196 void
   197 Cocoa_InitMouseEventTap(SDL_MouseData* driverdata)
   198 {
   199     SDL_MouseEventTapData *tapdata;
   200     driverdata->tapdata = SDL_calloc(1, sizeof(SDL_MouseEventTapData));
   201     tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
   202 
   203     tapdata->runloopStartedSemaphore = SDL_CreateSemaphore(0);
   204     if (tapdata->runloopStartedSemaphore) {
   205         tapdata->tap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap,
   206                                         kCGEventTapOptionDefault, allGrabbedEventsMask,
   207                                         &Cocoa_MouseTapCallback, tapdata);
   208         if (tapdata->tap) {
   209             /* Tap starts disabled, until app requests mouse grab */
   210             CGEventTapEnable(tapdata->tap, false);
   211             tapdata->thread = SDL_CreateThreadInternal(&Cocoa_MouseTapThread, "Event Tap Loop", 512 * 1024, tapdata);
   212             if (tapdata->thread) {
   213                 /* Success - early out. Ownership transferred to thread. */
   214             	return;
   215             }
   216             CFRelease(tapdata->tap);
   217         }
   218         SDL_DestroySemaphore(tapdata->runloopStartedSemaphore);
   219     }
   220     SDL_free(driverdata->tapdata);
   221     driverdata->tapdata = NULL;
   222 }
   223 
   224 void
   225 Cocoa_EnableMouseEventTap(SDL_MouseData *driverdata, SDL_bool enabled)
   226 {
   227     SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
   228     if (tapdata && tapdata->tap)
   229     {
   230         CGEventTapEnable(tapdata->tap, !!enabled);
   231     }
   232 }
   233 
   234 void
   235 Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata)
   236 {
   237     SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
   238     int status;
   239 
   240     /* Ensure that the runloop has been started first.
   241      * TODO: Move this to InitMouseEventTap, check for error conditions that can
   242      * happen in Cocoa_MouseTapThread, and fall back to the non-EventTap way of
   243      * grabbing the mouse if it fails to Init.
   244      */
   245     status = SDL_SemWaitTimeout(tapdata->runloopStartedSemaphore, 5000);
   246     if (status > -1) {
   247         /* Then stop it, which will cause Cocoa_MouseTapThread to return. */
   248         CFRunLoopStop(tapdata->runloop);
   249         /* And then wait for Cocoa_MouseTapThread to finish cleaning up. It
   250          * releases some of the pointers in tapdata. */
   251         SDL_WaitThread(tapdata->thread, &status);
   252     }
   253 
   254     SDL_free(driverdata->tapdata);
   255     driverdata->tapdata = NULL;
   256 }
   257 
   258 #else /* SDL_MAC_NO_SANDBOX */
   259 
   260 void
   261 Cocoa_InitMouseEventTap(SDL_MouseData *unused)
   262 {
   263 }
   264 
   265 void
   266 Cocoa_EnableMouseEventTap(SDL_MouseData *driverdata, SDL_bool enabled)
   267 {
   268 }
   269 
   270 void
   271 Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata)
   272 {
   273 }
   274 
   275 #endif /* !SDL_MAC_NO_SANDBOX */
   276 
   277 #endif /* SDL_VIDEO_DRIVER_COCOA */
   278 
   279 /* vi: set ts=4 sw=4 expandtab: */