src/video/cocoa/SDL_cocoamousetap.m
author Sam Lantinga <slouken@libsdl.org>
Sat, 26 Nov 2016 10:26:32 -0800
changeset 10655 a303ec46889b
parent 10654 a9713e5c7788
child 10656 18f1e8b0737d
permissions -rw-r--r--
if the tap is explicitly disabled by code or by another program, let it remain disabled! this is different than the automatic "event tap was too slow therefore we stopped processing it" timeout which we want to re-enable after.
jorgen@7593
     1
/*
jorgen@7593
     2
  Simple DirectMedia Layer
slouken@9998
     3
  Copyright (C) 1997-2016 Sam Lantinga <slouken@libsdl.org>
jorgen@7593
     4
jorgen@7593
     5
  This software is provided 'as-is', without any express or implied
jorgen@7593
     6
  warranty.  In no event will the authors be held liable for any damages
jorgen@7593
     7
  arising from the use of this software.
jorgen@7593
     8
jorgen@7593
     9
  Permission is granted to anyone to use this software for any purpose,
jorgen@7593
    10
  including commercial applications, and to alter it and redistribute it
jorgen@7593
    11
  freely, subject to the following restrictions:
jorgen@7593
    12
jorgen@7593
    13
  1. The origin of this software must not be misrepresented; you must not
jorgen@7593
    14
     claim that you wrote the original software. If you use this software
jorgen@7593
    15
     in a product, an acknowledgment in the product documentation would be
jorgen@7593
    16
     appreciated but is not required.
jorgen@7593
    17
  2. Altered source versions must be plainly marked as such, and must not be
jorgen@7593
    18
     misrepresented as being the original software.
jorgen@7593
    19
  3. This notice may not be removed or altered from any source distribution.
jorgen@7593
    20
*/
icculus@8093
    21
#include "../../SDL_internal.h"
jorgen@7593
    22
jorgen@7593
    23
#if SDL_VIDEO_DRIVER_COCOA
jorgen@7593
    24
jorgen@7593
    25
#include "SDL_cocoamousetap.h"
jorgen@7593
    26
jorgen@7593
    27
/* Event taps are forbidden in the Mac App Store, so we can only enable this
jorgen@7593
    28
 * code if your app doesn't need to ship through the app store.
jorgen@7593
    29
 * This code makes it so that a grabbed cursor cannot "leak" a mouse click
jorgen@7593
    30
 * past the edge of the window if moving the cursor too fast.
jorgen@7593
    31
 */
jorgen@7593
    32
#if SDL_MAC_NO_SANDBOX
jorgen@7593
    33
jorgen@7593
    34
#include "SDL_keyboard.h"
jorgen@7593
    35
#include "SDL_cocoavideo.h"
icculus@10147
    36
#include "../../thread/SDL_systhread.h"
jorgen@7593
    37
jorgen@7593
    38
#include "../../events/SDL_mouse_c.h"
jorgen@7593
    39
jorgen@7593
    40
typedef struct {
jorgen@7593
    41
    CFMachPortRef tap;
jorgen@7593
    42
    CFRunLoopRef runloop;
jorgen@7593
    43
    CFRunLoopSourceRef runloopSource;
jorgen@7593
    44
    SDL_Thread *thread;
jorgen@7593
    45
    SDL_sem *runloopStartedSemaphore;
jorgen@7593
    46
} SDL_MouseEventTapData;
jorgen@7593
    47
jorgen@7593
    48
static const CGEventMask movementEventsMask =
jorgen@7593
    49
      CGEventMaskBit(kCGEventLeftMouseDragged)
jorgen@7593
    50
    | CGEventMaskBit(kCGEventRightMouseDragged)
jorgen@7593
    51
    | CGEventMaskBit(kCGEventMouseMoved);
jorgen@7593
    52
jorgen@7593
    53
static const CGEventMask allGrabbedEventsMask =
jorgen@7593
    54
      CGEventMaskBit(kCGEventLeftMouseDown)    | CGEventMaskBit(kCGEventLeftMouseUp)
jorgen@7593
    55
    | CGEventMaskBit(kCGEventRightMouseDown)   | CGEventMaskBit(kCGEventRightMouseUp)
jorgen@7593
    56
    | CGEventMaskBit(kCGEventOtherMouseDown)   | CGEventMaskBit(kCGEventOtherMouseUp)
jorgen@7593
    57
    | CGEventMaskBit(kCGEventLeftMouseDragged) | CGEventMaskBit(kCGEventRightMouseDragged)
