mirror of
https://github.com/libsdl-org/SDL.git
synced 2026-06-14 15:43:56 +00:00
Add the Cocoa notification driver
Supported on macOS 10.14+ and iOS.
This commit is contained in:
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
|
||||
360
src/notification/cocoa/SDL_cocoanotification.m
Normal file
360
src/notification/cocoa/SDL_cocoanotification.m
Normal file
@@ -0,0 +1,360 @@
|
||||
/*
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
|
||||
|
||||
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 <Foundation/Foundation.h>
|
||||
#import <UserNotifications/UNNotification.h>
|
||||
#import <UserNotifications/UNNotificationAction.h>
|
||||
#import <UserNotifications/UNNotificationAttachment.h>
|
||||
#import <UserNotifications/UNNotificationCategory.h>
|
||||
#import <UserNotifications/UNNotificationContent.h>
|
||||
#import <UserNotifications/UNNotificationRequest.h>
|
||||
#import <UserNotifications/UNNotificationResponse.h>
|
||||
#import <UserNotifications/UNNotificationSettings.h>
|
||||
#import <UserNotifications/UNNotificationSound.h>
|
||||
#import <UserNotifications/UNUserNotificationCenter.h>
|
||||
|
||||
@interface SDLNotificationDelegate : NSObject <UNUserNotificationCenterDelegate>
|
||||
@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?
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user