src/video/cocoa/SDL_cocoamouse.m
author Alex Szpakowski <slime73@gmail.com>
Sat, 24 Sep 2016 13:28:40 -0300
changeset 10365 7f3be5258f80
parent 10159 d41acf6379f6
child 10737 3406a0f8b041
permissions -rw-r--r--
Fix mouse wheel events on macOS 10.12 (thanks Eric Wasylishen!)

Fixes bug #3432
     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 
    67 static SDL_Cursor *
    68 Cocoa_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 
    87 static SDL_Cursor *
    88 Cocoa_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 
   112 static SDL_Cursor *
   113 Cocoa_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 
   171 static void
   172 Cocoa_FreeCursor(SDL_Cursor * cursor)
   173 { @autoreleasepool
   174 {
   175     NSCursor *nscursor = (NSCursor *)cursor->driverdata;
   176 
   177     [nscursor release];
   178     SDL_free(cursor);
   179 }}
   180 
   181 static int
   182 Cocoa_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 
   198 static SDL_Window *
   199 SDL_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 
   213 static int
   214 Cocoa_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 
   254 static void
   255 Cocoa_WarpMouse(SDL_Window * window, int x, int y)
   256 {
   257     Cocoa_WarpMouseGlobal(x + window->x, y + window->y);
   258 }
   259 
   260 static int
   261 Cocoa_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 
   302 static int
   303 Cocoa_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 
   310 static Uint32
   311 Cocoa_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 
   335 void
   336 Cocoa_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 
   362 void
   363 Cocoa_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 
   419 void
   420 Cocoa_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 
   447 void
   448 Cocoa_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 
   461 void
   462 Cocoa_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: */