src/video/cocoa/SDL_cocoamousetap.m
author Sam Lantinga <slouken@libsdl.org>
Sat, 16 Nov 2013 21:19:16 -0800
changeset 8005 d8edfca6e3c2
parent 7917 686df9c2b98a
child 8054 2a38ef3eeabb
permissions -rw-r--r--
When the mouse is grabbed it's constrained to the client area, not the window frame.
     1 /*
     2   Simple DirectMedia Layer
     3   Copyright (C) 1997-2013 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_config.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_thread.h"
    36 #include "SDL_cocoavideo.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 = ((SDL_WindowData *) window->driverdata)->nswindow;
    67     NSRect windowRect;
    68     CGPoint eventLocation;
    69 
    70     switch (type)
    71     {
    72         case kCGEventTapDisabledByTimeout:
    73         case kCGEventTapDisabledByUserInput:
    74             {
    75                 CGEventTapEnable(tapdata->tap, true);
    76                 return NULL;
    77             }
    78         default:
    79             break;
    80     }
    81 
    82 
    83     if (!window || !mouse) {
    84         return event;
    85     }
    86 
    87     if (mouse->relative_mode) {
    88         return event;
    89     }
    90 
    91     if (!(window->flags & SDL_WINDOW_INPUT_GRABBED)) {
    92         return event;
    93     }
    94 
    95     /* This is the same coordinate system as Cocoa uses. */
    96     eventLocation = CGEventGetUnflippedLocation(event);
    97     windowRect = [nswindow contentRectForFrameRect:[nswindow frame]];
    98 
    99     if (!NSPointInRect(NSPointFromCGPoint(eventLocation), windowRect)) {
   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) + 1);
   116         }
   117 
   118         CGSetLocalEventsSuppressionInterval(0);
   119         CGWarpMouseCursorPosition(newLocation);
   120         CGSetLocalEventsSuppressionInterval(0.25);
   121 
   122         if ((CGEventMaskBit(type) & movementEventsMask) == 0) {
   123             /* For click events, we just constrain the event to the window, so
   124              * no other app receives the click event. We can't due the same to
   125              * movement events, since they mean that our warp cursor above
   126              * behaves strangely.
   127              */
   128             CGEventSetLocation(event, newLocation);
   129         }
   130     }
   131 
   132     return event;
   133 }
   134 
   135 static void
   136 SemaphorePostCallback(CFRunLoopTimerRef timer, void *info)
   137 {
   138     SDL_SemPost((SDL_sem*)info);
   139 }
   140 
   141 static int
   142 Cocoa_MouseTapThread(void *data)
   143 {
   144     SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)data;
   145 
   146     /* Create a tap. */
   147     CFMachPortRef eventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap,
   148                                               kCGEventTapOptionDefault, allGrabbedEventsMask,
   149                                               &Cocoa_MouseTapCallback, tapdata);
   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->tap = eventTap;
   155             tapdata->runloopSource = runloopSource;
   156         } else {
   157             CFRelease(eventTap);
   158             SDL_SemPost(tapdata->runloopStartedSemaphore);
   159             /* TODO: Both here and in the return below, set some state in
   160              * tapdata to indicate that initialization failed, which we should
   161              * check in InitMouseEventTap, after we move the semaphore check
   162              * from Quit to Init.
   163              */
   164             return 1;
   165         }
   166     } else {
   167         SDL_SemPost(tapdata->runloopStartedSemaphore);
   168         return 1;
   169     }
   170 
   171     tapdata->runloop = CFRunLoopGetCurrent();
   172     CFRunLoopAddSource(tapdata->runloop, tapdata->runloopSource, kCFRunLoopCommonModes);
   173     CFRunLoopTimerContext context = {.info = tapdata->runloopStartedSemaphore};
   174     /* We signal the runloop started semaphore *after* the run loop has started, indicating it's safe to CFRunLoopStop it. */
   175     CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent(), 0, 0, 0, &SemaphorePostCallback, &context);
   176     CFRunLoopAddTimer(tapdata->runloop, timer, kCFRunLoopCommonModes);
   177     CFRelease(timer);
   178 
   179     /* Run the event loop to handle events in the event tap. */
   180     CFRunLoopRun();
   181     /* Make sure this is signaled so that SDL_QuitMouseEventTap knows it can safely SDL_WaitThread for us. */
   182     if (SDL_SemValue(tapdata->runloopStartedSemaphore) < 1) {
   183         SDL_SemPost(tapdata->runloopStartedSemaphore);
   184     }
   185     CFRunLoopRemoveSource(tapdata->runloop, tapdata->runloopSource, kCFRunLoopCommonModes);
   186 
   187     /* Clean up. */
   188     CGEventTapEnable(tapdata->tap, false);
   189     CFRelease(tapdata->runloopSource);
   190     CFRelease(tapdata->tap);
   191     tapdata->runloopSource = NULL;
   192     tapdata->tap = NULL;
   193 
   194     return 0;
   195 }
   196 
   197 void
   198 Cocoa_InitMouseEventTap(SDL_MouseData* driverdata)
   199 {
   200     SDL_MouseEventTapData *tapdata;
   201     driverdata->tapdata = SDL_calloc(1, sizeof(SDL_MouseEventTapData));
   202     tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
   203 
   204     tapdata->runloopStartedSemaphore = SDL_CreateSemaphore(0);
   205     if (tapdata->runloopStartedSemaphore) {
   206         tapdata->thread = SDL_CreateThread(&Cocoa_MouseTapThread, "Event Tap Loop", tapdata);
   207         if (!tapdata->thread) {
   208             SDL_DestroySemaphore(tapdata->runloopStartedSemaphore);
   209         }
   210     }
   211 
   212     if (!tapdata->thread) {
   213         SDL_free(driverdata->tapdata);
   214         driverdata->tapdata = NULL;
   215     }
   216 }
   217 
   218 void
   219 Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata)
   220 {
   221     SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
   222     int status;
   223 
   224     /* Ensure that the runloop has been started first.
   225      * TODO: Move this to InitMouseEventTap, check for error conditions that can
   226      * happen in Cocoa_MouseTapThread, and fall back to the non-EventTap way of
   227      * grabbing the mouse if it fails to Init.
   228      */
   229     status = SDL_SemWaitTimeout(tapdata->runloopStartedSemaphore, 5000);
   230     if (status > -1) {
   231         /* Then stop it, which will cause Cocoa_MouseTapThread to return. */
   232         CFRunLoopStop(tapdata->runloop);
   233         /* And then wait for Cocoa_MouseTapThread to finish cleaning up. It
   234          * releases some of the pointers in tapdata. */
   235         SDL_WaitThread(tapdata->thread, &status);
   236     }
   237 
   238     SDL_free(driverdata->tapdata);
   239     driverdata->tapdata = NULL;
   240 }
   241 
   242 #else /* SDL_MAC_NO_SANDBOX */
   243 
   244 void
   245 Cocoa_InitMouseEventTap(SDL_MouseData *unused)
   246 {
   247 }
   248 
   249 void
   250 Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata)
   251 {
   252 }
   253 
   254 #endif /* !SDL_MAC_NO_SANDBOX */
   255 
   256 #endif /* SDL_VIDEO_DRIVER_COCOA */
   257 
   258 /* vi: set ts=4 sw=4 expandtab: */