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#include "SDL_timer.h"
25
26#include "SDL_cocoavideo.h"
27#include "../../events/SDL_events_c.h"
28#include "SDL_assert.h"
29#include "SDL_hints.h"
30
31/* This define was added in the 10.9 SDK. */
32#ifndef kIOPMAssertPreventUserIdleDisplaySleep
33#define kIOPMAssertPreventUserIdleDisplaySleep kIOPMAssertionTypePreventUserIdleDisplaySleep
34#endif
35
36@interface SDLApplication : NSApplication
37
38- (void)terminate:(id)sender;
39- (void)sendEvent:(NSEvent *)theEvent;
40
41@end
42
43@implementation SDLApplication
44
45// Override terminate to handle Quit and System Shutdown smoothly.
46- (void)terminate:(id)sender
47{
48    SDL_SendQuit();
49}
50
51static SDL_bool s_bShouldHandleEventsInSDLApplication = SDL_FALSE;
52
53static void Cocoa_DispatchEvent(NSEvent *theEvent)
54{
55    SDL_VideoDevice *_this = SDL_GetVideoDevice();
56
57    switch ([theEvent type]) {
58        case NSLeftMouseDown:
59        case NSOtherMouseDown:
60        case NSRightMouseDown:
61        case NSLeftMouseUp:
62        case NSOtherMouseUp:
63        case NSRightMouseUp:
64        case NSLeftMouseDragged:
65        case NSRightMouseDragged:
66        case NSOtherMouseDragged: /* usually middle mouse dragged */
67        case NSMouseMoved:
68        case NSScrollWheel:
69            Cocoa_HandleMouseEvent(_this, theEvent);
70            break;
71        case NSKeyDown:
72        case NSKeyUp:
73        case NSFlagsChanged:
74            Cocoa_HandleKeyEvent(_this, theEvent);
75            break;
76        default:
77            break;
78    }
79}
80
81// Dispatch events here so that we can handle events caught by
82// nextEventMatchingMask in SDL, as well as events caught by other
83// processes (such as CEF) that are passed down to NSApp.
84- (void)sendEvent:(NSEvent *)theEvent
85{
86    if (s_bShouldHandleEventsInSDLApplication) {
87        Cocoa_DispatchEvent(theEvent);
88    }
89
90    [super sendEvent:theEvent];
91}
92
93@end // SDLApplication
94
95/* setAppleMenu disappeared from the headers in 10.4 */
96@interface NSApplication(NSAppleMenu)
97- (void)setAppleMenu:(NSMenu *)menu;
98@end
99
100@interface SDLAppDelegate : NSObject <NSApplicationDelegate> {
101@public
102    BOOL seenFirstActivate;
103}
104
105- (id)init;
106@end
107
108@implementation SDLAppDelegate : NSObject
109- (id)init
110{
111    self = [super init];
112    if (self) {
113        NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
114
115        seenFirstActivate = NO;
116
117        [center addObserver:self
118                   selector:@selector(windowWillClose:)
119                       name:NSWindowWillCloseNotification
120                     object:nil];
121
122        [center addObserver:self
123                   selector:@selector(focusSomeWindow:)
124                       name:NSApplicationDidBecomeActiveNotification
125                     object:nil];
126    }
127
128    return self;
129}
130
131- (void)dealloc
132{
133    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
134
135    [center removeObserver:self name:NSWindowWillCloseNotification object:nil];
136    [center removeObserver:self name:NSApplicationDidBecomeActiveNotification object:nil];
137
138    [super dealloc];
139}
140
141- (void)windowWillClose:(NSNotification *)notification;
142{
143    NSWindow *win = (NSWindow*)[notification object];
144
145    if (![win isKeyWindow]) {
146        return;
147    }
148
149    /* HACK: Make the next window in the z-order key when the key window is
150     * closed. The custom event loop and/or windowing code we have seems to
151     * prevent the normal behavior: https://bugzilla.libsdl.org/show_bug.cgi?id=1825
152     */
153
154    /* +[NSApp orderedWindows] never includes the 'About' window, but we still
155     * want to try its list first since the behavior in other apps is to only
156     * make the 'About' window key if no other windows are on-screen.
157     */
158    for (NSWindow *window in [NSApp orderedWindows]) {
159        if (window != win && [window canBecomeKeyWindow]) {
160            if (![window isOnActiveSpace]) {
161                continue;
162            }
163            [window makeKeyAndOrderFront:self];
164            return;
165        }
166    }
167
168    /* If a window wasn't found above, iterate through all visible windows in
169     * the active Space in z-order (including the 'About' window, if it's shown)
170     * and make the first one key.
171     */
172    for (NSNumber *num in [NSWindow windowNumbersWithOptions:0]) {
173        NSWindow *window = [NSApp windowWithWindowNumber:[num integerValue]];
174        if (window && window != win && [window canBecomeKeyWindow]) {
175            [window makeKeyAndOrderFront:self];
176            return;
177        }
178    }
179}
180
181- (void)focusSomeWindow:(NSNotification *)aNotification
182{
183    /* HACK: Ignore the first call. The application gets a
184     * applicationDidBecomeActive: a little bit after the first window is
185     * created, and if we don't ignore it, a window that has been created with
186     * SDL_WINDOW_MINIMIZED will ~immediately be restored.
187     */
188    if (!seenFirstActivate) {
189        seenFirstActivate = YES;
190        return;
191    }
192
193    SDL_VideoDevice *device = SDL_GetVideoDevice();
194    if (device && device->windows) {
195        SDL_Window *window = device->windows;
196        int i;
197        for (i = 0; i < device->num_displays; ++i) {
198            SDL_Window *fullscreen_window = device->displays[i].fullscreen_window;
199            if (fullscreen_window) {
200                if (fullscreen_window->flags & SDL_WINDOW_MINIMIZED) {
201                    SDL_RestoreWindow(fullscreen_window);
202                }
203                return;
204            }
205        }
206
207        if (window->flags & SDL_WINDOW_MINIMIZED) {
208            SDL_RestoreWindow(window);
209        } else {
210            SDL_RaiseWindow(window);
211        }
212    }
213}
214
215- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename
216{
217    return (BOOL)SDL_SendDropFile(NULL, [filename UTF8String]) && SDL_SendDropComplete(NULL);
218}
219@end
220
221static SDLAppDelegate *appDelegate = nil;
222
223static NSString *
224GetApplicationName(void)
225{
226    NSString *appName;
227
228    /* Determine the application name */
229    appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
230    if (!appName) {
231        appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
232    }
233
234    if (![appName length]) {
235        appName = [[NSProcessInfo processInfo] processName];
236    }
237
238    return appName;
239}
240
241static void
242CreateApplicationMenus(void)
243{
244    NSString *appName;
245    NSString *title;
246    NSMenu *appleMenu;
247    NSMenu *serviceMenu;
248    NSMenu *windowMenu;
249    NSMenu *viewMenu;
250    NSMenuItem *menuItem;
251    NSMenu *mainMenu;
252
253    if (NSApp == nil) {
254        return;
255    }
256
257    mainMenu = [[NSMenu alloc] init];
258
259    /* Create the main menu bar */
260    [NSApp setMainMenu:mainMenu];
261
262    [mainMenu release];  /* we're done with it, let NSApp own it. */
263    mainMenu = nil;
264
265    /* Create the application menu */
266    appName = GetApplicationName();
267    appleMenu = [[NSMenu alloc] initWithTitle:@""];
268
269    /* Add menu items */
270    title = [@"About " stringByAppendingString:appName];
271    [appleMenu addItemWithTitle:title action:@selector(orderFrontStandardAboutPanel:) keyEquivalent:@""];
272
273    [appleMenu addItem:[NSMenuItem separatorItem]];
274
275    [appleMenu addItemWithTitle:@"Preferences…" action:nil keyEquivalent:@","];
276
277    [appleMenu addItem:[NSMenuItem separatorItem]];
278
279    serviceMenu = [[NSMenu alloc] initWithTitle:@""];
280    menuItem = (NSMenuItem *)[appleMenu addItemWithTitle:@"Services" action:nil keyEquivalent:@""];
281    [menuItem setSubmenu:serviceMenu];
282
283    [NSApp setServicesMenu:serviceMenu];
284    [serviceMenu release];
285
286    [appleMenu addItem:[NSMenuItem separatorItem]];
287
288    title = [@"Hide " stringByAppendingString:appName];
289    [appleMenu addItemWithTitle:title action:@selector(hide:) keyEquivalent:@"h"];
290
291    menuItem = (NSMenuItem *)[appleMenu addItemWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"];
292    [menuItem setKeyEquivalentModifierMask:(NSAlternateKeyMask|NSCommandKeyMask)];
293
294    [appleMenu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""];
295
296    [appleMenu addItem:[NSMenuItem separatorItem]];
297
298    title = [@"Quit " stringByAppendingString:appName];
299    [appleMenu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"];
300
301    /* Put menu into the menubar */
302    menuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""];
303    [menuItem setSubmenu:appleMenu];
304    [[NSApp mainMenu] addItem:menuItem];
305    [menuItem release];
306
307    /* Tell the application object that this is now the application menu */
308    [NSApp setAppleMenu:appleMenu];
309    [appleMenu release];
310
311
312    /* Create the window menu */
313    windowMenu = [[NSMenu alloc] initWithTitle:@"Window"];
314
315    /* Add menu items */
316    [windowMenu addItemWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"];
317
318    [windowMenu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""];
319
320    /* Put menu into the menubar */
321    menuItem = [[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""];
322    [menuItem setSubmenu:windowMenu];
323    [[NSApp mainMenu] addItem:menuItem];
324    [menuItem release];
325
326    /* Tell the application object that this is now the window menu */
327    [NSApp setWindowsMenu:windowMenu];
328    [windowMenu release];
329
330
331    /* Add the fullscreen view toggle menu option, if supported */
332    if (floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_6) {
333        /* Create the view menu */
334        viewMenu = [[NSMenu alloc] initWithTitle:@"View"];
335
336        /* Add menu items */
337        menuItem = [viewMenu addItemWithTitle:@"Toggle Full Screen" action:@selector(toggleFullScreen:) keyEquivalent:@"f"];
338        [menuItem setKeyEquivalentModifierMask:NSControlKeyMask | NSCommandKeyMask];
339
340        /* Put menu into the menubar */
341        menuItem = [[NSMenuItem alloc] initWithTitle:@"View" action:nil keyEquivalent:@""];
342        [menuItem setSubmenu:viewMenu];
343        [[NSApp mainMenu] addItem:menuItem];
344        [menuItem release];
345
346        [viewMenu release];
347    }
348}
349
350void
351Cocoa_RegisterApp(void)
352{ @autoreleasepool
353{
354    /* This can get called more than once! Be careful what you initialize! */
355
356    if (NSApp == nil) {
357        [SDLApplication sharedApplication];
358        SDL_assert(NSApp != nil);
359
360        s_bShouldHandleEventsInSDLApplication = SDL_TRUE;
361
362        if (!SDL_GetHintBoolean(SDL_HINT_MAC_BACKGROUND_APP, SDL_FALSE)) {
363            [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
364            [NSApp activateIgnoringOtherApps:YES];
365		}
366
367        if ([NSApp mainMenu] == nil) {
368            CreateApplicationMenus();
369        }
370        [NSApp finishLaunching];
371        NSDictionary *appDefaults = [[NSDictionary alloc] initWithObjectsAndKeys:
372            [NSNumber numberWithBool:NO], @"AppleMomentumScrollSupported",
373            [NSNumber numberWithBool:NO], @"ApplePressAndHoldEnabled",
374            [NSNumber numberWithBool:YES], @"ApplePersistenceIgnoreState",
375            nil];
376        [[NSUserDefaults standardUserDefaults] registerDefaults:appDefaults];
377        [appDefaults release];
378    }
379    if (NSApp && !appDelegate) {
380        appDelegate = [[SDLAppDelegate alloc] init];
381
382        /* If someone else has an app delegate, it means we can't turn a
383         * termination into SDL_Quit, and we can't handle application:openFile:
384         */
385        if (![NSApp delegate]) {
386            [(NSApplication *)NSApp setDelegate:appDelegate];
387        } else {
388            appDelegate->seenFirstActivate = YES;
389        }
390    }
391}}
392
393void
394Cocoa_PumpEvents(_THIS)
395{ @autoreleasepool
396{
397    /* Update activity every 30 seconds to prevent screensaver */
398    SDL_VideoData *data = (SDL_VideoData *)_this->driverdata;
399    if (_this->suspend_screensaver && !data->screensaver_use_iopm) {
400        Uint32 now = SDL_GetTicks();
401        if (!data->screensaver_activity ||
402            SDL_TICKS_PASSED(now, data->screensaver_activity + 30000)) {
403            UpdateSystemActivity(UsrActivity);
404            data->screensaver_activity = now;
405        }
406    }
407
408    for ( ; ; ) {
409        NSEvent *event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES ];
410        if ( event == nil ) {
411            break;
412        }
413
414        if (!s_bShouldHandleEventsInSDLApplication) {
415            Cocoa_DispatchEvent(event);
416        }
417
418        // Pass events down to SDLApplication to be handled in sendEvent:
419        [NSApp sendEvent:event];
420    }
421}}
422
423void
424Cocoa_SuspendScreenSaver(_THIS)
425{ @autoreleasepool
426{
427    SDL_VideoData *data = (SDL_VideoData *)_this->driverdata;
428
429    if (!data->screensaver_use_iopm) {
430        return;
431    }
432
433    if (data->screensaver_assertion) {
434        IOPMAssertionRelease(data->screensaver_assertion);
435        data->screensaver_assertion = 0;
436    }
437
438    if (_this->suspend_screensaver) {
439        /* FIXME: this should ideally describe the real reason why the game
440         * called SDL_DisableScreenSaver. Note that the name is only meant to be
441         * seen by OS X power users. there's an additional optional human-readable
442         * (localized) reason parameter which we don't set.
443         */
444        NSString *name = [GetApplicationName() stringByAppendingString:@" using SDL_DisableScreenSaver"];
445        IOPMAssertionCreateWithDescription(kIOPMAssertPreventUserIdleDisplaySleep,
446                                           (CFStringRef) name,
447                                           NULL, NULL, NULL, 0, NULL,
448                                           &data->screensaver_assertion);
449    }
450}}
451
452#endif /* SDL_VIDEO_DRIVER_COCOA */
453
454/* vi: set ts=4 sw=4 expandtab: */
455