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
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            {
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
137static void
138SemaphorePostCallback(CFRunLoopTimerRef timer, void *info)
139{
140    SDL_SemPost((SDL_sem*)info);
141}
142
143static int
144Cocoa_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
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->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
224void
225Cocoa_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
234void
235Cocoa_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
267void
268Cocoa_InitMouseEventTap(SDL_MouseData *unused)
269{
270}
271
272void
273Cocoa_EnableMouseEventTap(SDL_MouseData *driverdata, SDL_bool enabled)
274{
275}
276
277void
278Cocoa_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: */
287