jorgen@7593
    58
    | CGEventMaskBit(kCGEventMouseMoved);
jorgen@7593
    59
jorgen@7593
    60
static CGEventRef
jorgen@7593
    61
Cocoa_MouseTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
jorgen@7593
    62
{
slouken@7917
    63
    SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)refcon;
jorgen@7593
    64
    SDL_Mouse *mouse = SDL_GetMouse();
jorgen@7593
    65
    SDL_Window *window = SDL_GetKeyboardFocus();
slouken@8054
    66
    NSWindow *nswindow;
jorgen@7593
    67
    NSRect windowRect;
jorgen@7593
    68
    CGPoint eventLocation;
jorgen@7593
    69
slouken@8986
    70
    switch (type) {
jorgen@7593
    71
        case kCGEventTapDisabledByTimeout:
jorgen@7593
    72
            {
slouken@7917
    73
                CGEventTapEnable(tapdata->tap, true);
jorgen@7593
    74
                return NULL;
jorgen@7593
    75
            }
slouken@10655
    76
        case kCGEventTapDisabledByUserInput:
jorgen@7593
    77
        default:
jorgen@7593
    78
            break;
jorgen@7593
    79
    }
jorgen@7593
    80
jorgen@7593
    81
jorgen@7593
    82
    if (!window || !mouse) {
jorgen@7593
    83
        return event;
jorgen@7593
    84
    }
jorgen@7593
    85
jorgen@7593
    86
    if (mouse->relative_mode) {
jorgen@7593
    87
        return event;
jorgen@7593
    88
    }
jorgen@7593
    89
jorgen@7593
    90
    if (!(window->flags & SDL_WINDOW_INPUT_GRABBED)) {
jorgen@7593
    91
        return event;
jorgen@7593
    92
    }
jorgen@7593
    93
jorgen@7593
    94
    /* This is the same coordinate system as Cocoa uses. */
slouken@8054
    95
    nswindow = ((SDL_WindowData *) window->driverdata)->nswindow;
jorgen@7593
    96
    eventLocation = CGEventGetUnflippedLocation(event);
slouken@8005
    97
    windowRect = [nswindow contentRectForFrameRect:[nswindow frame]];
jorgen@7593
    98
slime73@10159
    99
    if (!NSMouseInRect(NSPointFromCGPoint(eventLocation), windowRect, NO)) {
jorgen@7593
   100
jorgen@7593
   101
        /* This is in CGs global screenspace coordinate system, which has a
jorgen@7593
   102
         * flipped Y.
jorgen@7593
   103
         */
jorgen@7593
   104
        CGPoint newLocation = CGEventGetLocation(event);
jorgen@7593
   105
jorgen@7593
   106
        if (eventLocation.x < NSMinX(windowRect)) {
jorgen@7593
   107
            newLocation.x = NSMinX(windowRect);
jorgen@7593
   108
        } else if (eventLocation.x >= NSMaxX(windowRect)) {
jorgen@7593
   109
            newLocation.x = NSMaxX(windowRect) - 1.0;
jorgen@7593
   110
        }
jorgen@7593
   111
slime73@10159
   112
        if (eventLocation.y <= NSMinY(windowRect)) {
jorgen@7593
   113
            newLocation.y -= (NSMinY(windowRect) - eventLocation.y + 1);
slime73@10159
   114
        } else if (eventLocation.y > NSMaxY(windowRect)) {
slime73@10159
   115
            newLocation.y += (eventLocation.y - NSMaxY(windowRect));
jorgen@7593
   116
        }
jorgen@7593
   117
jorgen@7593
   118
        CGWarpMouseCursorPosition(newLocation);
slime73@10122
   119
        CGAssociateMouseAndMouseCursorPosition(YES);
jorgen@7593
   120
jorgen@7593
   121
        if ((CGEventMaskBit(type) & movementEventsMask) == 0) {
jorgen@7593
   122
            /* For click events, we just constrain the event to the window, so
jorgen@7593
   123
             * no other app receives the click event. We can't due the same to
jorgen@7593
   124
             * movement events, since they mean that our warp cursor above
jorgen@7593
   125
             * behaves strangely.
jorgen@7593
   126
             */
jorgen@7593
   127
            CGEventSetLocation(event, newLocation);
jorgen@7593
   128
        }
jorgen@7593
   129
    }
jorgen@7593
   130
jorgen@7593
   131
    return event;
jorgen@7593
   132
}
jorgen@7593
   133
jorgen@7610
   134
