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
40typedef struct {
41    CFMachPortRef tap;
42    CFRunLoopRef runloop;
43    CFRunLoopSourceRef runloopSource;
44    SDL_Thread *thread;
45    SDL_sem *runloopStartedSemaphore;
46} SDL_MouseEventTapData;
47
48static const CGEventMask movementEventsMask =
49      CGEventMaskBit(kCGEventLeftMouseDragged)
50    | CGEventMaskBit(kCGEventRightMouseDragged)
51    | CGEventMaskBit(kCGEventMouseMoved);
52
53static 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
60static CGEventRef
61Cocoa_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        case kCGEventTapDisabledByUserInput:
73            {
74                CGEventTapEnable(tapdata->tap, true);
75                return NULL;
76            }
77        default:
78            break;
79    }
80
81
82    if (!window || !mouse) {
83        return event;
84    }
85
86    if (mouse->relative_mode) {
87        return event;
88    }
89
90    if (!(window->flags & SDL_WINDOW_INPUT_GRABBED)) {
91        return event;
92    }
93
94    /* This is the same coordinate system as Cocoa uses. */
95    nswindow = ((SDL_WindowData *) window->driverdata)->nswindow;
96    eventLocation = CGEventGetUnflippedLocation(event);
97    windowRect = [nswindow contentRectForFrameRect:[nswindow frame]];
98
99    if (!NSMouseInRect(NSPointFromCGPoint(eventLocation), windowRect, NO)) {
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));
116        }
117
118        CGWarpMouseCursorPosition(newLocation);
119        CGAssociateMouseAndMouseCursorPosition(YES);
120
121        if ((CGEventMaskBit(type) & movementEventsMask) == 0) {
122            /* For click events, we just constrain the event to the window, so
123             * no other app receives the click event. We can't due the same to
124             * movement events, since they mean that our warp cursor above
125             * behaves strangely.
126             */
127            CGEventSetLocation(event, newLocation);
128        }
129    }
130
131    return event;
132}
133
134static void
135SemaphorePostCallback(CFRunLoopTimerRef timer, void *info)
136{
137    SDL_SemPost((SDL_sem*)info);
138}
139
140static int
141Cocoa_MouseTapThread(void *data)
142{
143    SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)data;
144
145    /* Create a tap. */
146    CFMachPortRef eventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap,
147                                              kCGEventTapOptionDefault, allGrabbedEventsMask,
148                                              &Cocoa_MouseTapCallback, tapdata);
149    if (eventTap) {
150        /* Try to create a runloop source we can schedule. */
151        CFRunLoopSourceRef runloopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
152        if  (runloopSource) {
153            tapdata->tap = eventTap;
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
196void
197Cocoa_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->thread = SDL_CreateThreadInternal(&Cocoa_MouseTapThread, "Event Tap Loop", 512 * 1024, tapdata);
206        if (!tapdata->thread) {
207            SDL_DestroySemaphore(tapdata->runloopStartedSemaphore);
208        }
209    }
210
211    if (!tapdata->thread) {
212        SDL_free(driverdata->tapdata);
213        driverdata->tapdata = NULL;
214    }
215}
216
217void
218Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata)
219{
220    SDL_MouseEventTapData *tapdata = (SDL_MouseEventTapData*)driverdata->tapdata;
221    int status;
222
223    /* Ensure that the runloop has been started first.
224     * TODO: Move this to InitMouseEventTap, check for error conditions that can
225     * happen in Cocoa_MouseTapThread, and fall back to the non-EventTap way of
226     * grabbing the mouse if it fails to Init.
227     */
228    status = SDL_SemWaitTimeout(tapdata->runloopStartedSemaphore, 5000);
229    if (status > -1) {
230        /* Then stop it, which will cause Cocoa_MouseTapThread to return. */
231        CFRunLoopStop(tapdata->runloop);
232        /* And then wait for Cocoa_MouseTapThread to finish cleaning up. It
233         * releases some of the pointers in tapdata. */
234        SDL_WaitThread(tapdata->thread, &status);
235    }
236
237    SDL_free(driverdata->tapdata);
238    driverdata->tapdata = NULL;
239}
240
241#else /* SDL_MAC_NO_SANDBOX */
242
243void
244Cocoa_InitMouseEventTap(SDL_MouseData *unused)
245{
246}
247
248void
249Cocoa_QuitMouseEventTap(SDL_MouseData *driverdata)
250{
251}
252
253#endif /* !SDL_MAC_NO_SANDBOX */
254
255#endif /* SDL_VIDEO_DRIVER_COCOA */
256
257/* vi: set ts=4 sw=4 expandtab: */
258