1/*
2    SDL - Simple DirectMedia Layer
3    Copyright (C) 1997-2012  Sam Lantinga
4
5    This library is free software; you can redistribute it and/or
6    modify it under the terms of the GNU Library General Public
7    License as published by the Free Software Foundation; either
8    version 2 of the License, or (at your option) any later version.
9
10    This library is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13    Library General Public License for more details.
14
15    You should have received a copy of the GNU Library General Public
16    License along with this library; if not, write to the Free
17    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18
19    Sam Lantinga
20    slouken@libsdl.org
21*/
22#include "SDL_config.h"
23
24#include "SDL_QuartzVideo.h"
25#include "SDL_QuartzWM.h"
26
27
28void QZ_FreeWMCursor     (_THIS, WMcursor *cursor) {
29
30    if ( cursor != NULL ) {
31        [ cursor->nscursor release ];
32        free (cursor);
33    }
34}
35
36WMcursor*    QZ_CreateWMCursor   (_THIS, Uint8 *data, Uint8 *mask,
37                                         int w, int h, int hot_x, int hot_y) {
38    WMcursor *cursor;
39    NSBitmapImageRep *imgrep;
40    NSImage *img;
41    unsigned char *planes[5];
42    int i;
43    NSAutoreleasePool *pool;
44
45    pool = [ [ NSAutoreleasePool alloc ] init ];
46
47    /* Allocate the cursor memory */
48    cursor = (WMcursor *)SDL_malloc(sizeof(WMcursor));
49    if (cursor == NULL) goto outOfMemory;
50
51    /* create the image representation and get the pointers to its storage */
52    imgrep = [ [ [ NSBitmapImageRep alloc ] initWithBitmapDataPlanes: NULL pixelsWide: w pixelsHigh: h bitsPerSample: 1 samplesPerPixel: 2 hasAlpha: YES isPlanar: YES colorSpaceName: NSDeviceWhiteColorSpace bytesPerRow: (w+7)/8 bitsPerPixel: 0 ] autorelease ];
53    if (imgrep == nil) goto outOfMemory;
54    [ imgrep getBitmapDataPlanes: planes ];
55
56    /* copy data and mask, extending the mask to all black pixels because the inversion effect doesn't work with Cocoa's alpha-blended cursors */
57    for (i = 0; i < (w+7)/8*h; i++) {
58        planes[0][i] = data[i] ^ 0xFF;
59        planes[1][i] = mask[i] | data[i];
60    }
61
62    /* create image and cursor */
63    img = [ [ [ NSImage alloc ] initWithSize: NSMakeSize(w, h) ] autorelease ];
64    if (img == nil) goto outOfMemory;
65    [ img addRepresentation: imgrep ];
66    if (system_version < 0x1030) { /* on 10.2, cursors must be 16*16 */
67        if (w > 16 || h > 16) { /* too big: scale it down */
68            [ img setScalesWhenResized: YES ];
69            hot_x = hot_x*16/w;
70            hot_y = hot_y*16/h;
71        }
72        else { /* too small (or just right): extend it (from the bottom left corner, so hot_y must be adjusted) */
73            hot_y += 16 - h;
74        }
75        [ img setSize: NSMakeSize(16, 16) ];
76    }
77    cursor->nscursor = [ [ NSCursor alloc ] initWithImage: img hotSpot: NSMakePoint(hot_x, hot_y) ];
78    if (cursor->nscursor == nil) goto outOfMemory;
79
80    [ pool release ];
81    return(cursor);
82
83outOfMemory:
84    [ pool release ];
85    if (cursor != NULL) SDL_free(cursor);
86    SDL_OutOfMemory();
87    return(NULL);
88}
89
90void QZ_UpdateCursor (_THIS) {
91    BOOL state;
92
93    if (cursor_should_be_visible || !(SDL_GetAppState() & SDL_APPMOUSEFOCUS)) {
94        state = YES;
95    } else {
96        state = NO;
97    }
98    if (state != cursor_visible) {
99        if (state) {
100            [ NSCursor unhide ];
101        } else {
102            [ NSCursor hide ];
103        }
104        cursor_visible = state;
105    }
106}
107
108BOOL QZ_IsMouseInWindow (_THIS) {
109    if (qz_window == nil || (mode_flags & SDL_FULLSCREEN)) return YES; /*fullscreen*/
110    else {
111        NSPoint p = [ qz_window mouseLocationOutsideOfEventStream ];
112        p.y -= 1.0f; /* Apparently y goes from 1 to h, not from 0 to h-1 (i.e. the "location of the mouse" seems to be defined as "the location of the top left corner of the mouse pointer's hot pixel" */
113        return NSPointInRect(p, [ window_view frame ]);
114    }
115}
116
117int QZ_ShowWMCursor (_THIS, WMcursor *cursor) {
118
119    if ( cursor == NULL) {
120        if ( cursor_should_be_visible ) {
121            cursor_should_be_visible = NO;
122            QZ_ChangeGrabState (this, QZ_HIDECURSOR);
123        }
124        QZ_UpdateCursor(this);
125    }
126    else {
127        if ( qz_window != nil && !(mode_flags & SDL_FULLSCREEN) ) {
128            [ qz_window invalidateCursorRectsForView: [ qz_window contentView ] ];
129        }
130        if ( ! cursor_should_be_visible ) {
131            cursor_should_be_visible = YES;
132            QZ_ChangeGrabState (this, QZ_SHOWCURSOR);
133        }
134        [ cursor->nscursor performSelectorOnMainThread:@selector(set) withObject:nil waitUntilDone:NO ];
135        QZ_UpdateCursor(this);
136    }
137
138    return 1;
139}
140
141/*
142    Coordinate conversion functions, for convenience
143    Cocoa sets the origin at the lower left corner of the window/screen
144    SDL, CoreGraphics/WindowServer, and QuickDraw use the origin at the upper left corner
145    The routines were written so they could be called before SetVideoMode() has finished;
146    this might have limited usefulness at the moment, but the extra cost is trivial.
147*/
148
149/* Convert Cocoa screen coordinate to Cocoa window coordinate */
150void QZ_PrivateGlobalToLocal (_THIS, NSPoint *p) {
151
152	if ( ! CGDisplayIsCaptured (display_id) )
153		*p = [ qz_window convertScreenToBase:*p ];
154}
155
156
157/* Convert Cocoa window coordinate to Cocoa screen coordinate */
158void QZ_PrivateLocalToGlobal (_THIS, NSPoint *p) {
159
160	if ( ! CGDisplayIsCaptured (display_id) )
161		*p = [ qz_window convertBaseToScreen:*p ];
162}
163
164/* Convert SDL coordinate to Cocoa coordinate */
165void QZ_PrivateSDLToCocoa (_THIS, NSPoint *p) {
166
167    if ( CGDisplayIsCaptured (display_id) ) { /* capture signals fullscreen */
168
169        p->y = CGDisplayPixelsHigh (display_id) - p->y;
170    }
171    else {
172
173        *p = [ window_view convertPoint:*p toView: nil ];
174        p->y = [window_view frame].size.height - p->y;
175    }
176}
177
178/* Convert Cocoa coordinate to SDL coordinate */
179void QZ_PrivateCocoaToSDL (_THIS, NSPoint *p) {
180
181    if ( CGDisplayIsCaptured (display_id) ) { /* capture signals fullscreen */
182
183        p->y = CGDisplayPixelsHigh (display_id) - p->y;
184    }
185    else {
186
187        *p = [ window_view convertPoint:*p fromView: nil ];
188        p->y = [window_view frame].size.height - p->y;
189    }
190}
191
192/* Convert SDL coordinate to window server (CoreGraphics) coordinate */
193CGPoint QZ_PrivateSDLToCG (_THIS, NSPoint *p) {
194
195    CGPoint cgp;
196
197    if ( ! CGDisplayIsCaptured (display_id) ) { /* not captured => not fullscreen => local coord */
198
199        int height;
200
201        QZ_PrivateSDLToCocoa (this, p);
202        QZ_PrivateLocalToGlobal (this, p);
203
204        height = CGDisplayPixelsHigh (display_id);
205        p->y = height - p->y;
206    }
207
208    cgp.x = p->x;
209    cgp.y = p->y;
210
211    return cgp;
212}
213
214#if 0 /* Dead code */
215/* Convert window server (CoreGraphics) coordinate to SDL coordinate */
216void QZ_PrivateCGToSDL (_THIS, NSPoint *p) {
217
218    if ( ! CGDisplayIsCaptured (display_id) ) { /* not captured => not fullscreen => local coord */
219
220        int height;
221
222        /* Convert CG Global to Cocoa Global */
223        height = CGDisplayPixelsHigh (display_id);
224        p->y = height - p->y;
225
226        QZ_PrivateGlobalToLocal (this, p);
227        QZ_PrivateCocoaToSDL (this, p);
228    }
229}
230#endif /* Dead code */
231
232void  QZ_PrivateWarpCursor (_THIS, int x, int y) {
233    NSPoint p;
234    CGPoint cgp;
235
236    p = NSMakePoint (x, y);
237    cgp = QZ_PrivateSDLToCG (this, &p);
238
239    /* this is the magic call that fixes cursor "freezing" after warp */
240    CGAssociateMouseAndMouseCursorPosition (0);
241    CGWarpMouseCursorPosition (cgp);
242    if (grab_state != QZ_INVISIBLE_GRAB) { /* can't leave it disassociated? */
243        CGAssociateMouseAndMouseCursorPosition (1);
244    }
245    SDL_PrivateAppActive (QZ_IsMouseInWindow (this), SDL_APPMOUSEFOCUS);
246}
247
248void QZ_WarpWMCursor (_THIS, Uint16 x, Uint16 y) {
249
250    /* Only allow warping when in foreground */
251    if ( ! [ NSApp isActive ] )
252        return;
253
254    /* Do the actual warp */
255    if (grab_state != QZ_INVISIBLE_GRAB) QZ_PrivateWarpCursor (this, x, y);
256
257    /* Generate the mouse moved event */
258    SDL_PrivateMouseMotion (0, 0, x, y);
259}
260
261void QZ_MoveWMCursor     (_THIS, int x, int y) { }
262void QZ_CheckMouseMode   (_THIS) { }
263
264void QZ_SetCaption    (_THIS, const char *title, const char *icon) {
265
266    if ( qz_window != nil ) {
267        NSString *string;
268        if ( title != NULL ) {
269            string = [ [ NSString alloc ] initWithUTF8String:title ];
270            [ qz_window setTitle:string ];
271            [ string release ];
272        }
273        if ( icon != NULL ) {
274            string = [ [ NSString alloc ] initWithUTF8String:icon ];
275            [ qz_window setMiniwindowTitle:string ];
276            [ string release ];
277        }
278    }
279}
280
281void QZ_SetWindowPos (_THIS, int  x, int  y)
282{
283    if ( qz_window == nil ) {
284        //printf( "%s(%d,%d): called for NULL window\n", __FUNCTION__, x, y );
285        return;
286    }
287
288    [ qz_window setFrameTopLeftPoint:NSMakePoint( x, this->hidden->height - y ) ];
289    //printf( "%s(%d,%d): done\n", __FUNCTION__, x, y );
290}
291
292void  QZ_GetWindowPos(_THIS, int  *px, int  *py)
293{
294    NSPoint  pt;
295
296    *px = *py = 0;
297
298    if ( qz_window == NULL ) {
299        //printf( "%s: called on NULL window\n", __FUNCTION__ );
300    }
301
302    if ( qz_window != nil ) {
303        NSRect  rect = [ qz_window frame ];
304        *px = rect.origin.x;
305        *py = this->hidden->height - rect.origin.y - rect.size.height;
306        //printf( "%s: returning (%d,%d)\n", __FUNCTION__, *px, *py );
307    }
308}
309
310/* determine if the window is fully visible on the current screen configuration */
311int  QZ_IsWindowVisible(_THIS, int  recenter)
312{
313    int  result = 0;
314
315    //printf( "... enter %s\n", __FUNCTION__ );
316
317    if ( qz_window != NULL ) {
318        NSRect        frame   = [ qz_window frame ];
319        NSArray*      screens = [ NSScreen screens ];
320        unsigned int  count   = [ screens count ];
321        unsigned int  n;
322        //printf( "window frame (%d,%d) (%d,%d)\n", frame.origin.x, frame.origin.y,
323        //        frame.size.width, frame.size.height );
324        for (n = 0; n < count; n++) {
325            NSScreen*  screen = [ screens objectAtIndex: n ];
326            NSRect     vis    = [ screen visibleFrame ];
327
328            //printf( "screen %d/%d  frame (%d,%d) (%d,%d)\n", n+1, count,
329            //        vis.origin.x, vis.origin.y, vis.size.width, vis.size.height );
330
331            if (frame.origin.x >= vis.origin.x &&
332                frame.origin.x + frame.size.width <= vis.origin.x + vis.size.width &&
333                frame.origin.y >= vis.origin.y &&
334                frame.origin.y + frame.size.height <= vis.origin.y + vis.size.height )
335            {
336                result = 1;
337                break;
338            }
339        }
340    }
341    //printf ( "... exit %s, result = %d\n", __FUNCTION__, result );
342    if ( !result && recenter ) {
343        [ qz_window center ] ;
344    }
345    return result;
346}
347
348int QZ_GetMonitorDPI(_THIS, int  *xDpi, int *yDpi)
349{
350    /* FIXME: how to get this information from Cocoa ? */
351    return -1;
352}
353
354int QZ_GetMonitorRect   (_THIS, SDL_Rect  *rect)
355{
356    NSWindow*     window = qz_window;
357    NSRect        frame   = [ window frame ];
358    int           fx1     = frame.origin.x;
359    int           fy1     = frame.origin.y;
360    int           fx2     = frame.size.width + fx1;
361    int           fy2     = frame.size.height + fy1;
362    NSArray*      screens = [ NSScreen screens ];
363    unsigned int  count   = [ screens count ];
364    int           bestScreen = -1;
365    int           bestArea = 0;
366
367    unsigned int  n;
368
369    /* we need to compute which screen has the most window pixels */
370    for (n = 0; n < count; n++) {
371        NSScreen*  screen = [ screens objectAtIndex: n ];
372        NSRect     vis    = [ screen visibleFrame ];
373        int        vx1    = vis.origin.x;
374        int        vy1    = vis.origin.y;
375        int        vx2    = vis.size.width + vx1;
376        int        vy2    = vis.size.height + vy1;
377        int        cx1, cx2, cy1, cy2, cArea;
378
379        if (fx1 >= vx2 || vx1 >= fx2 || fy1 >= vy2 || vy1 >= fy2)
380            continue;
381
382        cx1 = (fx1 < vx1) ? vx1 : fx1;
383        cx2 = (fx2 > vx2) ? vx2 : fx2;
384        cy1 = (fy1 < vy1) ? vy1 : fy1;
385        cy2 = (fy2 > vy2) ? vy2 : fy2;
386
387        if (cx1 >= cx2 || cy1 >= cy2)
388            continue;
389
390        cArea = (cx2-cx1)*(cy2-cy1);
391
392        if (bestScreen < 0 || cArea > bestArea) {
393            bestScreen = n;
394            bestArea   = cArea;
395        }
396    }
397    if (bestScreen < 0)
398        bestScreen = 0;
399
400    {
401        NSScreen*  screen = [ screens objectAtIndex: bestScreen ];
402        NSRect     vis    = [ screen visibleFrame ];
403
404        rect->x = vis.origin.x;
405        rect->y = vis.origin.y;
406        rect->w = vis.size.width;
407        rect->h = vis.size.height;
408    }
409    return 0;
410}
411
412void QZ_SetIcon       (_THIS, SDL_Surface *icon, Uint8 *mask)
413{
414    NSBitmapImageRep *imgrep;
415    NSImage *img;
416    SDL_Surface *mergedSurface;
417    NSAutoreleasePool *pool;
418    Uint8 *pixels;
419    SDL_bool iconSrcAlpha;
420    Uint8 iconAlphaValue;
421    int i, j, maskPitch, index;
422
423    pool = [ [ NSAutoreleasePool alloc ] init ];
424
425    imgrep = [ [ [ NSBitmapImageRep alloc ] initWithBitmapDataPlanes: NULL pixelsWide: icon->w pixelsHigh: icon->h bitsPerSample: 8 samplesPerPixel: 4 hasAlpha: YES isPlanar: NO colorSpaceName: NSDeviceRGBColorSpace bytesPerRow: 4*icon->w bitsPerPixel: 32 ] autorelease ];
426    if (imgrep == nil) goto freePool;
427    pixels = [ imgrep bitmapData ];
428    SDL_memset(pixels, 0, 4*icon->w*icon->h); /* make the background, which will survive in colorkeyed areas, completely transparent */
429
430#if SDL_BYTEORDER == SDL_BIG_ENDIAN
431#define BYTEORDER_DEPENDENT_RGBA_MASKS 0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF
432#else
433#define BYTEORDER_DEPENDENT_RGBA_MASKS 0x000000FF, 0x0000FF00, 0x00FF0000, 0xFF000000
434#endif
435    mergedSurface = SDL_CreateRGBSurfaceFrom(pixels, icon->w, icon->h, 32, 4*icon->w, BYTEORDER_DEPENDENT_RGBA_MASKS);
436    if (mergedSurface == NULL) goto freePool;
437
438    /* blit, with temporarily cleared SRCALPHA flag because we want to copy, not alpha-blend */
439    iconSrcAlpha = ((icon->flags & SDL_SRCALPHA) != 0);
440    iconAlphaValue = icon->format->alpha;
441    SDL_SetAlpha(icon, 0, 255);
442    SDL_BlitSurface(icon, NULL, mergedSurface, NULL);
443    if (iconSrcAlpha) SDL_SetAlpha(icon, SDL_SRCALPHA, iconAlphaValue);
444
445    SDL_FreeSurface(mergedSurface);
446
447    /* apply mask, source alpha, and premultiply color values by alpha */
448    maskPitch = (icon->w+7)/8;
449    for (i = 0; i < icon->h; i++) {
450        for (j = 0; j < icon->w; j++) {
451            index = i*4*icon->w + j*4;
452            if (!(mask[i*maskPitch + j/8] & (128 >> j%8))) {
453                pixels[index + 3] = 0;
454            }
455            else {
456                if (iconSrcAlpha) {
457                    if (icon->format->Amask == 0) pixels[index + 3] = icon->format->alpha;
458                }
459                else {
460                    pixels[index + 3] = 255;
461                }
462            }
463            if (pixels[index + 3] < 255) {
464                pixels[index + 0] = (Uint16)pixels[index + 0]*pixels[index + 3]/255;
465                pixels[index + 1] = (Uint16)pixels[index + 1]*pixels[index + 3]/255;
466                pixels[index + 2] = (Uint16)pixels[index + 2]*pixels[index + 3]/255;
467            }
468        }
469    }
470
471    img = [ [ [ NSImage alloc ] initWithSize: NSMakeSize(icon->w, icon->h) ] autorelease ];
472    if (img == nil) goto freePool;
473    [ img addRepresentation: imgrep ];
474    [ NSApp setApplicationIconImage:img ];
475
476freePool:
477    [ pool release ];
478}
479
480int  QZ_IconifyWindow (_THIS) {
481
482    if ( ! [ qz_window isMiniaturized ] ) {
483        [ qz_window miniaturize:nil ];
484        if ( ! [ qz_window isMiniaturized ] ) {
485            SDL_SetError ("window iconification failed");
486            return 0;
487        }
488        return 1;
489    }
490    else {
491        SDL_SetError ("window already iconified");
492        return 0;
493    }
494}
495
496int  QZ_GetWMInfo  (_THIS, SDL_SysWMinfo *info) {
497    info->nsWindowPtr = qz_window;
498    return 0;
499}
500
501void QZ_ChangeGrabState (_THIS, int action) {
502
503    /*
504        Figure out what the next state should be based on the action.
505        Ignore actions that can't change the current state.
506    */
507    if ( grab_state == QZ_UNGRABBED ) {
508        if ( action == QZ_ENABLE_GRAB ) {
509            if ( cursor_should_be_visible )
510                grab_state = QZ_VISIBLE_GRAB;
511            else
512                grab_state = QZ_INVISIBLE_GRAB;
513        }
514    }
515    else if ( grab_state == QZ_VISIBLE_GRAB ) {
516        if ( action == QZ_DISABLE_GRAB )
517            grab_state = QZ_UNGRABBED;
518        else if ( action == QZ_HIDECURSOR )
519            grab_state = QZ_INVISIBLE_GRAB;
520    }
521    else {
522        assert( grab_state == QZ_INVISIBLE_GRAB );
523
524        if ( action == QZ_DISABLE_GRAB )
525            grab_state = QZ_UNGRABBED;
526        else if ( action == QZ_SHOWCURSOR )
527            grab_state = QZ_VISIBLE_GRAB;
528    }
529
530    /* now apply the new state */
531    if (grab_state == QZ_UNGRABBED) {
532
533        CGAssociateMouseAndMouseCursorPosition (1);
534    }
535    else if (grab_state == QZ_VISIBLE_GRAB) {
536
537        CGAssociateMouseAndMouseCursorPosition (1);
538    }
539    else {
540        assert( grab_state == QZ_INVISIBLE_GRAB );
541
542        QZ_PrivateWarpCursor (this, SDL_VideoSurface->w / 2, SDL_VideoSurface->h / 2);
543        CGAssociateMouseAndMouseCursorPosition (0);
544    }
545}
546
547SDL_GrabMode QZ_GrabInput (_THIS, SDL_GrabMode grab_mode) {
548
549    int doGrab = grab_mode & SDL_GRAB_ON;
550    /*int fullscreen = grab_mode & SDL_GRAB_FULLSCREEN;*/
551
552    if ( this->screen == NULL ) {
553        SDL_SetError ("QZ_GrabInput: screen is NULL");
554        return SDL_GRAB_OFF;
555    }
556
557    if ( ! video_set ) {
558        /*SDL_SetError ("QZ_GrabInput: video is not set, grab will take effect on mode switch"); */
559        current_grab_mode = grab_mode;
560        return grab_mode;       /* Will be set later on mode switch */
561    }
562
563    if ( grab_mode != SDL_GRAB_QUERY ) {
564        if ( doGrab )
565            QZ_ChangeGrabState (this, QZ_ENABLE_GRAB);
566        else
567            QZ_ChangeGrabState (this, QZ_DISABLE_GRAB);
568
569        current_grab_mode = doGrab ? SDL_GRAB_ON : SDL_GRAB_OFF;
570        QZ_UpdateCursor(this);
571    }
572
573    return current_grab_mode;
574}
575