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_assert.h"
26#include "SDL_events.h"
27#include "SDL_cocoamouse.h"
28#include "SDL_cocoamousetap.h"
29
30#include "../../events/SDL_mouse_c.h"
31
32/* #define DEBUG_COCOAMOUSE */
33
34#ifdef DEBUG_COCOAMOUSE
35#define DLog(fmt, ...) printf("%s: " fmt "\n", __func__, ##__VA_ARGS__)
36#else
37#define DLog(...) do { } while (0)
38#endif
39
40@implementation NSCursor (InvisibleCursor)
41+ (NSCursor *)invisibleCursor
42{
43    static NSCursor *invisibleCursor = NULL;
44    if (!invisibleCursor) {
45        /* RAW 16x16 transparent GIF */
46        static unsigned char cursorBytes[] = {
47            0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0x10, 0x00, 0x10, 0x00, 0x80,
48            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04,
49            0x01, 0x00, 0x00, 0x01, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x10,
50            0x00, 0x10, 0x00, 0x00, 0x02, 0x0E, 0x8C, 0x8F, 0xA9, 0xCB, 0xED,
51            0x0F, 0xA3, 0x9C, 0xB4, 0xDA, 0x8B, 0xB3, 0x3E, 0x05, 0x00, 0x3B
52        };
53
54        NSData *cursorData = [NSData dataWithBytesNoCopy:&cursorBytes[0]
55                                                  length:sizeof(cursorBytes)
56                                            freeWhenDone:NO];
57        NSImage *cursorImage = [[[NSImage alloc] initWithData:cursorData] autorelease];
58        invisibleCursor = [[NSCursor alloc] initWithImage:cursorImage
59                                                  hotSpot:NSZeroPoint];
60    }
61
62    return invisibleCursor;
63}
64@end
65
66
67static SDL_Cursor *
68Cocoa_CreateDefaultCursor()
69{ @autoreleasepool
70{
71    NSCursor *nscursor;
72    SDL_Cursor *cursor = NULL;
73
74    nscursor = [NSCursor arrowCursor];
75
76    if (nscursor) {
77        cursor = SDL_calloc(1, sizeof(*cursor));
78        if (cursor) {
79            cursor->driverdata = nscursor;
80            [nscursor retain];
81        }
82    }
83
84    return cursor;
85}}
86
87static SDL_Cursor *
88Cocoa_CreateCursor(SDL_Surface * surface, int hot_x, int hot_y)
89{ @autoreleasepool
90{
91    NSImage *nsimage;
92    NSCursor *nscursor = NULL;
93    SDL_Cursor *cursor = NULL;
94
95    nsimage = Cocoa_CreateImage(surface);
96    if (nsimage) {
97        nscursor = [[NSCursor alloc] initWithImage: nsimage hotSpot: NSMakePoint(hot_x, hot_y)];
98    }
99
100    if (nscursor) {
101        cursor = SDL_calloc(1, sizeof(*cursor));
102        if (cursor) {
103            cursor->driverdata = nscursor;
104        } else {
105            [nscursor release];
106        }
107    }
108
109    return cursor;
110}}
111
112static SDL_Cursor *
113Cocoa_CreateSystemCursor(SDL_SystemCursor id)
114{ @autoreleasepool
115{
116    NSCursor *nscursor = NULL;
117    SDL_Cursor *cursor = NULL;
118
119    switch(id) {
120    case SDL_SYSTEM_CURSOR_ARROW:
121        nscursor = [NSCursor arrowCursor];
122        break;
123    case SDL_SYSTEM_CURSOR_IBEAM:
124        nscursor = [NSCursor IBeamCursor];
125        break;
126    case SDL_SYSTEM_CURSOR_WAIT:
127        nscursor = [NSCursor arrowCursor];
128        break;
129    case SDL_SYSTEM_CURSOR_CROSSHAIR:
130        nscursor = [NSCursor crosshairCursor];
131        break;
132    case SDL_SYSTEM_CURSOR_WAITARROW:
133        nscursor = [NSCursor arrowCursor];
134        break;
135    case SDL_SYSTEM_CURSOR_SIZENWSE:
136    case SDL_SYSTEM_CURSOR_SIZENESW:
137        nscursor = [NSCursor closedHandCursor];
138        break;
139    case SDL_SYSTEM_CURSOR_SIZEWE:
140        nscursor = [NSCursor resizeLeftRightCursor];
141        break;
142    case SDL_SYSTEM_CURSOR_SIZENS:
143        nscursor = [NSCursor resizeUpDownCursor];
144        break;
145    case SDL_SYSTEM_CURSOR_SIZEALL:
146        nscursor = [NSCursor closedHandCursor];
147        break;
148    case SDL_SYSTEM_CURSOR_NO:
149        nscursor = [NSCursor operationNotAllowedCursor];
150        break;
151    case SDL_SYSTEM_CURSOR_HAND:
152        nscursor = [NSCursor pointingHandCursor];
153        break;
154    default:
155        SDL_assert(!"Unknown system cursor");
156        return NULL;
157    }
158
159    if (nscursor) {
160        cursor = SDL_calloc(1, sizeof(*cursor));
161        if (cursor) {
162            /* We'll free it later, so retain it here */
163            [nscursor retain];
164            cursor->driverdata = nscursor;
165        }
166    }
167
168    return cursor;
169}}
170
171static void
172Cocoa_FreeCursor(SDL_Cursor * cursor)
173{ @autoreleasepool
174{
175    NSCursor *nscursor = (NSCursor *)cursor->driverdata;
176
177    [nscursor release];
178    SDL_free(cursor);
179}}
180
181static int
182Cocoa_ShowCursor(SDL_Cursor * cursor)
183{ @autoreleasepool
184{
185    SDL_VideoDevice *device = SDL_GetVideoDevice();
186    SDL_Window *window = (device ? device->windows : NULL);
187    for (; window != NULL; window = window->next) {
188        SDL_WindowData *driverdata = (SDL_WindowData *)window->driverdata;
189        if (driverdata) {
190            [driverdata->nswindow performSelectorOnMainThread:@selector(invalidateCursorRectsForView:)
191                                                   withObject:[driverdata->nswindow contentView]
192                                                waitUntilDone:NO];
193        }
194    }
195    return 0;
196}}
197
198static SDL_Window *
199SDL_FindWindowAtPoint(const int x, const int y)
200{
201    const SDL_Point pt = { x, y };
202    SDL_Window *i;
203    for (i = SDL_GetVideoDevice()->windows; i; i = i->next) {
204        const SDL_Rect r = { i->x, i->y, i->w, i->h };
205        if (SDL_PointInRect(&pt, &r)) {
206            return i;
207        }
208    }
209
210    return NULL;
211}
212
213static int
214Cocoa_WarpMouseGlobal(int x, int y)
215{
216    SDL_Mouse *mouse = SDL_GetMouse();
217    if (mouse->focus) {
218        SDL_WindowData *data = (SDL_WindowData *) mouse->focus->driverdata;
219        if ([data->listener isMoving]) {
220            DLog("Postponing warp, window being moved.");
221            [data->listener setPendingMoveX:x Y:y];
222            return 0;
223        }
224    }
225    const CGPoint point = CGPointMake((float)x, (float)y);
226
227    Cocoa_HandleMouseWarp(point.x, point.y);
228
229    CGWarpMouseCursorPosition(point);
230
231    /* CGWarpMouse causes a short delay by default, which is preventable by
232     * Calling this directly after. CGSetLocalEventsSuppressionInterval can also
233     * prevent it, but it's deprecated as of OS X 10.6.
234     */
235    if (!mouse->relative_mode) {
236        CGAssociateMouseAndMouseCursorPosition(YES);
237    }
238
239    /* CGWarpMouseCursorPosition doesn't generate a window event, unlike our
240     * other implementations' APIs. Send what's appropriate.
241     */
242    if (!mouse->relative_mode) {
243        SDL_Window *win = SDL_FindWindowAtPoint(x, y);
244        SDL_SetMouseFocus(win);
245        if (win) {
246            SDL_assert(win == mouse->focus);
247            SDL_SendMouseMotion(win, mouse->mouseID, 0, x - win->x, y - win->y);
248        }
249    }
250
251    return 0;
252}
253
254static void
255Cocoa_WarpMouse(SDL_Window * window, int x, int y)
256{
257    Cocoa_WarpMouseGlobal(x + window->x, y + window->y);
258}
259
260static int
261Cocoa_SetRelativeMouseMode(SDL_bool enabled)
262{
263    /* We will re-apply the relative mode when the window gets focus, if it
264     * doesn't have focus right now.
265     */
266    SDL_Window *window = SDL_GetMouseFocus();
267    if (!window) {
268      return 0;
269    }
270
271    /* We will re-apply the relative mode when the window finishes being moved,
272     * if it is being moved right now.
273     */
274    SDL_WindowData *data = (SDL_WindowData *) window->driverdata;
275    if ([data->listener isMoving]) {
276        return 0;
277    }
278
279    CGError result;
280    if (enabled) {
281        DLog("Turning on.");
282        result = CGAssociateMouseAndMouseCursorPosition(NO);
283    } else {
284        DLog("Turning off.");
285        result = CGAssociateMouseAndMouseCursorPosition(YES);
286    }
287    if (result != kCGErrorSuccess) {
288        return SDL_SetError("CGAssociateMouseAndMouseCursorPosition() failed");
289    }
290
291    /* The hide/unhide calls are redundant most of the time, but they fix
292     * https://bugzilla.libsdl.org/show_bug.cgi?id=2550
293     */
294    if (enabled) {
295        [NSCursor hide];
296    } else {
297        [NSCursor unhide];
298    }
299    return 0;
300}
301
302static int
303Cocoa_CaptureMouse(SDL_Window *window)
304{
305    /* our Cocoa event code already tracks the mouse outside the window,
306        so all we have to do here is say "okay" and do what we always do. */
307    return 0;
308}
309
310static Uint32
311Cocoa_GetGlobalMouseState(int *x, int *y)
312{
313    const NSUInteger cocoaButtons = [NSEvent pressedMouseButtons];
314    const NSPoint cocoaLocation = [NSEvent mouseLocation];
315    Uint32 retval = 0;
316
317    for (NSScreen *screen in [NSScreen screens]) {
318        NSRect frame = [screen frame];
319        if (NSMouseInRect(cocoaLocation, frame, NO)) {
320            *x = (int) cocoaLocation.x;
321            *y = (int) ((frame.origin.y + frame.size.height) - cocoaLocation.y);
322            break;
323        }
324    }
325
326    retval |= (cocoaButtons & (1 << 0)) ? SDL_BUTTON_LMASK : 0;
327    retval |= (cocoaButtons & (1 << 1)) ? SDL_BUTTON_RMASK : 0;
328    retval |= (cocoaButtons & (1 << 2)) ? SDL_BUTTON_MMASK : 0;
329    retval |= (cocoaButtons & (1 << 3)) ? SDL_BUTTON_X1MASK : 0;
330    retval |= (cocoaButtons & (1 << 4)) ? SDL_BUTTON_X2MASK : 0;
331
332    return retval;
333}
334
335void
336Cocoa_InitMouse(_THIS)
337{
338    SDL_Mouse *mouse = SDL_GetMouse();
339
340    mouse->driverdata = SDL_calloc(1, sizeof(SDL_MouseData));
341
342    mouse->CreateCursor = Cocoa_CreateCursor;
343    mouse->CreateSystemCursor = Cocoa_CreateSystemCursor;
344    mouse->ShowCursor = Cocoa_ShowCursor;
345    mouse->FreeCursor = Cocoa_FreeCursor;
346    mouse->WarpMouse = Cocoa_WarpMouse;
347    mouse->WarpMouseGlobal = Cocoa_WarpMouseGlobal;
348    mouse->SetRelativeMouseMode = Cocoa_SetRelativeMouseMode;
349    mouse->CaptureMouse = Cocoa_CaptureMouse;
350    mouse->GetGlobalMouseState = Cocoa_GetGlobalMouseState;
351
352    SDL_SetDefaultCursor(Cocoa_CreateDefaultCursor());
353
354    Cocoa_InitMouseEventTap(mouse->driverdata);
355
356    SDL_MouseData *driverdata = (SDL_MouseData*)mouse->driverdata;
357    const NSPoint location =  [NSEvent mouseLocation];
358    driverdata->lastMoveX = location.x;
359    driverdata->lastMoveY = location.y;
360}
361
362void
363Cocoa_HandleMouseEvent(_THIS, NSEvent *event)
364{
365    switch ([event type]) {
366        case NSMouseMoved:
367        case NSLeftMouseDragged:
368        case NSRightMouseDragged:
369        case NSOtherMouseDragged:
370            break;
371
372        default:
373            /* Ignore any other events. */
374            return;
375    }
376
377    SDL_Mouse *mouse = SDL_GetMouse();
378    SDL_MouseData *driverdata = (SDL_MouseData*)mouse->driverdata;
379    if (!driverdata) {
380        return;  /* can happen when returning from fullscreen Space on shutdown */
381    }
382
383    const SDL_bool seenWarp = driverdata->seenWarp;
384    driverdata->seenWarp = NO;
385
386    const NSPoint location =  [NSEvent mouseLocation];
387    const CGFloat lastMoveX = driverdata->lastMoveX;
388    const CGFloat lastMoveY = driverdata->lastMoveY;
389    driverdata->lastMoveX = location.x;
390    driverdata->lastMoveY = location.y;
391    DLog("Last seen mouse: (%g, %g)", location.x, location.y);
392
393    /* Non-relative movement is handled in -[Cocoa_WindowListener mouseMoved:] */
394    if (!mouse->relative_mode) {
395        return;
396    }
397
398    /* Ignore events that aren't inside the client area (i.e. title bar.) */
399    if ([event window]) {
400        NSRect windowRect = [[[event window] contentView] frame];
401        if (!NSMouseInRect([event locationInWindow], windowRect, NO)) {
402            return;
403        }
404    }
405
406    float deltaX = [event deltaX];
407    float deltaY = [event deltaY];
408
409    if (seenWarp) {
410        deltaX += (lastMoveX - driverdata->lastWarpX);
411        deltaY += ((CGDisplayPixelsHigh(kCGDirectMainDisplay) - lastMoveY) - driverdata->lastWarpY);
412
413        DLog("Motion was (%g, %g), offset to (%g, %g)", [event deltaX], [event deltaY], deltaX, deltaY);
414    }
415
416    SDL_SendMouseMotion(mouse->focus, mouse->mouseID, 1, (int)deltaX, (int)deltaY);
417}
418
419void
420Cocoa_HandleMouseWheel(SDL_Window *window, NSEvent *event)
421{
422    SDL_Mouse *mouse = SDL_GetMouse();
423
424    CGFloat x = -[event deltaX];
425    CGFloat y = [event deltaY];
426    SDL_MouseWheelDirection direction = SDL_MOUSEWHEEL_NORMAL;
427
428    if ([event respondsToSelector:@selector(isDirectionInvertedFromDevice)]) {
429        if ([event isDirectionInvertedFromDevice] == YES) {
430            direction = SDL_MOUSEWHEEL_FLIPPED;
431        }
432    }
433
434    if (x > 0) {
435        x = SDL_ceil(x);
436    } else if (x < 0) {
437        x = SDL_floor(x);
438    }
439    if (y > 0) {
440        y = SDL_ceil(y);
441    } else if (y < 0) {
442        y = SDL_floor(y);
443    }
444    SDL_SendMouseWheel(window, mouse->mouseID, (int)x, (int)y, direction);
445}
446
447void
448Cocoa_HandleMouseWarp(CGFloat x, CGFloat y)
449{
450    /* This makes Cocoa_HandleMouseEvent ignore the delta caused by the warp,
451     * since it gets included in the next movement event.
452     */
453    SDL_MouseData *driverdata = (SDL_MouseData*)SDL_GetMouse()->driverdata;
454    driverdata->lastWarpX = x;
455    driverdata->lastWarpY = y;
456    driverdata->seenWarp = SDL_TRUE;
457
458    DLog("(%g, %g)", x, y);
459}
460
461void
462Cocoa_QuitMouse(_THIS)
463{
464    SDL_Mouse *mouse = SDL_GetMouse();
465    if (mouse) {
466        if (mouse->driverdata) {
467            Cocoa_QuitMouseEventTap(((SDL_MouseData*)mouse->driverdata));
468        }
469
470        SDL_free(mouse->driverdata);
471    }
472}
473
474#endif /* SDL_VIDEO_DRIVER_COCOA */
475
476/* vi: set ts=4 sw=4 expandtab: */
477