1/*
2  ==============================================================================
3
4   This file is part of the JUCE library.
5   Copyright (c) 2020 - Raw Material Software Limited
6
7   JUCE is an open source library subject to commercial or open-source
8   licensing.
9
10   The code included in this file is provided under the terms of the ISC license
11   http://www.isc.org/downloads/software-support-policy/isc-license. Permission
12   To use, copy, modify, and/or distribute this software for any purpose with or
13   without fee is hereby granted provided that the above copyright notice and
14   this permission notice appear in all copies.
15
16   JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
17   EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
18   DISCLAIMED.
19
20  ==============================================================================
21*/
22
23namespace juce
24{
25
26using AppFocusChangeCallback = void (*)();
27AppFocusChangeCallback appFocusChangeCallback = nullptr;
28
29using CheckEventBlockedByModalComps = bool (*)(NSEvent*);
30CheckEventBlockedByModalComps isEventBlockedByModalComps = nullptr;
31
32using MenuTrackingChangedCallback = void (*)(bool);
33MenuTrackingChangedCallback menuTrackingChangedCallback = nullptr;
34
35//==============================================================================
36struct AppDelegate
37{
38public:
39    AppDelegate()
40    {
41        static AppDelegateClass cls;
42        delegate = [cls.createInstance() init];
43
44        NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
45
46        [center addObserver: delegate selector: @selector (mainMenuTrackingBegan:)
47                       name: NSMenuDidBeginTrackingNotification object: nil];
48        [center addObserver: delegate selector: @selector (mainMenuTrackingEnded:)
49                       name: NSMenuDidEndTrackingNotification object: nil];
50
51        if (JUCEApplicationBase::isStandaloneApp())
52        {
53            [NSApp setDelegate: delegate];
54
55            [[NSDistributedNotificationCenter defaultCenter] addObserver: delegate
56                                                                selector: @selector (broadcastMessageCallback:)
57                                                                    name: getBroadcastEventName()
58                                                                  object: nil
59                                                      suspensionBehavior: NSNotificationSuspensionBehaviorDeliverImmediately];
60        }
61        else
62        {
63            [center addObserver: delegate selector: @selector (applicationDidResignActive:)
64                           name: NSApplicationDidResignActiveNotification object: NSApp];
65
66            [center addObserver: delegate selector: @selector (applicationDidBecomeActive:)
67                           name: NSApplicationDidBecomeActiveNotification object: NSApp];
68
69            [center addObserver: delegate selector: @selector (applicationWillUnhide:)
70                           name: NSApplicationWillUnhideNotification object: NSApp];
71        }
72    }
73
74    ~AppDelegate()
75    {
76        [[NSRunLoop currentRunLoop] cancelPerformSelectorsWithTarget: delegate];
77        [[NSNotificationCenter defaultCenter] removeObserver: delegate];
78
79        if (JUCEApplicationBase::isStandaloneApp())
80        {
81            [NSApp setDelegate: nil];
82
83            [[NSDistributedNotificationCenter defaultCenter] removeObserver: delegate
84                                                                       name: getBroadcastEventName()
85                                                                     object: nil];
86        }
87
88        [delegate release];
89    }
90
91    static NSString* getBroadcastEventName()
92    {
93        return juceStringToNS ("juce_" + String::toHexString (File::getSpecialLocation (File::currentExecutableFile).hashCode64()));
94    }
95
96    MessageQueue messageQueue;
97    id delegate;
98
99private:
100    //==============================================================================
101    struct AppDelegateClass   : public ObjCClass<NSObject>
102    {
103        AppDelegateClass()  : ObjCClass<NSObject> ("JUCEAppDelegate_")
104        {
105            addMethod (@selector (applicationWillFinishLaunching:), applicationWillFinishLaunching, "v@:@");
106            addMethod (@selector (getUrl:withReplyEvent:),          getUrl_withReplyEvent,          "v@:@@");
107            addMethod (@selector (applicationShouldTerminate:),     applicationShouldTerminate,     "I@:@");
108            addMethod (@selector (applicationWillTerminate:),       applicationWillTerminate,       "v@:@");
109            addMethod (@selector (application:openFile:),           application_openFile,           "c@:@@");
110            addMethod (@selector (application:openFiles:),          application_openFiles,          "v@:@@");
111            addMethod (@selector (applicationDidBecomeActive:),     applicationDidBecomeActive,     "v@:@");
112            addMethod (@selector (applicationDidResignActive:),     applicationDidResignActive,     "v@:@");
113            addMethod (@selector (applicationWillUnhide:),          applicationWillUnhide,          "v@:@");
114            addMethod (@selector (broadcastMessageCallback:),       broadcastMessageCallback,       "v@:@");
115            addMethod (@selector (mainMenuTrackingBegan:),          mainMenuTrackingBegan,          "v@:@");
116            addMethod (@selector (mainMenuTrackingEnded:),          mainMenuTrackingEnded,          "v@:@");
117            addMethod (@selector (dummyMethod),                     dummyMethod,                    "v@:");
118
119           #if JUCE_PUSH_NOTIFICATIONS
120            //==============================================================================
121            addIvar<NSObject<NSApplicationDelegate, NSUserNotificationCenterDelegate>*> ("pushNotificationsDelegate");
122
123            addMethod (@selector (applicationDidFinishLaunching:),                                applicationDidFinishLaunching,          "v@:@");
124            addMethod (@selector (setPushNotificationsDelegate:),                                 setPushNotificationsDelegate,           "v@:@");
125            addMethod (@selector (application:didRegisterForRemoteNotificationsWithDeviceToken:), registeredForRemoteNotifications,       "v@:@@");
126            addMethod (@selector (application:didFailToRegisterForRemoteNotificationsWithError:), failedToRegisterForRemoteNotifications, "v@:@@");
127            addMethod (@selector (application:didReceiveRemoteNotification:),                     didReceiveRemoteNotification,           "v@:@@");
128           #endif
129
130            registerClass();
131        }
132
133    private:
134        static void applicationWillFinishLaunching (id self, SEL, NSNotification*)
135        {
136            [[NSAppleEventManager sharedAppleEventManager] setEventHandler: self
137                                                               andSelector: @selector (getUrl:withReplyEvent:)
138                                                             forEventClass: kInternetEventClass
139                                                                andEventID: kAEGetURL];
140        }
141
142       #if JUCE_PUSH_NOTIFICATIONS
143        static void applicationDidFinishLaunching (id self, SEL, NSNotification* notification)
144        {
145            if (notification.userInfo != nil)
146            {
147                JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
148                // NSUserNotification is deprecated from macOS 11, but there doesn't seem to be a
149                // replacement for NSApplicationLaunchUserNotificationKey returning a non-deprecated type
150                NSUserNotification* userNotification = notification.userInfo[NSApplicationLaunchUserNotificationKey];
151                JUCE_END_IGNORE_WARNINGS_GCC_LIKE
152
153                if (userNotification != nil && userNotification.userInfo != nil)
154                    didReceiveRemoteNotification (self, nil, [NSApplication sharedApplication], userNotification.userInfo);
155            }
156        }
157       #endif
158
159        static NSApplicationTerminateReply applicationShouldTerminate (id /*self*/, SEL, NSApplication*)
160        {
161            if (auto* app = JUCEApplicationBase::getInstance())
162            {
163                app->systemRequestedQuit();
164
165                if (! MessageManager::getInstance()->hasStopMessageBeenSent())
166                    return NSTerminateCancel;
167            }
168
169            return NSTerminateNow;
170        }
171
172        static void applicationWillTerminate (id /*self*/, SEL, NSNotification*)
173        {
174            JUCEApplicationBase::appWillTerminateByForce();
175        }
176
177        static BOOL application_openFile (id /*self*/, SEL, NSApplication*, NSString* filename)
178        {
179            if (auto* app = JUCEApplicationBase::getInstance())
180            {
181                app->anotherInstanceStarted (quotedIfContainsSpaces (filename));
182                return YES;
183            }
184
185            return NO;
186        }
187
188        static void application_openFiles (id /*self*/, SEL, NSApplication*, NSArray* filenames)
189        {
190            if (auto* app = JUCEApplicationBase::getInstance())
191            {
192                StringArray files;
193
194                for (NSString* f in filenames)
195                    files.add (quotedIfContainsSpaces (f));
196
197                if (files.size() > 0)
198                    app->anotherInstanceStarted (files.joinIntoString (" "));
199            }
200        }
201
202        static void applicationDidBecomeActive (id /*self*/, SEL, NSNotification*)  { focusChanged(); }
203        static void applicationDidResignActive (id /*self*/, SEL, NSNotification*)  { focusChanged(); }
204        static void applicationWillUnhide      (id /*self*/, SEL, NSNotification*)  { focusChanged(); }
205
206        static void broadcastMessageCallback (id /*self*/, SEL, NSNotification* n)
207        {
208            NSDictionary* dict = (NSDictionary*) [n userInfo];
209            auto messageString = nsStringToJuce ((NSString*) [dict valueForKey: nsStringLiteral ("message")]);
210            MessageManager::getInstance()->deliverBroadcastMessage (messageString);
211        }
212
213        static void mainMenuTrackingBegan (id /*self*/, SEL, NSNotification*)
214        {
215            if (menuTrackingChangedCallback != nullptr)
216                (*menuTrackingChangedCallback) (true);
217        }
218
219        static void mainMenuTrackingEnded (id /*self*/, SEL, NSNotification*)
220        {
221            if (menuTrackingChangedCallback != nullptr)
222                (*menuTrackingChangedCallback) (false);
223        }
224
225        static void dummyMethod (id /*self*/, SEL) {}   // (used as a way of running a dummy thread)
226
227        static void focusChanged()
228        {
229            if (appFocusChangeCallback != nullptr)
230                (*appFocusChangeCallback)();
231        }
232
233        static void getUrl_withReplyEvent (id /*self*/, SEL, NSAppleEventDescriptor* event, NSAppleEventDescriptor*)
234        {
235            if (auto* app = JUCEApplicationBase::getInstance())
236                app->anotherInstanceStarted (quotedIfContainsSpaces ([[event paramDescriptorForKeyword: keyDirectObject] stringValue]));
237        }
238
239        static String quotedIfContainsSpaces (NSString* file)
240        {
241            String s (nsStringToJuce (file));
242            s = s.unquoted().replace ("\"", "\\\"");
243
244            if (s.containsChar (' '))
245                s = s.quoted();
246
247            return s;
248        }
249
250       #if JUCE_PUSH_NOTIFICATIONS
251        //==============================================================================
252        static void setPushNotificationsDelegate (id self, SEL, NSObject<NSApplicationDelegate, NSUserNotificationCenterDelegate>* delegate)
253        {
254            object_setInstanceVariable (self, "pushNotificationsDelegate", delegate);
255        }
256
257        static NSObject<NSApplicationDelegate, NSUserNotificationCenterDelegate>* getPushNotificationsDelegate (id self)
258        {
259            return getIvar<NSObject<NSApplicationDelegate, NSUserNotificationCenterDelegate>*> (self, "pushNotificationsDelegate");
260        }
261
262        static void registeredForRemoteNotifications (id self, SEL, NSApplication* application, NSData* deviceToken)
263        {
264            auto* delegate = getPushNotificationsDelegate (self);
265
266            SEL selector = NSSelectorFromString (@"application:didRegisterForRemoteNotificationsWithDeviceToken:");
267
268            if (delegate != nil && [delegate respondsToSelector: selector])
269            {
270                NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [delegate methodSignatureForSelector: selector]];
271                [invocation setSelector: selector];
272                [invocation setTarget: delegate];
273                [invocation setArgument: &application atIndex:2];
274                [invocation setArgument: &deviceToken atIndex:3];
275
276                [invocation invoke];
277            }
278        }
279
280        static void failedToRegisterForRemoteNotifications (id self, SEL, NSApplication* application, NSError* error)
281        {
282            auto* delegate = getPushNotificationsDelegate (self);
283
284            SEL selector = NSSelectorFromString (@"application:didFailToRegisterForRemoteNotificationsWithError:");
285
286            if (delegate != nil && [delegate respondsToSelector: selector])
287            {
288                NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [delegate methodSignatureForSelector: selector]];
289                [invocation setSelector: selector];
290                [invocation setTarget: delegate];
291                [invocation setArgument: &application atIndex:2];
292                [invocation setArgument: &error       atIndex:3];
293
294                [invocation invoke];
295            }
296        }
297
298        static void didReceiveRemoteNotification (id self, SEL, NSApplication* application, NSDictionary* userInfo)
299        {
300            auto* delegate = getPushNotificationsDelegate (self);
301
302            SEL selector = NSSelectorFromString (@"application:didReceiveRemoteNotification:");
303
304            if (delegate != nil && [delegate respondsToSelector: selector])
305            {
306                NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [delegate methodSignatureForSelector: selector]];
307                [invocation setSelector: selector];
308                [invocation setTarget: delegate];
309                [invocation setArgument: &application atIndex:2];
310                [invocation setArgument: &userInfo    atIndex:3];
311
312                [invocation invoke];
313            }
314        }
315       #endif
316    };
317};
318
319//==============================================================================
320void MessageManager::runDispatchLoop()
321{
322    if (quitMessagePosted.get() == 0) // check that the quit message wasn't already posted..
323    {
324        JUCE_AUTORELEASEPOOL
325        {
326            // must only be called by the message thread!
327            jassert (isThisTheMessageThread());
328
329          #if JUCE_PROJUCER_LIVE_BUILD
330            runDispatchLoopUntil (std::numeric_limits<int>::max());
331          #else
332           #if JUCE_CATCH_UNHANDLED_EXCEPTIONS
333            @try
334            {
335                [NSApp run];
336            }
337            @catch (NSException* e)
338            {
339                // An AppKit exception will kill the app, but at least this provides a chance to log it.,
340                std::runtime_error ex (std::string ("NSException: ") + [[e name] UTF8String] + ", Reason:" + [[e reason] UTF8String]);
341                JUCEApplicationBase::sendUnhandledException (&ex, __FILE__, __LINE__);
342            }
343            @finally
344            {
345            }
346           #else
347            [NSApp run];
348           #endif
349          #endif
350        }
351    }
352}
353
354static void shutdownNSApp()
355{
356    [NSApp stop: nil];
357    [NSEvent startPeriodicEventsAfterDelay: 0  withPeriod: 0.1];
358}
359
360void MessageManager::stopDispatchLoop()
361{
362   #if JUCE_PROJUCER_LIVE_BUILD
363    quitMessagePosted = true;
364   #else
365
366    if (isThisTheMessageThread())
367    {
368        quitMessagePosted = true;
369        shutdownNSApp();
370    }
371    else
372    {
373        struct QuitCallback  : public CallbackMessage
374        {
375            QuitCallback() {}
376            void messageCallback() override    { MessageManager::getInstance()->stopDispatchLoop(); }
377        };
378
379        (new QuitCallback())->post();
380    }
381   #endif
382}
383
384#if JUCE_MODAL_LOOPS_PERMITTED
385bool MessageManager::runDispatchLoopUntil (int millisecondsToRunFor)
386{
387    jassert (millisecondsToRunFor >= 0);
388    jassert (isThisTheMessageThread()); // must only be called by the message thread
389
390    auto endTime = Time::currentTimeMillis() + millisecondsToRunFor;
391
392    while (quitMessagePosted.get() == 0)
393    {
394        JUCE_AUTORELEASEPOOL
395        {
396            auto msRemaining = endTime - Time::currentTimeMillis();
397
398            if (msRemaining <= 0)
399                break;
400
401            CFRunLoopRunInMode (kCFRunLoopDefaultMode, jmin (1.0, msRemaining * 0.001), true);
402
403            if (NSEvent* e = [NSApp nextEventMatchingMask: NSEventMaskAny
404                                                untilDate: [NSDate dateWithTimeIntervalSinceNow: 0.001]
405                                                   inMode: NSDefaultRunLoopMode
406                                                  dequeue: YES])
407                if (isEventBlockedByModalComps == nullptr || ! (*isEventBlockedByModalComps) (e))
408                    [NSApp sendEvent: e];
409        }
410    }
411
412    return quitMessagePosted.get() == 0;
413}
414#endif
415
416//==============================================================================
417void initialiseNSApplication();
418void initialiseNSApplication()
419{
420    JUCE_AUTORELEASEPOOL
421    {
422        [NSApplication sharedApplication];
423    }
424}
425
426static AppDelegate* appDelegate = nullptr;
427
428void MessageManager::doPlatformSpecificInitialisation()
429{
430    if (appDelegate == nil)
431        appDelegate = new AppDelegate();
432}
433
434void MessageManager::doPlatformSpecificShutdown()
435{
436    delete appDelegate;
437    appDelegate = nullptr;
438}
439
440bool MessageManager::postMessageToSystemQueue (MessageBase* message)
441{
442    jassert (appDelegate != nil);
443    appDelegate->messageQueue.post (message);
444    return true;
445}
446
447void MessageManager::broadcastMessage (const String& message)
448{
449    NSDictionary* info = [NSDictionary dictionaryWithObject: juceStringToNS (message)
450                                                     forKey: nsStringLiteral ("message")];
451
452    [[NSDistributedNotificationCenter defaultCenter] postNotificationName: AppDelegate::getBroadcastEventName()
453                                                                   object: nil
454                                                                 userInfo: info];
455}
456
457// Special function used by some plugin classes to re-post carbon events
458void __attribute__ ((visibility("default"))) repostCurrentNSEvent();
459void __attribute__ ((visibility("default"))) repostCurrentNSEvent()
460{
461    struct EventReposter  : public CallbackMessage
462    {
463        EventReposter() : e ([[NSApp currentEvent] retain])  {}
464        ~EventReposter() override  { [e release]; }
465
466        void messageCallback() override
467        {
468            [NSApp postEvent: e atStart: YES];
469        }
470
471        NSEvent* e;
472    };
473
474    (new EventReposter())->post();
475}
476
477
478//==============================================================================
479#if JUCE_MAC
480struct MountedVolumeListChangeDetector::Pimpl
481{
482    Pimpl (MountedVolumeListChangeDetector& d)  : owner (d)
483    {
484        static ObserverClass cls;
485        delegate = [cls.createInstance() init];
486        ObserverClass::setOwner (delegate, this);
487
488        NSNotificationCenter* nc = [[NSWorkspace sharedWorkspace] notificationCenter];
489
490        [nc addObserver: delegate selector: @selector (changed:) name: NSWorkspaceDidMountNotification   object: nil];
491        [nc addObserver: delegate selector: @selector (changed:) name: NSWorkspaceDidUnmountNotification object: nil];
492    }
493
494    ~Pimpl()
495    {
496        [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver: delegate];
497        [delegate release];
498    }
499
500private:
501    MountedVolumeListChangeDetector& owner;
502    id delegate;
503
504    struct ObserverClass   : public ObjCClass<NSObject>
505    {
506        ObserverClass()  : ObjCClass<NSObject> ("JUCEDriveObserver_")
507        {
508            addIvar<Pimpl*> ("owner");
509            addMethod (@selector (changed:), changed, "v@:@");
510            addProtocol (@protocol (NSTextInput));
511            registerClass();
512        }
513
514        static Pimpl* getOwner (id self)                { return getIvar<Pimpl*> (self, "owner"); }
515        static void setOwner (id self, Pimpl* owner)    { object_setInstanceVariable (self, "owner", owner); }
516
517        static void changed (id self, SEL, NSNotification*)
518        {
519            getOwner (self)->owner.mountedVolumeListChanged();
520        }
521    };
522};
523
524MountedVolumeListChangeDetector::MountedVolumeListChangeDetector()  { pimpl.reset (new Pimpl (*this)); }
525MountedVolumeListChangeDetector::~MountedVolumeListChangeDetector() {}
526#endif
527
528} // namespace juce
529