#include "SDL_internal.h"
#ifdef SDL_VIDEO_DRIVER_COCOA
#include "SDL_cocoavideo.h"
#include "../../events/SDL_events_c.h"
static SDL_Window *FindSDLWindowForNSWindow(NSWindow *win)
{
SDL_Window *sdlwindow = NULL;
SDL_VideoDevice *device = SDL_GetVideoDevice();
if (device && device->windows) {
for (sdlwindow = device->windows; sdlwindow; sdlwindow = sdlwindow->next) {
NSWindow *nswindow = ((__bridge SDL_CocoaWindowData *)sdlwindow->internal).nswindow;
if (win == nswindow) {
return sdlwindow;
}
}
}
return sdlwindow;
}
@interface SDL3Application : NSApplication
- (void)terminate:(id)sender;
- (void)sendEvent:(NSEvent *)theEvent;
+ (void)registerUserDefaults;
@end
@implementation SDL3Application
- (void)terminate:(id)sender
{
SDL_SendQuit();
}
static bool s_bShouldHandleEventsInSDLApplication = false;
static void Cocoa_DispatchEvent(NSEvent *theEvent)
{
SDL_VideoDevice *_this = SDL_GetVideoDevice();
switch ([theEvent type]) {
case NSEventTypeLeftMouseDown:
case NSEventTypeOtherMouseDown:
case NSEventTypeRightMouseDown:
case NSEventTypeLeftMouseUp:
case NSEventTypeOtherMouseUp:
case NSEventTypeRightMouseUp:
case NSEventTypeLeftMouseDragged:
case NSEventTypeRightMouseDragged:
case NSEventTypeOtherMouseDragged: case NSEventTypeMouseMoved:
case NSEventTypeScrollWheel:
case NSEventTypeMouseEntered:
case NSEventTypeMouseExited:
Cocoa_HandleMouseEvent(_this, theEvent);
break;
case NSEventTypeKeyDown:
case NSEventTypeKeyUp:
case NSEventTypeFlagsChanged:
Cocoa_HandleKeyEvent(_this, theEvent);
break;
default:
break;
}
}
- (void)sendEvent:(NSEvent *)theEvent
{
if (s_bShouldHandleEventsInSDLApplication) {
Cocoa_DispatchEvent(theEvent);
}
[super sendEvent:theEvent];
}
+ (void)registerUserDefaults
{
BOOL momentumScrollSupported = (BOOL)SDL_GetHintBoolean(SDL_HINT_MAC_SCROLL_MOMENTUM, false);
BOOL pressAndHoldEnabled = (BOOL)SDL_GetHintBoolean(SDL_HINT_MAC_PRESS_AND_HOLD, true);
NSDictionary *appDefaults = [[NSDictionary alloc] initWithObjectsAndKeys:
[NSNumber numberWithBool:momentumScrollSupported], @"AppleMomentumScrollSupported",
[NSNumber numberWithBool:pressAndHoldEnabled], @"ApplePressAndHoldEnabled",
[NSNumber numberWithBool:YES], @"ApplePersistenceIgnoreState",
nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:appDefaults];
}
@end
@interface NSApplication (NSAppleMenu)
- (void)setAppleMenu:(NSMenu *)menu;
@end
@interface SDL3AppDelegate : NSObject <NSApplicationDelegate>
{
@public
BOOL seenFirstActivate;
}
- (id)init;
- (void)localeDidChange:(NSNotification *)notification;
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context;
- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app;
- (IBAction)menu:(id)sender;
@end
@implementation SDL3AppDelegate : NSObject
- (id)init
{
self = [super init];
if (self) {
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
bool registerActivationHandlers = SDL_GetHintBoolean("SDL_MAC_REGISTER_ACTIVATION_HANDLERS", true);
seenFirstActivate = NO;
if (registerActivationHandlers) {
[center addObserver:self
selector:@selector(windowWillClose:)
name:NSWindowWillCloseNotification
object:nil];
[center addObserver:self
selector:@selector(focusSomeWindow:)
name:NSApplicationDidBecomeActiveNotification
object:nil];
[center addObserver:self
selector:@selector(screenParametersChanged:)
name:NSApplicationDidChangeScreenParametersNotification
object:nil];
}
[center addObserver:self
selector:@selector(localeDidChange:)
name:NSCurrentLocaleDidChangeNotification
object:nil];
[NSApp addObserver:self
forKeyPath:@"effectiveAppearance"
options:NSKeyValueObservingOptionInitial
context:nil];
}
return self;
}
- (void)dealloc
{
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center removeObserver:self name:NSWindowWillCloseNotification object:nil];
[center removeObserver:self name:NSApplicationDidBecomeActiveNotification object:nil];
[center removeObserver:self name:NSApplicationDidChangeScreenParametersNotification object:nil];
[center removeObserver:self name:NSCurrentLocaleDidChangeNotification object:nil];
[NSApp removeObserver:self forKeyPath:@"effectiveAppearance"];
if ([NSApp delegate] == self) {
[[NSAppleEventManager sharedAppleEventManager]
removeEventHandlerForEventClass:kInternetEventClass
andEventID:kAEGetURL];
}
}
- (void)windowWillClose:(NSNotification *)notification
{
NSWindow *win = (NSWindow *)[notification object];
if (![win isKeyWindow]) {
return;
}
if (FindSDLWindowForNSWindow(win) == NULL) {
return;
}
for (NSWindow *window in [NSApp orderedWindows]) {
if (window != win && [window canBecomeKeyWindow]) {
if (![window isOnActiveSpace]) {
continue;
}
[window makeKeyAndOrderFront:self];
return;
}
}
for (NSNumber *num in [NSWindow windowNumbersWithOptions:0]) {
NSWindow *window = [NSApp windowWithWindowNumber:[num integerValue]];
if (window && window != win && [window canBecomeKeyWindow]) {
[window makeKeyAndOrderFront:self];
return;
}
}
}
- (void)focusSomeWindow:(NSNotification *)aNotification
{
SDL_VideoDevice *device;
if (!seenFirstActivate) {
seenFirstActivate = YES;
return;
}
if ([NSApp keyWindow] && FindSDLWindowForNSWindow([NSApp keyWindow]) == NULL) {
return;
}
device = SDL_GetVideoDevice();
if (device && device->windows) {
for (int i = 0; i < device->num_displays; ++i) {
SDL_Window *fullscreen_window = device->displays[i]->fullscreen_window;
if (fullscreen_window) {
if (fullscreen_window->flags & SDL_WINDOW_MINIMIZED) {
SDL_RestoreWindow(fullscreen_window);
}
return;
}
}
}
}
- (void)screenParametersChanged:(NSNotification *)aNotification
{
SDL_VideoDevice *device = SDL_GetVideoDevice();
if (device) {
Cocoa_UpdateDisplays(device);
}
}
- (void)localeDidChange:(NSNotification *)notification
{
SDL_SendLocaleChangedEvent();
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
SDL_SetSystemTheme(Cocoa_GetSystemTheme());
}
- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename
{
return (BOOL)SDL_SendDropFile(NULL, NULL, [filename UTF8String]) && SDL_SendDropComplete(NULL);
}
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
if (!SDL_GetHintBoolean("SDL_MAC_REGISTER_ACTIVATION_HANDLERS", true)) {
return;
}
bool background_app_default = false;
if (@available(macOS 14.0, *)) {
background_app_default = true;
}
if (!SDL_GetHintBoolean(SDL_HINT_MAC_BACKGROUND_APP, background_app_default)) {
for (NSRunningApplication *i in [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.dock"]) {
[i activateWithOptions:NSApplicationActivateIgnoringOtherApps];
break;
}
SDL_Delay(300); [NSApp activateIgnoringOtherApps:YES];
}
[SDL3Application registerUserDefaults];
}
- (void)handleURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent
{
NSString *path = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
SDL_SendDropFile(NULL, NULL, [path UTF8String]);
SDL_SendDropComplete(NULL);
}
- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app
{
return YES;
}
- (IBAction)menu:(id)sender
{
SDL_TrayEntry *entry = [[sender representedObject] pointerValue];
SDL_ClickTrayEntry(entry);
}
@end
static SDL3AppDelegate *appDelegate = nil;
static NSString *GetApplicationName(void)
{
NSString *appName = nil;
const char *metaname = SDL_GetStringProperty(SDL_GetGlobalProperties(), SDL_PROP_APP_METADATA_NAME_STRING, NULL);
if (metaname && *metaname) {
appName = [NSString stringWithUTF8String:metaname];
}
if (!appName) {
appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
if (!appName) {
appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
}
}
if (![appName length]) {
appName = [[NSProcessInfo processInfo] processName];
}
return appName;
}
static bool LoadMainMenuNibIfAvailable(void)
{
NSDictionary *infoDict;
NSString *mainNibFileName;
bool success = false;
infoDict = [[NSBundle mainBundle] infoDictionary];
if (infoDict) {
mainNibFileName = [infoDict valueForKey:@"NSMainNibFile"];
if (mainNibFileName) {
success = [[NSBundle mainBundle] loadNibNamed:mainNibFileName owner:[NSApplication sharedApplication] topLevelObjects:nil];
}
}
return success;
}
static void CreateApplicationMenus(void)
{
NSString *appName;
NSString *title;
NSMenu *appleMenu;
NSMenu *serviceMenu;
NSMenu *windowMenu;
NSMenuItem *menuItem;
NSMenu *mainMenu;
if (NSApp == nil) {
return;
}
mainMenu = [[NSMenu alloc] init];
[NSApp setMainMenu:mainMenu];
appName = GetApplicationName();
appleMenu = [[NSMenu alloc] initWithTitle:@""];
title = [@"About " stringByAppendingString:appName];
[appleMenu addItemWithTitle:title action:@selector(orderFrontStandardAboutPanel:) keyEquivalent:@""];
[appleMenu addItem:[NSMenuItem separatorItem]];
[appleMenu addItemWithTitle:@"Preferences…" action:nil keyEquivalent:@","];
[appleMenu addItem:[NSMenuItem separatorItem]];
serviceMenu = [[NSMenu alloc] initWithTitle:@""];
menuItem = [appleMenu addItemWithTitle:@"Services" action:nil keyEquivalent:@""];
[menuItem setSubmenu:serviceMenu];
[NSApp setServicesMenu:serviceMenu];
[appleMenu addItem:[NSMenuItem separatorItem]];
title = [@"Hide " stringByAppendingString:appName];
[appleMenu addItemWithTitle:title action:@selector(hide:) keyEquivalent:@"h"];
menuItem = [appleMenu addItemWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"];
[menuItem setKeyEquivalentModifierMask:(NSEventModifierFlagOption | NSEventModifierFlagCommand)];
[appleMenu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""];
[appleMenu addItem:[NSMenuItem separatorItem]];
title = [@"Quit " stringByAppendingString:appName];
[appleMenu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"];
menuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""];
[menuItem setSubmenu:appleMenu];
[[NSApp mainMenu] addItem:menuItem];
[NSApp setAppleMenu:appleMenu];
windowMenu = [[NSMenu alloc] initWithTitle:@"Window"];
[windowMenu addItemWithTitle:@"Close" action:@selector(performClose:) keyEquivalent:@"w"];
[windowMenu addItemWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"];
[windowMenu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""];
menuItem = [[NSMenuItem alloc] initWithTitle:@"Toggle Full Screen" action:@selector(toggleFullScreen:) keyEquivalent:@"f"];
[menuItem setKeyEquivalentModifierMask:NSEventModifierFlagControl | NSEventModifierFlagCommand];
[windowMenu addItem:menuItem];
menuItem = [[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""];
[menuItem setSubmenu:windowMenu];
[[NSApp mainMenu] addItem:menuItem];
[NSApp setWindowsMenu:windowMenu];
}
void Cocoa_RegisterApp(void)
{
@autoreleasepool {
if (NSApp == nil) {
[SDL3Application sharedApplication];
SDL_assert(NSApp != nil);
s_bShouldHandleEventsInSDLApplication = true;
if (!SDL_GetHintBoolean(SDL_HINT_MAC_BACKGROUND_APP, false)) {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
}
if ([NSApp mainMenu] == nil) {
bool nibLoaded;
nibLoaded = LoadMainMenuNibIfAvailable();
if (!nibLoaded) {
CreateApplicationMenus();
}
}
[NSApp finishLaunching];
if ([NSApp delegate]) {
[SDL3Application registerUserDefaults];
}
}
if (NSApp && !appDelegate) {
appDelegate = [[SDL3AppDelegate alloc] init];
if (![NSApp delegate]) {
[[NSAppleEventManager sharedAppleEventManager]
setEventHandler:appDelegate
andSelector:@selector(handleURLEvent:withReplyEvent:)
forEventClass:kInternetEventClass
andEventID:kAEGetURL];
[(NSApplication *)NSApp setDelegate:appDelegate];
} else {
appDelegate->seenFirstActivate = YES;
}
}
}
}
Uint64 Cocoa_GetEventTimestamp(NSTimeInterval nsTimestamp)
{
static Uint64 timestamp_offset;
Uint64 timestamp = (Uint64)(nsTimestamp * SDL_NS_PER_SECOND);
Uint64 now = SDL_GetTicksNS();
if (!timestamp_offset) {
timestamp_offset = (now - timestamp);
}
timestamp += timestamp_offset;
if (timestamp > now) {
timestamp_offset -= (timestamp - now);
timestamp = now;
}
return timestamp;
}
int Cocoa_PumpEventsUntilDate(SDL_VideoDevice *_this, NSDate *expiration, bool accumulate)
{
for (SDL_Window *w = _this->windows; w; w = w->next) {
SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)w->internal;
if (data.modal_session) {
[NSApp runModalSession:data.modal_session];
}
}
for (;;) {
NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:expiration inMode:NSDefaultRunLoopMode dequeue:YES];
if (event == nil) {
return 0;
}
if (!s_bShouldHandleEventsInSDLApplication) {
Cocoa_DispatchEvent(event);
}
[NSApp sendEvent:event];
if (!accumulate) {
break;
}
}
return 1;
}
int Cocoa_WaitEventTimeout(SDL_VideoDevice *_this, Sint64 timeoutNS)
{
@autoreleasepool {
if (timeoutNS > 0) {
NSDate *limitDate = [NSDate dateWithTimeIntervalSinceNow:(double)timeoutNS / SDL_NS_PER_SECOND];
return Cocoa_PumpEventsUntilDate(_this, limitDate, false);
} else if (timeoutNS == 0) {
return Cocoa_PumpEventsUntilDate(_this, [NSDate distantPast], false);
} else {
while (Cocoa_PumpEventsUntilDate(_this, [NSDate distantFuture], false) == 0) {
}
}
return 1;
}
}
void Cocoa_PumpEvents(SDL_VideoDevice *_this)
{
@autoreleasepool {
Cocoa_PumpEventsUntilDate(_this, [NSDate distantPast], true);
}
}
void Cocoa_SendWakeupEvent(SDL_VideoDevice *_this, SDL_Window *window)
{
@autoreleasepool {
NSEvent *event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:NSMakePoint(0, 0)
modifierFlags:0
timestamp:0.0
windowNumber:((__bridge SDL_CocoaWindowData *)window->internal).window_number
context:nil
subtype:0
data1:0
data2:0];
[NSApp postEvent:event atStart:YES];
}
}
bool Cocoa_SuspendScreenSaver(SDL_VideoDevice *_this)
{
@autoreleasepool {
SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal;
if (data.screensaver_assertion) {
IOPMAssertionRelease(data.screensaver_assertion);
data.screensaver_assertion = kIOPMNullAssertionID;
}
if (_this->suspend_screensaver) {
IOPMAssertionID assertion = kIOPMNullAssertionID;
NSString *name = [GetApplicationName() stringByAppendingString:@" using SDL_DisableScreenSaver"];
IOPMAssertionCreateWithDescription(kIOPMAssertPreventUserIdleDisplaySleep,
(__bridge CFStringRef)name,
NULL, NULL, NULL, 0, NULL,
&assertion);
data.screensaver_assertion = assertion;
}
}
return true;
}
#endif