1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-13 00:04:19 +00:00
JUCE/modules/juce_gui_extra/native/juce_mac_PushNotifications.cpp
reuk 85226c33d4
Mac MainMenu: Allow commands without modifiers to be passed to peer
Ideally, we want to pass shortcut keys to the component to handle, and
only fall back to invoking a menu item if the component was unable to
handle the keyboard event, or if the action was triggered by
clicking/selecting an item in the menu itself. The old implementation
tried to work out whether the action was triggered by a shortcut by
checking the event's characters and modifiers. This method was
inaccurate, because some shortcuts (such as arrow keys) may add
unexpected numpad/function modifier flags.

We now try handling shortcut keys directly in the peer, and pass events
up to the superclass (which will forward them to the main menu) if the
event could not be handled.

This commit also adjusts some Objective-C method signatures to use the
correct string encoding for the BOOL type.
2021-07-14 14:49:05 +01:00

568 lines
24 KiB
C++

/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
End User License Agreement: www.juce.com/juce-6-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
namespace juce
{
namespace PushNotificationsDelegateDetailsOsx
{
using Action = PushNotifications::Notification::Action;
//==============================================================================
NSUserNotification* juceNotificationToNSUserNotification (const PushNotifications::Notification& n,
bool isEarlierThanMavericks,
bool isEarlierThanYosemite)
{
auto notification = [[NSUserNotification alloc] init];
notification.title = juceStringToNS (n.title);
notification.subtitle = juceStringToNS (n.subtitle);
notification.informativeText = juceStringToNS (n.body);
notification.userInfo = varObjectToNSDictionary (n.properties);
auto triggerTime = Time::getCurrentTime() + RelativeTime (n.triggerIntervalSec);
notification.deliveryDate = [NSDate dateWithTimeIntervalSince1970: triggerTime.toMilliseconds() / 1000.];
if (n.repeat && n.triggerIntervalSec >= 60)
{
auto dateComponents = [[NSDateComponents alloc] init];
auto intervalSec = NSInteger (n.triggerIntervalSec);
dateComponents.second = intervalSec;
dateComponents.nanosecond = NSInteger ((n.triggerIntervalSec - intervalSec) * 1000000000);
notification.deliveryRepeatInterval = dateComponents;
[dateComponents autorelease];
}
auto soundToPlayString = n.soundToPlay.toString (true);
if (soundToPlayString == "default_os_sound")
{
notification.soundName = NSUserNotificationDefaultSoundName;
}
else if (soundToPlayString.isNotEmpty())
{
auto* soundName = juceStringToNS (soundToPlayString.fromLastOccurrenceOf ("/", false, false)
.upToLastOccurrenceOf (".", false, false));
notification.soundName = soundName;
}
notification.hasActionButton = n.actions.size() > 0;
if (n.actions.size() > 0)
notification.actionButtonTitle = juceStringToNS (n.actions.getReference (0).title);
if (! isEarlierThanMavericks)
{
notification.identifier = juceStringToNS (n.identifier);
if (n.actions.size() > 0)
{
notification.hasReplyButton = n.actions.getReference (0).style == Action::text;
notification.responsePlaceholder = juceStringToNS (n.actions.getReference (0).textInputPlaceholder);
}
auto* imageDirectory = n.icon.contains ("/")
? juceStringToNS (n.icon.upToLastOccurrenceOf ("/", false, true))
: [NSString string];
auto* imageName = juceStringToNS (n.icon.fromLastOccurrenceOf ("/", false, false)
.upToLastOccurrenceOf (".", false, false));
auto* imageExtension = juceStringToNS (n.icon.fromLastOccurrenceOf (".", false, false));
NSString* imagePath = nil;
if ([imageDirectory length] == NSUInteger (0))
{
imagePath = [[NSBundle mainBundle] pathForResource: imageName
ofType: imageExtension];
}
else
{
imagePath = [[NSBundle mainBundle] pathForResource: imageName
ofType: imageExtension
inDirectory: imageDirectory];
}
notification.contentImage = [[NSImage alloc] initWithContentsOfFile: imagePath];
if (! isEarlierThanYosemite)
{
if (n.actions.size() > 1)
{
auto additionalActions = [NSMutableArray arrayWithCapacity: (NSUInteger) n.actions.size() - 1];
for (int a = 1; a < n.actions.size(); ++a)
[additionalActions addObject: [NSUserNotificationAction actionWithIdentifier: juceStringToNS (n.actions[a].identifier)
title: juceStringToNS (n.actions[a].title)]];
notification.additionalActions = additionalActions;
}
}
}
[notification autorelease];
return notification;
}
//==============================================================================
PushNotifications::Notification nsUserNotificationToJuceNotification (NSUserNotification* n,
bool isEarlierThanMavericks,
bool isEarlierThanYosemite)
{
PushNotifications::Notification notif;
notif.title = nsStringToJuce (n.title);
notif.subtitle = nsStringToJuce (n.subtitle);
notif.body = nsStringToJuce (n.informativeText);
notif.repeat = n.deliveryRepeatInterval != nil;
if (n.deliveryRepeatInterval != nil)
{
notif.triggerIntervalSec = n.deliveryRepeatInterval.second + (n.deliveryRepeatInterval.nanosecond / 1000000000.);
}
else
{
NSDate* dateNow = [NSDate date];
NSDate* deliveryDate = n.deliveryDate;
notif.triggerIntervalSec = [dateNow timeIntervalSinceDate: deliveryDate];
}
notif.soundToPlay = URL (nsStringToJuce (n.soundName));
notif.properties = nsDictionaryToVar (n.userInfo);
if (! isEarlierThanMavericks)
{
notif.identifier = nsStringToJuce (n.identifier);
if (n.contentImage != nil)
notif.icon = nsStringToJuce ([n.contentImage name]);
}
Array<Action> actions;
if (n.actionButtonTitle != nil)
{
Action action;
action.title = nsStringToJuce (n.actionButtonTitle);
if (! isEarlierThanMavericks)
{
if (n.hasReplyButton)
action.style = Action::text;
if (n.responsePlaceholder != nil)
action.textInputPlaceholder = nsStringToJuce (n.responsePlaceholder);
}
actions.add (action);
}
if (! isEarlierThanYosemite)
{
if (n.additionalActions != nil)
{
for (NSUserNotificationAction* a in n.additionalActions)
{
Action action;
action.identifier = nsStringToJuce (a.identifier);
action.title = nsStringToJuce (a.title);
actions.add (action);
}
}
}
return notif;
}
//==============================================================================
var getNotificationPropertiesFromDictionaryVar (const var& dictionaryVar)
{
auto* dictionaryVarObject = dictionaryVar.getDynamicObject();
if (dictionaryVarObject == nullptr)
return {};
const auto& properties = dictionaryVarObject->getProperties();
DynamicObject::Ptr propsVarObject = new DynamicObject();
for (int i = 0; i < properties.size(); ++i)
{
auto propertyName = properties.getName (i).toString();
if (propertyName == "aps")
continue;
propsVarObject->setProperty (propertyName, properties.getValueAt (i));
}
return var (propsVarObject.get());
}
PushNotifications::Notification nsDictionaryToJuceNotification (NSDictionary* dictionary)
{
const var dictionaryVar = nsDictionaryToVar (dictionary);
const var apsVar = dictionaryVar.getProperty ("aps", {});
if (! apsVar.isObject())
return {};
var alertVar = apsVar.getProperty ("alert", {});
const var titleVar = alertVar.getProperty ("title", {});
const var bodyVar = alertVar.isObject() ? alertVar.getProperty ("body", {}) : alertVar;
const var categoryVar = apsVar.getProperty ("category", {});
const var soundVar = apsVar.getProperty ("sound", {});
const var badgeVar = apsVar.getProperty ("badge", {});
const var threadIdVar = apsVar.getProperty ("thread-id", {});
PushNotifications::Notification notification;
notification.title = titleVar .toString();
notification.body = bodyVar .toString();
notification.groupId = threadIdVar.toString();
notification.category = categoryVar.toString();
notification.soundToPlay = URL (soundVar.toString());
notification.badgeNumber = (int) badgeVar;
notification.properties = getNotificationPropertiesFromDictionaryVar (dictionaryVar);
return notification;
}
}
//==============================================================================
struct PushNotificationsDelegate
{
PushNotificationsDelegate() : delegate ([getClass().createInstance() init])
{
Class::setThis (delegate.get(), this);
id<NSApplicationDelegate> appDelegate = [[NSApplication sharedApplication] delegate];
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
if ([appDelegate respondsToSelector: @selector (setPushNotificationsDelegate:)])
[appDelegate performSelector: @selector (setPushNotificationsDelegate:) withObject: delegate.get()];
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
[NSUserNotificationCenter defaultUserNotificationCenter].delegate = delegate.get();
}
virtual ~PushNotificationsDelegate()
{
[NSUserNotificationCenter defaultUserNotificationCenter].delegate = nil;
}
virtual void registeredForRemoteNotifications (NSData* deviceToken) = 0;
virtual void failedToRegisterForRemoteNotifications (NSError* error) = 0;
virtual void didReceiveRemoteNotification (NSDictionary* userInfo) = 0;
virtual void didDeliverNotification (NSUserNotification* notification) = 0;
virtual void didActivateNotification (NSUserNotification* notification) = 0;
virtual bool shouldPresentNotification (NSUserNotification* notification) = 0;
protected:
NSUniquePtr<NSObject<NSApplicationDelegate, NSUserNotificationCenterDelegate>> delegate;
private:
struct Class : public ObjCClass<NSObject<NSApplicationDelegate, NSUserNotificationCenterDelegate>>
{
Class() : ObjCClass<NSObject<NSApplicationDelegate, NSUserNotificationCenterDelegate>> ("JucePushNotificationsDelegate_")
{
addIvar<PushNotificationsDelegate*> ("self");
addMethod (@selector (application:didRegisterForRemoteNotificationsWithDeviceToken:), registeredForRemoteNotifications, "v@:@@");
addMethod (@selector (application:didFailToRegisterForRemoteNotificationsWithError:), failedToRegisterForRemoteNotifications, "v@:@@");
addMethod (@selector (application:didReceiveRemoteNotification:), didReceiveRemoteNotification, "v@:@@");
addMethod (@selector (userNotificationCenter:didDeliverNotification:), didDeliverNotification, "v@:@@");
addMethod (@selector (userNotificationCenter:didActivateNotification:), didActivateNotification, "v@:@@");
addMethod (@selector (userNotificationCenter:shouldPresentNotification:), shouldPresentNotification, "c@:@@");
registerClass();
}
//==============================================================================
static PushNotificationsDelegate& getThis (id self) { return *getIvar<PushNotificationsDelegate*> (self, "self"); }
static void setThis (id self, PushNotificationsDelegate* d) { object_setInstanceVariable (self, "self", d); }
//==============================================================================
static void registeredForRemoteNotifications (id self, SEL, NSApplication*,
NSData* deviceToken) { getThis (self).registeredForRemoteNotifications (deviceToken); }
static void failedToRegisterForRemoteNotifications (id self, SEL, NSApplication*,
NSError* error) { getThis (self).failedToRegisterForRemoteNotifications (error); }
static void didReceiveRemoteNotification (id self, SEL, NSApplication*,
NSDictionary* userInfo) { getThis (self).didReceiveRemoteNotification (userInfo); }
static void didDeliverNotification (id self, SEL, NSUserNotificationCenter*,
NSUserNotification* notification) { getThis (self).didDeliverNotification (notification); }
static void didActivateNotification (id self, SEL, NSUserNotificationCenter*,
NSUserNotification* notification) { getThis (self).didActivateNotification (notification); }
static bool shouldPresentNotification (id self, SEL, NSUserNotificationCenter*,
NSUserNotification* notification) { return getThis (self).shouldPresentNotification (notification); }
};
//==============================================================================
static Class& getClass()
{
static Class c;
return c;
}
};
//==============================================================================
bool PushNotifications::Notification::isValid() const noexcept { return true; }
//==============================================================================
struct PushNotifications::Pimpl : private PushNotificationsDelegate
{
Pimpl (PushNotifications& p)
: owner (p)
{
}
void requestPermissionsWithSettings (const PushNotifications::Settings& settingsToUse)
{
if (isEarlierThanLion)
return;
settings = settingsToUse;
NSRemoteNotificationType types = NSUInteger ((bool) settings.allowBadge);
if (isAtLeastMountainLion)
types |= (NSUInteger) ((bool) settings.allowSound << 1 | (bool) settings.allowAlert << 2);
[[NSApplication sharedApplication] registerForRemoteNotificationTypes: types];
}
void requestSettingsUsed()
{
if (isEarlierThanLion)
{
// no settings available
owner.listeners.call ([] (Listener& l) { l.notificationSettingsReceived ({}); });
return;
}
settings.allowBadge = [NSApplication sharedApplication].enabledRemoteNotificationTypes & NSRemoteNotificationTypeBadge;
if (isAtLeastMountainLion)
{
settings.allowSound = [NSApplication sharedApplication].enabledRemoteNotificationTypes & NSRemoteNotificationTypeSound;
settings.allowAlert = [NSApplication sharedApplication].enabledRemoteNotificationTypes & NSRemoteNotificationTypeAlert;
}
owner.listeners.call ([&] (Listener& l) { l.notificationSettingsReceived (settings); });
}
bool areNotificationsEnabled() const { return true; }
void sendLocalNotification (const Notification& n)
{
auto* notification = PushNotificationsDelegateDetailsOsx::juceNotificationToNSUserNotification (n, isEarlierThanMavericks, isEarlierThanYosemite);
[[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification: notification];
}
void getDeliveredNotifications() const
{
Array<PushNotifications::Notification> notifs;
for (NSUserNotification* n in [NSUserNotificationCenter defaultUserNotificationCenter].deliveredNotifications)
notifs.add (PushNotificationsDelegateDetailsOsx::nsUserNotificationToJuceNotification (n, isEarlierThanMavericks, isEarlierThanYosemite));
owner.listeners.call ([&] (Listener& l) { l.deliveredNotificationsListReceived (notifs); });
}
void removeAllDeliveredNotifications()
{
[[NSUserNotificationCenter defaultUserNotificationCenter] removeAllDeliveredNotifications];
}
void removeDeliveredNotification (const String& identifier)
{
PushNotifications::Notification n;
n.identifier = identifier;
auto nsNotification = PushNotificationsDelegateDetailsOsx::juceNotificationToNSUserNotification (n, isEarlierThanMavericks, isEarlierThanYosemite);
[[NSUserNotificationCenter defaultUserNotificationCenter] removeDeliveredNotification: nsNotification];
}
void setupChannels (const Array<ChannelGroup>& groups, const Array<Channel>& channels)
{
ignoreUnused (groups, channels);
}
void getPendingLocalNotifications() const
{
Array<PushNotifications::Notification> notifs;
for (NSUserNotification* n in [NSUserNotificationCenter defaultUserNotificationCenter].scheduledNotifications)
notifs.add (PushNotificationsDelegateDetailsOsx::nsUserNotificationToJuceNotification (n, isEarlierThanMavericks, isEarlierThanYosemite));
owner.listeners.call ([&] (Listener& l) { l.pendingLocalNotificationsListReceived (notifs); });
}
void removePendingLocalNotification (const String& identifier)
{
PushNotifications::Notification n;
n.identifier = identifier;
auto nsNotification = PushNotificationsDelegateDetailsOsx::juceNotificationToNSUserNotification (n, isEarlierThanMavericks, isEarlierThanYosemite);
[[NSUserNotificationCenter defaultUserNotificationCenter] removeScheduledNotification: nsNotification];
}
void removeAllPendingLocalNotifications()
{
for (NSUserNotification* n in [NSUserNotificationCenter defaultUserNotificationCenter].scheduledNotifications)
[[NSUserNotificationCenter defaultUserNotificationCenter] removeScheduledNotification: n];
}
String getDeviceToken()
{
// You need to call requestPermissionsWithSettings() first.
jassert (initialised);
return deviceToken;
}
//==============================================================================
//PushNotificationsDelegate
void registeredForRemoteNotifications (NSData* deviceTokenToUse) override
{
deviceToken = [deviceTokenToUse]() -> String
{
auto length = deviceTokenToUse.length;
if (auto* buffer = (const unsigned char*) deviceTokenToUse.bytes)
{
NSMutableString* hexString = [NSMutableString stringWithCapacity: (length * 2)];
for (NSUInteger i = 0; i < length; ++i)
[hexString appendFormat:@"%02x", buffer[i]];
return nsStringToJuce ([hexString copy]);
}
return {};
}();
initialised = true;
owner.listeners.call ([&] (Listener& l) { l.deviceTokenRefreshed (deviceToken); });
}
void failedToRegisterForRemoteNotifications (NSError* error) override
{
ignoreUnused (error);
deviceToken.clear();
}
void didReceiveRemoteNotification (NSDictionary* userInfo) override
{
auto n = PushNotificationsDelegateDetailsOsx::nsDictionaryToJuceNotification (userInfo);
owner.listeners.call ([&] (Listener& l) { l.handleNotification (true, n); });
}
void didDeliverNotification (NSUserNotification* notification) override
{
ignoreUnused (notification);
}
void didActivateNotification (NSUserNotification* notification) override
{
auto n = PushNotificationsDelegateDetailsOsx::nsUserNotificationToJuceNotification (notification, isEarlierThanMavericks, isEarlierThanYosemite);
if (notification.activationType == NSUserNotificationActivationTypeContentsClicked)
{
owner.listeners.call ([&] (Listener& l) { l.handleNotification (notification.remote, n); });
}
else
{
auto actionIdentifier = (! isEarlierThanYosemite && notification.additionalActivationAction != nil)
? nsStringToJuce (notification.additionalActivationAction.identifier)
: nsStringToJuce (notification.actionButtonTitle);
auto reply = notification.activationType == NSUserNotificationActivationTypeReplied
? nsStringToJuce ([notification.response string])
: String();
owner.listeners.call ([&] (Listener& l) { l.handleNotificationAction (notification.remote, n, actionIdentifier, reply); });
}
}
bool shouldPresentNotification (NSUserNotification*) override { return true; }
void subscribeToTopic (const String& topic) { ignoreUnused (topic); }
void unsubscribeFromTopic (const String& topic) { ignoreUnused (topic); }
void sendUpstreamMessage (const String& serverSenderId,
const String& collapseKey,
const String& messageId,
const String& messageType,
int timeToLive,
const StringPairArray& additionalData)
{
ignoreUnused (serverSenderId, collapseKey, messageId, messageType);
ignoreUnused (timeToLive, additionalData);
}
private:
PushNotifications& owner;
const bool isEarlierThanLion = std::floor (NSFoundationVersionNumber) < std::floor (NSFoundationVersionNumber10_7);
const bool isAtLeastMountainLion = std::floor (NSFoundationVersionNumber) >= NSFoundationVersionNumber10_7;
const bool isEarlierThanMavericks = std::floor (NSFoundationVersionNumber) < NSFoundationVersionNumber10_9;
const bool isEarlierThanYosemite = std::floor (NSFoundationVersionNumber) <= NSFoundationVersionNumber10_9;
bool initialised = false;
String deviceToken;
PushNotifications::Settings settings;
};
} // namespace juce
JUCE_END_IGNORE_WARNINGS_GCC_LIKE