static void
jorgen@7610
   135
SemaphorePostCallback(CFRunLoopTimerRef timer, void *info)
jorgen@7610
   136
{
jorgen@7610
   137
    SDL_SemPost((SDL_sem*)info);
jorgen@7610
   138
}
jorgen@7610
   139
jorgen@7593
   140
static int
jorgen@7593
   141
Cocoa_MouseTapThread(void *data)
jorgen@7593
   142
{
jorgen@7593
   143
    SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)data;
jorgen@7593
   144
slouken@10653
   145
    /* Tap was created on main thread but we own it now. */
slouken@10653
   146
    CFMachPortRef eventTap = tapdata->tap;
jorgen@7593
   147
    if (eventTap) {
jorgen@7593
   148
        /* Try to create a runloop source we can schedule. */
jorgen@7593
   149
        CFRunLoopSourceRef runloopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
jorgen@7593
   150
        if  (runloopSource) {
jorgen@7593
   151
            tapdata->runloopSource = runloopSource;
jorgen@7593
   152
        } else {
jorgen@7593
   153
            CFRelease(eventTap);
jorgen@7593
   154
            SDL_SemPost(tapdata->runloopStartedSemaphore);
jorgen@7593
   155
            /* TODO: Both here and in the return below, set some state in
jorgen@7593
   156
             * tapdata to indicate that initialization failed, which we should
jorgen@7593
   157
             * check in InitMouseEventTap, after we move the semaphore check
jorgen@7593
   158
             * from Quit to Init.
jorgen@7593
   159
             */
jorgen@7593
   160
            return 1;
jorgen@7593
   161
        }
jorgen@7593
   162
    } else {
jorgen@7593
   163
        SDL_SemPost(tapdata->runloopStartedSemaphore);
jorgen@7593
   164
        return 1;
jorgen@7593
   165
    }
jorgen@7593
   166
jorgen@7593
   167
    tapdata->runloop = CFRunLoopGetCurrent();
jorgen@7593
   168
    CFRunLoopAddSource(tapdata->runloop, tapdata->runloopSource, kCFRunLoopCommonModes);
jorgen@7610
   169
    CFRunLoopTimerContext context = {.info = tapdata->runloopStartedSemaphore};
jorgen@7610
   170
    /* We signal the runloop started semaphore *after* the run loop has started, indicating it's safe to CFRunLoopStop it. */
jorgen@7610
   171
    CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent(), 0, 0, 0, &SemaphorePostCallback, &context);
jorgen@7610
   172
    CFRunLoopAddTimer(tapdata->runloop, timer, kCFRunLoopCommonModes);
jorgen@7610
   173
    CFRelease(timer);
jorgen@7593
   174
jorgen@7593
   175
    /* Run the event loop to handle events in the event tap. */
jorgen@7593
   176
    CFRunLoopRun();
jorgen@7593
   177
    /* Make sure this is signaled so that SDL_QuitMouseEventTap knows it can safely SDL_WaitThread for us. */
jorgen@7593
   178
    if (SDL_SemValue(tapdata->runloopStartedSemaphore) < 1) {
jorgen@7593
   179
        SDL_SemPost(tapdata->runloopStartedSemaphore);
jorgen@7593
   180
    }
jorgen@7593
   181
    CFRunLoopRemoveSource(tapdata->runloop, tapdata->runloopSource, kCFRunLoopCommonModes);
jorgen@7593
   182
jorgen@7593
   183
    /* Clean up. */
jorgen@7593
   184
    CGEventTapEnable(tapdata->tap, false);
jorgen@7593
   185
    CFRelease(tapdata->runloopSource);
jorgen@7593
   186
    CFRelease(tapdata->tap);
jorgen@7593
   187
    tapdata->runloopSource = NULL;
jorgen@7593
   188
    tapdata->tap = NULL;
jorgen@7593
   189
jorgen@7593
   190
    return 0;
jorgen@7593
   191
}
jorgen@7593
   192
jorgen@7593
   193
void
jorgen@7593
   194
