sdl

FORK: Simple Directmedia Layer
git clone https://git.neptards.moe/neptards/sdl.git
Log | Files | Refs

SDL_cocoamousetap.m (9842B)


      1 /*
      2   Simple DirectMedia Layer
      3   Copyright (C) 1997-2020 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: */