From ba7c0b897bda025a7d10d46de3b92ee4e8bb58bf Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Mon, 6 Apr 2026 12:46:29 -0400 Subject: [PATCH] Add the Cocoa notification driver Supported on macOS 10.14+ and iOS. --- Xcode/SDL/SDL.xcodeproj/project.pbxproj | 44 +++ src/SDL.c | 5 + src/notification/SDL_notification_c.h | 4 + .../cocoa/SDL_cocoanotification.m | 360 ++++++++++++++++++ .../dummy/SDL_dummynotification.c | 7 + 5 files changed, 420 insertions(+) create mode 100644 src/notification/cocoa/SDL_cocoanotification.m diff --git a/Xcode/SDL/SDL.xcodeproj/project.pbxproj b/Xcode/SDL/SDL.xcodeproj/project.pbxproj index 33664dcc30..6712bb71c1 100644 --- a/Xcode/SDL/SDL.xcodeproj/project.pbxproj +++ b/Xcode/SDL/SDL.xcodeproj/project.pbxproj @@ -58,6 +58,9 @@ 1485C3312BBA4AF30063985B /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1485C32F2BBA4A0C0063985B /* UniformTypeIdentifiers.framework */; platformFilters = (maccatalyst, macos, ); settings = {ATTRIBUTES = (Weak, ); }; }; 3AFD09EA2F9766BA00208BA9 /* SDL_CurvedUIShader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFD09E92F9766BA00208BA9 /* SDL_CurvedUIShader.swift */; platformFilters = (xros, ); }; 557D0CFA254586CA003913E3 /* CoreHaptics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F37DC5F225350EBC0002E6F7 /* CoreHaptics.framework */; platformFilters = (ios, maccatalyst, macos, tvos, xros, ); settings = {ATTRIBUTES = (Weak, ); }; }; + 30840D4C2F76A822000F1D1B /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30840D4B2F76A822000F1D1B /* UserNotifications.framework */; }; + 30840D4E2F76A8E7000F1D1B /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30840D4D2F76A8E7000F1D1B /* Security.framework */; }; + 557D0CFA254586CA003913E3 /* CoreHaptics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F37DC5F225350EBC0002E6F7 /* CoreHaptics.framework */; platformFilters = (ios, maccatalyst, macos, tvos, ); settings = {ATTRIBUTES = (Weak, ); }; }; 557D0CFB254586D7003913E3 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A75FDABD23E28B6200529352 /* GameController.framework */; settings = {ATTRIBUTES = (Required, ); }; }; 5616CA4C252BB2A6005D5928 /* SDL_url.c in Sources */ = {isa = PBXBuildFile; fileRef = 5616CA49252BB2A5005D5928 /* SDL_url.c */; }; 5616CA4D252BB2A6005D5928 /* SDL_sysurl.h in Headers */ = {isa = PBXBuildFile; fileRef = 5616CA4A252BB2A6005D5928 /* SDL_sysurl.h */; }; @@ -566,6 +569,12 @@ F3FD042E2C9B755700824C4C /* SDL_hidapi_nintendo.h in Headers */ = {isa = PBXBuildFile; fileRef = F3FD042C2C9B755700824C4C /* SDL_hidapi_nintendo.h */; }; F3FD042F2C9B755700824C4C /* SDL_hidapi_steam_hori.c in Sources */ = {isa = PBXBuildFile; fileRef = F3FD042D2C9B755700824C4C /* SDL_hidapi_steam_hori.c */; }; FA73671D19A540EF004122E4 /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA73671C19A540EF004122E4 /* CoreVideo.framework */; platformFilters = (ios, maccatalyst, macos, tvos, xros, ); settings = {ATTRIBUTES = (Required, ); }; }; + FA73671D19A540EF004122E4 /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA73671C19A540EF004122E4 /* CoreVideo.framework */; platformFilters = (ios, maccatalyst, macos, tvos, ); settings = {ATTRIBUTES = (Required, ); }; }; + 00009183ED11C92F23FC0000 /* SDL_notification.c in Sources */ = {isa = PBXBuildFile; fileRef = 000059D16599F687D87B0000 /* SDL_notification.c */; }; + 0000B6DBAE1F178E87010000 /* SDL_notification_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 000045BF87FD2865AB1C0000 /* SDL_notification_c.h */; }; + 0000FB5C9B8CE5929A250000 /* SDL_cocoanotification.m in Sources */ = {isa = PBXBuildFile; fileRef = 000088B5AFB11E40F7920000 /* SDL_cocoanotification.m */; }; + 0000AF1D2CED20010C2D0000 /* SDL_notificationevents.c in Sources */ = {isa = PBXBuildFile; fileRef = 0000D6CCB566B8AE47FE0000 /* SDL_notificationevents.c */; }; + 00004A19C923458228C10000 /* SDL_notificationevents_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 0000FA7334391F6720820000 /* SDL_notificationevents_c.h */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -624,6 +633,8 @@ 02D6A1C128A84B8F00A7F001 /* SDL_hidapi_sinput.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_sinput.c; sourceTree = ""; }; 1485C32F2BBA4A0C0063985B /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; 3AFD09E92F9766BA00208BA9 /* SDL_CurvedUIShader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDL_CurvedUIShader.swift; sourceTree = ""; }; + 30840D4B2F76A822000F1D1B /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; + 30840D4D2F76A8E7000F1D1B /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 5616CA49252BB2A5005D5928 /* SDL_url.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_url.c; sourceTree = ""; }; 5616CA4A252BB2A6005D5928 /* SDL_sysurl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_sysurl.h; sourceTree = ""; }; 5616CA4B252BB2A6005D5928 /* SDL_sysurl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDL_sysurl.m; sourceTree = ""; }; @@ -1168,6 +1179,11 @@ F59C710600D5CB5801000001 /* SDL.info */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = text; path = SDL.info; sourceTree = ""; }; F5A2EF3900C6A39A01000001 /* BUGS.txt */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = text; name = BUGS.txt; path = ../../BUGS.txt; sourceTree = SOURCE_ROOT; }; FA73671C19A540EF004122E4 /* CoreVideo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreVideo.framework; path = System/Library/Frameworks/CoreVideo.framework; sourceTree = SDKROOT; }; + 000059D16599F687D87B0000 /* SDL_notification.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = SDL_notification.c; path = SDL_notification.c; sourceTree = ""; }; + 000045BF87FD2865AB1C0000 /* SDL_notification_c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDL_notification_c.h; path = SDL_notification_c.h; sourceTree = ""; }; + 000088B5AFB11E40F7920000 /* SDL_cocoanotification.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SDL_cocoanotification.m; path = SDL_cocoanotification.m; sourceTree = ""; }; + 0000D6CCB566B8AE47FE0000 /* SDL_notificationevents.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = SDL_notificationevents.c; path = SDL_notificationevents.c; sourceTree = ""; }; + 0000FA7334391F6720820000 /* SDL_notificationevents_c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDL_notificationevents_c.h; path = SDL_notificationevents_c.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1176,11 +1192,13 @@ buildActionMask = 2147483647; files = ( 1485C3312BBA4AF30063985B /* UniformTypeIdentifiers.framework in Frameworks */, + 30840D4E2F76A8E7000F1D1B /* Security.framework in Frameworks */, A7381E971D8B6A0300B177DD /* AudioToolbox.framework in Frameworks */, 00D0D0D810675E46004B05EF /* Carbon.framework in Frameworks */, 007317A40858DECD00B2BC32 /* Cocoa.framework in Frameworks */, A7381E961D8B69D600B177DD /* CoreAudio.framework in Frameworks */, 557D0CFA254586CA003913E3 /* CoreHaptics.framework in Frameworks */, + 30840D4C2F76A822000F1D1B /* UserNotifications.framework in Frameworks */, 00D0D08410675DD9004B05EF /* CoreFoundation.framework in Frameworks */, FA73671D19A540EF004122E4 /* CoreVideo.framework in Frameworks */, 00CFA89D106B4BA100758660 /* ForceFeedback.framework in Frameworks */, @@ -1445,6 +1463,7 @@ F3E5A6EA2AD5E0E600293D83 /* SDL_properties.c */, F386F6E62884663E001840AA /* SDL_utils.c */, F386F6E52884663E001840AA /* SDL_utils_c.h */, + 0000A12B47E1FC5391780000 /* notification */, ); name = "Library Source"; path = ../../src; @@ -1474,6 +1493,8 @@ 564624341FF821B70074AC87 /* Frameworks */ = { isa = PBXGroup; children = ( + 30840D4D2F76A8E7000F1D1B /* Security.framework */, + 30840D4B2F76A822000F1D1B /* UserNotifications.framework */, 1485C32F2BBA4A0C0063985B /* UniformTypeIdentifiers.framework */, F382339B2738ED6600F7F527 /* CoreBluetooth.framework */, F376F7272559B77100CFC0BC /* CoreAudio.framework */, @@ -2330,6 +2351,8 @@ A7D8A93723E2514000DCD162 /* SDL_touch_c.h */, A7D8A92F23E2514000DCD162 /* SDL_windowevents.c */, A7D8A94323E2514000DCD162 /* SDL_windowevents_c.h */, + 0000D6CCB566B8AE47FE0000 /* SDL_notificationevents.c */, + 0000FA7334391F6720820000 /* SDL_notificationevents_c.h */, ); path = events; sourceTree = ""; @@ -2551,6 +2574,24 @@ path = resources; sourceTree = ""; }; + 0000A12B47E1FC5391780000 /* notification */ = { + isa = PBXGroup; + children = ( + 000059D16599F687D87B0000 /* SDL_notification.c */, + 000045BF87FD2865AB1C0000 /* SDL_notification_c.h */, + 0000B071CC4D6AB5CE640000 /* cocoa */, + ); + path = notification; + sourceTree = ""; + }; + 0000B071CC4D6AB5CE640000 /* cocoa */ = { + isa = PBXGroup; + children = ( + 000088B5AFB11E40F7920000 /* SDL_cocoanotification.m */, + ); + path = cocoa; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -3199,6 +3240,9 @@ 0000A03C0F32C43816F40000 /* SDL_asyncio_windows_ioring.c in Sources */, 0000A877C7DB9FA935FC0000 /* SDL_uikitpen.m in Sources */, 63124A422E5C357500A53610 /* SDL_hidapi_zuiki.c in Sources */, + 00009183ED11C92F23FC0000 /* SDL_notification.c in Sources */, + 0000FB5C9B8CE5929A250000 /* SDL_cocoanotification.m in Sources */, + 0000AF1D2CED20010C2D0000 /* SDL_notificationevents.c in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/src/SDL.c b/src/SDL.c index ab814bd318..d3b3b09c93 100644 --- a/src/SDL.c +++ b/src/SDL.c @@ -359,6 +359,11 @@ bool SDL_InitSubSystem(SDL_InitFlags flags) SDL_DBus_Init(); #endif +#ifdef SDL_PLATFORM_APPLE + // Apple platforms require the notification delegate to be registered early. + Cocoa_RegisterNotificationDelegate(); +#endif + #ifdef SDL_PLATFORM_WINDOWS if (flags & (SDL_INIT_HAPTIC | SDL_INIT_JOYSTICK)) { if (!SDL_HelperWindowCreate()) { diff --git a/src/notification/SDL_notification_c.h b/src/notification/SDL_notification_c.h index d4b72aa348..0bdf367ad6 100644 --- a/src/notification/SDL_notification_c.h +++ b/src/notification/SDL_notification_c.h @@ -27,6 +27,10 @@ extern SDL_NotificationID SDL_SYS_ShowNotification(SDL_PropertiesID props); extern void SDL_CleanupNotifications(); +#ifdef SDL_PLATFORM_APPLE +extern void Cocoa_RegisterNotificationDelegate(); +#endif + #ifdef SDL_VIDEO_DRIVER_WAYLAND extern const char *SDL_GetNotificationActivationToken(); #endif diff --git a/src/notification/cocoa/SDL_cocoanotification.m b/src/notification/cocoa/SDL_cocoanotification.m new file mode 100644 index 0000000000..c826e9875c --- /dev/null +++ b/src/notification/cocoa/SDL_cocoanotification.m @@ -0,0 +1,360 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" + +#include "../SDL_notification_c.h" + +// tvOS doesn't support the notification features SDL cares about. +#ifndef SDL_PLATFORM_TVOS + +#include "../../events/SDL_notificationevents_c.h" +#include "../../video/SDL_surface_c.h" + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +@interface SDLNotificationDelegate : NSObject +@end + +@implementation SDLNotificationDelegate +- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler + API_AVAILABLE(macos(10.14)) +{ + if (@available(macOS 11, iOS 14, *)) { + completionHandler(UNNotificationPresentationOptionBanner + UNNotificationPresentationOptionSound); + } else { + completionHandler(UNNotificationPresentationOptionAlert + UNNotificationPresentationOptionSound); + } +} + +- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler + API_AVAILABLE(macos(10.14)) +{ + NSString *SDL_Identifier = @"SDL_LocalNotification-"; + NSString *identifier = [[[response notification] request] identifier]; + // const char *identifier = [[[[response notification] request] identifier] UTF8String]; + SDL_NotificationID id = 0; + + if ([identifier compare:SDL_Identifier options:0 range:NSMakeRange(0, [SDL_Identifier length])] == 0) { + id = (SDL_NotificationID)[[identifier substringFromIndex:[SDL_Identifier length]] integerValue]; + } + + if (id) { + NSString *action_id = [response actionIdentifier]; + if (action_id) { + if ([action_id isEqualToString:UNNotificationDefaultActionIdentifier]) { + SDL_SendNotificationAction(id, "default"); + } else { + if ([action_id length] != 0) { + SDL_SendNotificationAction(id, [action_id UTF8String]); + } + } + } + } + + completionHandler(); +} +@end + +API_AVAILABLE(macos(10.14)) +static UNUserNotificationCenter *center; +static SDLNotificationDelegate *delegate; + +static bool ShouldEnableNotifications() +{ +#if defined(SDL_PLATFORM_MACOS) + /* Notifications outside of an app bundle are unsupported, and will crash with an + * unhandled exception error, deep within a system library. + * + * FIXME: These functions are deprecated, find a modern way. + */ + CFBundleRef bundle = CFBundleGetMainBundle(); + CFURLRef bundleUrl = CFBundleCopyBundleURL(bundle); + + CFStringRef uti; + if (CFURLCopyResourcePropertyForKey(bundleUrl, kCFURLTypeIdentifierKey, &uti, NULL) && + uti && UTTypeConformsTo(uti, kUTTypeApplicationBundle)) { + return true; + } + + return false; +#else + // iOS can always enable notificaions. + return true; +#endif +} + +static NSURL *SaveTempImage(SDL_Surface *image) +{ + @autoreleasepool { + const UInt32 hash = SDL_murmur3_32(image->pixels, image->pitch * image->h, 0); + NSString *tempFileName = [NSString stringWithFormat:@"SDL_tmpimage-%u.png", hash]; + NSString *tempFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:tempFileName]; + + if (!SDL_SavePNG(image, [tempFilePath fileSystemRepresentation])) { + return nil; + } + + return [NSURL fileURLWithPath:tempFilePath]; + } +} + +void Cocoa_RegisterNotificationDelegate() +{ + if (!ShouldEnableNotifications()) { + return; + } + + if (@available(macOS 10.14, *)) { + @autoreleasepool { + if (!center) { + center = [UNUserNotificationCenter currentNotificationCenter]; + } + if (!delegate) { + delegate = [SDLNotificationDelegate new]; + [center setDelegate:delegate]; + } + } + } +} + +bool SDL_RequestNotificationPermission(void) +{ + @autoreleasepool { + if (@available(macOS 10.14, *)) { + // Notifications not initialized (not in a bundle). + if (!center) { + return SDL_SetError("macOS notifications not supported outside an application bundle"); + } + + // Check authorization to send notifications, and request it if necessary. + [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *_Nonnull settings) { + if (settings.authorizationStatus == UNAuthorizationStatusNotDetermined) { + UNAuthorizationOptions options = UNAuthorizationOptionAlert + UNAuthorizationOptionSound; + [center requestAuthorizationWithOptions:options + completionHandler:^(BOOL granted, NSError *_Nullable error) {}]; + } + }]; + + return true; + } else { + SDL_SetError("Notifications require macOS 10.14 or higher"); + return false; + } + } + + return false; +} + +bool SDL_RemoveNotification(SDL_NotificationID notification) +{ + @autoreleasepool { + if (@available(macOS 10.14, *)) { + // Notifications not initialized (not in a bundle). + if (!center) { + return SDL_SetError("macOS notifications not supported outside an application bundle"); + } + + NSString *identifier = [NSString stringWithFormat:@"SDL_LocalNotification-%u", notification]; + [center removePendingNotificationRequestsWithIdentifiers:@[ identifier ]]; + [center removeDeliveredNotificationsWithIdentifiers:@[ identifier ]]; + } + } + + return true; +} + +SDL_NotificationID SDL_SYS_ShowNotification(SDL_PropertiesID props) +{ + @autoreleasepool { + if (@available(macOS 10.14, *)) { + if (!SDL_RequestNotificationPermission()) { + return false; + } + + // Get the notification properties. + const char *title = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_TITLE_STRING, NULL); + const char *message = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_MESSAGE_STRING, ""); + const char *sound = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_SOUND_STRING, "default"); + SDL_Surface *image = SDL_GetPointerProperty(props, SDL_PROP_NOTIFICATION_IMAGE_POINTER, NULL); + const SDL_NotificationID replaces = (SDL_NotificationID)SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_REPLACES_NUMBER, 0); + const SDL_NotificationPriority priority = (SDL_NotificationPriority)SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_PRIORITY_NUMBER, SDL_NOTIFICATION_PRIORITY_NORMAL); + const SDL_NotificationAction *sdlactions = SDL_GetPointerProperty(props, SDL_PROP_NOTIFICATION_ACTIONS_POINTER, NULL); + const int num_sdlactions = (int)SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_ACTION_COUNT_NUMBER, 0); + const bool transient = SDL_GetBooleanProperty(props, SDL_PROP_NOTIFICATION_TRANSIENT_BOOLEAN, false); + + // Generate a new ID. + Uint32 new_id; + if (replaces) { + new_id = replaces; + } else if (SecRandomCopyBytes(kSecRandomDefault, sizeof(new_id), &new_id) != errSecSuccess) { + new_id = (Uint32)SDL_GetTicksNS(); + } + + // Build the action array. + NSMutableArray *actions = nil; + if (sdlactions && num_sdlactions) { + actions = [NSMutableArray array]; + for (int i = 0; i < num_sdlactions; ++i) { + if (sdlactions[i].type == SDL_NOTIFICATION_ACTION_TYPE_BUTTON) { + UNNotificationAction *action = [UNNotificationAction actionWithIdentifier:[NSString stringWithUTF8String:sdlactions[i].button.action_id] + title:[NSString stringWithUTF8String:sdlactions[i].button.action_label] + options:UNNotificationActionOptionNone]; + actions[i] = action; + } + } + } + + // Create the category. + NSString *category_id = nil; + if (actions && [actions count]) { + // Create the notification category. + category_id = [[NSUUID new] UUIDString]; + UNNotificationCategory *category = [UNNotificationCategory categoryWithIdentifier:category_id + actions:actions + intentIdentifiers:@[] + options:UNNotificationCategoryOptionNone]; + NSSet *categories = [NSSet setWithObject:category]; + [center setNotificationCategories:categories]; + } + + // Configure the content. + UNMutableNotificationContent *content = [UNMutableNotificationContent new]; + content.title = [NSString stringWithUTF8String:title]; + content.body = [NSString stringWithUTF8String:message]; + content.categoryIdentifier = category_id; + + if (SDL_strcmp(sound, "default") == 0) { + if (priority == SDL_NOTIFICATION_PRIORITY_CRITICAL) { + // defaultCriticalSound is only in iOS 12+ + if (@available(iOS 12, *)) { + content.sound = [UNNotificationSound defaultCriticalSound]; + } else { + content.sound = [UNNotificationSound defaultSound]; + } + } else { + content.sound = [UNNotificationSound defaultSound]; + } + } else if (SDL_strcmp(sound, "silent") != 0) { + if (priority == SDL_NOTIFICATION_PRIORITY_CRITICAL) { + if (@available(iOS 12, *)) { + content.sound = [UNNotificationSound criticalSoundNamed:[NSString stringWithUTF8String:sound]]; + } else { + content.sound = [UNNotificationSound soundNamed:[NSString stringWithUTF8String:sound]]; + } + } else { + content.sound = [UNNotificationSound soundNamed:[NSString stringWithUTF8String:sound]]; + } + } + + if (@available(macOS 12, iOS 15, *)) { + switch (priority) { + case SDL_NOTIFICATION_PRIORITY_LOW: + content.interruptionLevel = UNNotificationInterruptionLevelPassive; + break; + case SDL_NOTIFICATION_PRIORITY_CRITICAL: + content.interruptionLevel = UNNotificationInterruptionLevelCritical; + break; + case SDL_NOTIFICATION_PRIORITY_NORMAL: + case SDL_NOTIFICATION_PRIORITY_HIGH: + default: + content.interruptionLevel = UNNotificationInterruptionLevelActive; + break; + } + } + + // Notifications load images from file paths, so save it to a temporary location. + if (image) { + NSURL *url = SaveTempImage(image); + if (url) { + UNNotificationAttachment *attach = [UNNotificationAttachment attachmentWithIdentifier:@"" URL:url options:nil error:nil]; + content.attachments = @[ attach ]; + } + } + + NSString *identifier = [NSString stringWithFormat:@"SDL_LocalNotification-%u", new_id]; + UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier + content:content + trigger:nil]; + + [center addNotificationRequest:request + withCompletionHandler:^(NSError *_Nullable error) { + if (error != nil) { + SDL_SetError("Failed to show notification"); + } + }]; + + /* There is no way to tell if the notification was ignored or wound up + * in the notification center, so just remove transient notifications + * after a brief period via a timer. + */ + if (transient) { + [NSTimer scheduledTimerWithTimeInterval:7 repeats:NO block:^(NSTimer *timer) { + SDL_RemoveNotification(new_id); + }]; + } + + return new_id; + } else { + SDL_SetError("macOS 10.14+ required for notifications"); + } + + return 0; + } +} +#else + +// Notifications on tvOS are just for updating badges, and are of no use here. +SDL_NotificationID SDL_SYS_ShowNotification(SDL_PropertiesID props) +{ + SDL_SetError("Notifications not supported on tvOS"); + return 0; +} + +void Cocoa_RegisterNotificationDelegate() +{ +} + +bool SDL_RequestNotificationPermission(void) +{ + return SDL_SetError("Notifications not supported on tvOS"); +} + +bool SDL_RemoveNotification(SDL_NotificationID notification) +{ + return SDL_SetError("Notifications not supported on tvOS"); +} +#endif + +void SDL_CleanupNotifications() +{ + // TODO: Anything to do here? +} diff --git a/src/notification/dummy/SDL_dummynotification.c b/src/notification/dummy/SDL_dummynotification.c index 973207914b..fc2cb8ac06 100644 --- a/src/notification/dummy/SDL_dummynotification.c +++ b/src/notification/dummy/SDL_dummynotification.c @@ -43,6 +43,13 @@ void SDL_CleanupNotifications() // Nothing to do. } +#ifdef SDL_PLATFORM_APPLE +void Cocoa_RegisterNotificationDelegate() +{ + // Nothing to do. +} +#endif + #ifdef SDL_VIDEO_DRIVER_WAYLAND const char *SDL_GetNotificationActivationToken() {