Cocoa_InitMouseEventTap(SDL_MouseData* driverdata)
jorgen@7593
   195
{
jorgen@7593
   196
    SDL_MouseEventTapData *tapdata;
jorgen@7593
   197
    driverdata->tapdata = SDL_calloc(1, sizeof(SDL_MouseEventTapData));
jorgen@7593
   198
    tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
jorgen@7593
   199
jorgen@7593
   200
    tapdata->runloopStartedSemaphore = SDL_CreateSemaphore(0);
jorgen@7593
   201
    if (tapdata->runloopStartedSemaphore) {
slouken@10653
   202
        tapdata->tap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap,
slouken@10653
   203
                                        kCGEventTapOptionDefault, allGrabbedEventsMask,
slouken@10653
   204
                                        &Cocoa_MouseTapCallback, tapdata);
slouken@10653
   205
        if (tapdata->tap) {
slouken@10654
   206
            /* Tap starts disabled, until app requests mouse grab */
slouken@10654
   207
            CGEventTapEnable(tapdata->tap, false);
slouken@10653
   208
            tapdata->thread = SDL_CreateThreadInternal(&Cocoa_MouseTapThread, "Event Tap Loop", 512 * 1024, tapdata);
slouken@10653
   209
            if (tapdata->thread) {
slouken@10653
   210
                /* Success - early out. Ownership transferred to thread. */
slouken@10653
   211
            	return;
slouken@10653
   212
            }
slouken@10653
   213
            CFRelease(tapdata->tap);
jorgen@7593
   214
        }
slouken@10653
   215
        SDL_DestroySemaphore(tapdata->runloopStartedSemaphore);
jorgen@7593
   216
    }
slouken@10653
   217
    SDL_free(driverdata->tapdata);
slouken@10653
   218
    driverdata->tapdata = NULL;
slouken@10653
   219
}
jorgen@7593
   220
slouken@10653
   221
void
slouken@10653
   222
Cocoa_EnableMouseEventTap(SDL_MouseData *driverdata, SDL_bool enabled)
slouken@10653
   223
{
slouken@10653
   224
    SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
slouken@10653
   225
    if (tapdata && tapdata->tap)
slouken@10653
   226
    {
slouken@10654
   227
        CGEventTapEnable(tapdata->tap, !!enabled);
jorgen@7593
   228
    }
jorgen@7593
   229
}
jorgen@7593
   230
jorgen@7593
   231
void
jorgen@7593
   232
Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata)
jorgen@7593
   233
{
jorgen@7593
   234
    SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
jorgen@7593
   235
    int status;
jorgen@7593
   236
jorgen@7593
   237
    /* Ensure that the runloop has been started first.
jorgen@7593
   238
     * TODO: Move this to InitMouseEventTap, check for error conditions that can
jorgen@7593
   239
     * happen in Cocoa_MouseTapThread, and fall back to the non-EventTap way of
jorgen@7593
   240
     * grabbing the mouse if it fails to Init.
jorgen@7593
   241
     */
jorgen@7593
   242
    status = SDL_SemWaitTimeout(tapdata->runloopStartedSemaphore, 5000);
jorgen@7593
   243
    if (status > -1) {
jorgen@7593
   244
        /* Then stop it, which will cause Cocoa_MouseTapThread to return. */
jorgen@7593
   245
        CFRunLoopStop(tapdata->runloop);
jorgen@7593
   246
        /* And then wait for Cocoa_MouseTapThread to finish cleaning up. It
jorgen@7593
   247
         * releases some of the pointers in tapdata. */
jorgen@7593
   248
        SDL_WaitThread(tapdata->thread, &status);
jorgen@7593
   249
    }
jorgen@7593
   250
jorgen@7593
   251
    SDL_free(driverdata->tapdata);
jorgen@7593
   252
    driverdata->tapdata = NULL;
jorgen@7593
   253
}
jorgen@7593
   254
jorgen@7593
   255
#else /* SDL_MAC_NO_SANDBOX */
jorgen@7593
   256
jorgen@7593
   257
void
jorgen@7593
   258
Cocoa_InitMouseEventTap(SDL_MouseData *unused)
jorgen@7593
   259
{
jorgen@7593
   260
}
jorgen@7593
   261
jorgen@7593
   262
void
slouken@10653
   263
Cocoa_EnableMouseEventTap(SDL_MouseData *driverdata, SDL_bool enabled)
slouken@10653
   264
{
slouken@10653
   265
}
slouken@10653
   266
slouken@10653
   267
void
jorgen@7593
   268
Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata)
jorgen@7593
   269
{
jorgen@7593
   270
}
jorgen@7593
   271
jorgen@7593
   272
#endif /* !SDL_MAC_NO_SANDBOX */
jorgen@7593
   273
jorgen@7593
   274
#endif /* SDL_VIDEO_DRIVER_COCOA */
jorgen@7593
   275
jorgen@7593
   276
/* vi: set ts=4 sw=4 expandtab: */