src/video/cocoa/SDL_cocoamousetap.m
author Ethan Lee <flibitijibibo@flibitijibibo.com>
Wed, 17 Jul 2019 23:20:57 -0400
changeset 12950 05dddfb66b85
parent 12503 806492103856
permissions -rw-r--r--
Copypaste SDL_NSLog to UIKit backend, document it as such
     1 /*
     2   Simple DirectMedia Layer
     3   Copyright (C) 1997-2019 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     if (tapdata == NULL) {
   241         /* event tap was already cleaned up (possibly due to CGEventTapCreate
   242          * returning null.)
   243          */
   244         return;
   245     }
   246 
   247     /* Ensure that the runloop has been started first.
   248      * TODO: Move this to InitMouseEventTap, check for error conditions that can
   249      * happen in Cocoa_MouseTapThread, and fall back to the non-EventTap way of
   250      * grabbing the mouse if it fails to Init.
   251      */
   252     status = SDL_SemWaitTimeout(tapdata->runloopStartedSemaphore, 5000);
   253     if (status > -1) {
   254         /* Then stop it, which will cause Cocoa_MouseTapThread to return. */
   255         CFRunLoopStop(tapdata->runloop);
   256         /* And then wait for Cocoa_MouseTapThread to finish cleaning up. It
   257          * releases some of the pointers in tapdata. */
   258         SDL_WaitThread(tapdata->thread, &status);
   259     }
   260 
   261     SDL_free(driverdata->tapdata);
   262     driverdata->tapdata = NULL;
   263 }
   264 
   265 #else /* SDL_MAC_NO_SANDBOX */
   266 
   267 void
   268 Cocoa_InitMouseEventTap(SDL_MouseData *unused)
   269 {
   270 }
   271 
   272 void
   273 Cocoa_EnableMouseEventTap(SDL_MouseData *driverdata, SDL_bool enabled)
   274 {
   275 }
   276 
   277 void
   278 Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata)
   279 {
   280 }
   281 
   282 #endif /* !SDL_MAC_NO_SANDBOX */
   283 
   284 #endif /* SDL_VIDEO_DRIVER_COCOA */
   285 
   286 /* vi: set ts=4 sw=4 expandtab: */