michael@0: /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: // NSApplication delegate for Mac OS X Cocoa API. michael@0: michael@0: // As of 10.4 Tiger, the system can send six kinds of Apple Events to an application; michael@0: // a well-behaved XUL app should have some kind of handling for all of them. michael@0: // michael@0: // See http://developer.apple.com/documentation/Cocoa/Conceptual/ScriptableCocoaApplications/SApps_handle_AEs/chapter_11_section_3.html for details. michael@0: michael@0: #import michael@0: #import michael@0: michael@0: #include "nsCOMPtr.h" michael@0: #include "nsINativeAppSupport.h" michael@0: #include "nsAppRunner.h" michael@0: #include "nsComponentManagerUtils.h" michael@0: #include "nsIServiceManager.h" michael@0: #include "nsServiceManagerUtils.h" michael@0: #include "nsIAppStartup.h" michael@0: #include "nsIObserverService.h" michael@0: #include "nsISupportsPrimitives.h" michael@0: #include "nsObjCExceptions.h" michael@0: #include "nsIFile.h" michael@0: #include "nsDirectoryServiceDefs.h" michael@0: #include "nsICommandLineRunner.h" michael@0: #include "nsIMacDockSupport.h" michael@0: #include "nsIStandaloneNativeMenu.h" michael@0: #include "nsILocalFileMac.h" michael@0: #include "nsString.h" michael@0: #include "nsCommandLineServiceMac.h" michael@0: michael@0: class AutoAutoreleasePool { michael@0: public: michael@0: AutoAutoreleasePool() michael@0: { michael@0: mLocalPool = [[NSAutoreleasePool alloc] init]; michael@0: } michael@0: ~AutoAutoreleasePool() michael@0: { michael@0: [mLocalPool release]; michael@0: } michael@0: private: michael@0: NSAutoreleasePool *mLocalPool; michael@0: }; michael@0: michael@0: @interface MacApplicationDelegate : NSObject michael@0: { michael@0: } michael@0: michael@0: @end michael@0: michael@0: static bool sProcessedGetURLEvent = false; michael@0: michael@0: @class GeckoNSApplication; michael@0: michael@0: // Methods that can be called from non-Objective-C code. michael@0: michael@0: // This is needed, on relaunch, to force the OS to use the "Cocoa Dock API" michael@0: // instead of the "Carbon Dock API". For more info see bmo bug 377166. michael@0: void michael@0: EnsureUseCocoaDockAPI() michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: [GeckoNSApplication sharedApplication]; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: void michael@0: SetupMacApplicationDelegate() michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: // this is called during startup, outside an event loop, and therefore michael@0: // needs an autorelease pool to avoid cocoa object leakage (bug 559075) michael@0: AutoAutoreleasePool pool; michael@0: michael@0: // Ensure that ProcessPendingGetURLAppleEvents() doesn't regress bug 377166. michael@0: [GeckoNSApplication sharedApplication]; michael@0: michael@0: // This call makes it so that application:openFile: doesn't get bogus calls michael@0: // from Cocoa doing its own parsing of the argument string. And yes, we need michael@0: // to use a string with a boolean value in it. That's just how it works. michael@0: [[NSUserDefaults standardUserDefaults] setObject:@"NO" michael@0: forKey:@"NSTreatUnknownArgumentsAsOpen"]; michael@0: michael@0: // Create the delegate. This should be around for the lifetime of the app. michael@0: MacApplicationDelegate *delegate = [[MacApplicationDelegate alloc] init]; michael@0: [NSApp setDelegate:delegate]; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: // Indirectly make the OS process any pending GetURL Apple events. This is michael@0: // done via _DPSNextEvent() (an undocumented AppKit function called from michael@0: // [NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:]). Apple michael@0: // events are only processed if 'dequeue' is 'YES' -- so we need to call michael@0: // [NSApplication sendEvent:] on any event that gets returned. 'event' will michael@0: // never itself be an Apple event, and it may be 'nil' even when Apple events michael@0: // are processed. michael@0: void michael@0: ProcessPendingGetURLAppleEvents() michael@0: { michael@0: AutoAutoreleasePool pool; michael@0: bool keepSpinning = true; michael@0: while (keepSpinning) { michael@0: sProcessedGetURLEvent = false; michael@0: NSEvent *event = [NSApp nextEventMatchingMask:NSAnyEventMask michael@0: untilDate:nil michael@0: inMode:NSDefaultRunLoopMode michael@0: dequeue:YES]; michael@0: if (event) michael@0: [NSApp sendEvent:event]; michael@0: keepSpinning = sProcessedGetURLEvent; michael@0: } michael@0: } michael@0: michael@0: @implementation MacApplicationDelegate michael@0: michael@0: - (id)init michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; michael@0: michael@0: if ((self = [super init])) { michael@0: NSAppleEventManager *aeMgr = [NSAppleEventManager sharedAppleEventManager]; michael@0: michael@0: [aeMgr setEventHandler:self michael@0: andSelector:@selector(handleAppleEvent:withReplyEvent:) michael@0: forEventClass:kInternetEventClass michael@0: andEventID:kAEGetURL]; michael@0: michael@0: [aeMgr setEventHandler:self michael@0: andSelector:@selector(handleAppleEvent:withReplyEvent:) michael@0: forEventClass:'WWW!' michael@0: andEventID:'OURL']; michael@0: michael@0: [aeMgr setEventHandler:self michael@0: andSelector:@selector(handleAppleEvent:withReplyEvent:) michael@0: forEventClass:kCoreEventClass michael@0: andEventID:kAEOpenDocuments]; michael@0: michael@0: if (![NSApp windowsMenu]) { michael@0: // If the application has a windows menu, it will keep it up to date and michael@0: // prepend the window list to the Dock menu automatically. michael@0: NSMenu* windowsMenu = [[NSMenu alloc] initWithTitle:@"Window"]; michael@0: [NSApp setWindowsMenu:windowsMenu]; michael@0: [windowsMenu release]; michael@0: } michael@0: } michael@0: return self; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(nil); michael@0: } michael@0: michael@0: - (void)dealloc michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK; michael@0: michael@0: NSAppleEventManager *aeMgr = [NSAppleEventManager sharedAppleEventManager]; michael@0: [aeMgr removeEventHandlerForEventClass:kInternetEventClass andEventID:kAEGetURL]; michael@0: [aeMgr removeEventHandlerForEventClass:'WWW!' andEventID:'OURL']; michael@0: [aeMgr removeEventHandlerForEventClass:kCoreEventClass andEventID:kAEOpenDocuments]; michael@0: [super dealloc]; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK; michael@0: } michael@0: michael@0: // The method that NSApplication calls upon a request to reopen, such as when michael@0: // the Dock icon is clicked and no windows are open. A "visible" window may be michael@0: // miniaturized, so we can't skip nsCocoaNativeReOpen() if 'flag' is 'true'. michael@0: - (BOOL)applicationShouldHandleReopen:(NSApplication*)theApp hasVisibleWindows:(BOOL)flag michael@0: { michael@0: nsCOMPtr nas = do_CreateInstance(NS_NATIVEAPPSUPPORT_CONTRACTID); michael@0: NS_ENSURE_TRUE(nas, NO); michael@0: michael@0: // Go to the common Carbon/Cocoa reopen method. michael@0: nsresult rv = nas->ReOpen(); michael@0: NS_ENSURE_SUCCESS(rv, NO); michael@0: michael@0: // NO says we don't want NSApplication to do anything else for us. michael@0: return NO; michael@0: } michael@0: michael@0: // The method that NSApplication calls when documents are requested to be opened. michael@0: // It will be called once for each selected document. michael@0: - (BOOL)application:(NSApplication*)theApplication openFile:(NSString*)filename michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; michael@0: michael@0: NSURL *url = [NSURL fileURLWithPath:filename]; michael@0: if (!url) michael@0: return NO; michael@0: michael@0: NSString *urlString = [url absoluteString]; michael@0: if (!urlString) michael@0: return NO; michael@0: michael@0: // Add the URL to any command line we're currently setting up. michael@0: if (CommandLineServiceMac::AddURLToCurrentCommandLine([urlString UTF8String])) michael@0: return YES; michael@0: michael@0: nsCOMPtr inFile; michael@0: nsresult rv = NS_NewLocalFileWithCFURL((CFURLRef)url, true, getter_AddRefs(inFile)); michael@0: if (NS_FAILED(rv)) michael@0: return NO; michael@0: michael@0: nsCOMPtr cmdLine(do_CreateInstance("@mozilla.org/toolkit/command-line;1")); michael@0: if (!cmdLine) { michael@0: NS_ERROR("Couldn't create command line!"); michael@0: return NO; michael@0: } michael@0: michael@0: nsCString filePath; michael@0: rv = inFile->GetNativePath(filePath); michael@0: if (NS_FAILED(rv)) michael@0: return NO; michael@0: michael@0: nsCOMPtr workingDir; michael@0: rv = NS_GetSpecialDirectory(NS_OS_CURRENT_WORKING_DIR, getter_AddRefs(workingDir)); michael@0: if (NS_FAILED(rv)) michael@0: return NO; michael@0: michael@0: const char *argv[3] = {nullptr, "-file", filePath.get()}; michael@0: rv = cmdLine->Init(3, argv, workingDir, nsICommandLine::STATE_REMOTE_EXPLICIT); michael@0: if (NS_FAILED(rv)) michael@0: return NO; michael@0: michael@0: if (NS_SUCCEEDED(cmdLine->Run())) michael@0: return YES; michael@0: michael@0: return NO; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO); michael@0: } michael@0: michael@0: // The method that NSApplication calls when documents are requested to be printed michael@0: // from the Finder (under the "File" menu). michael@0: // It will be called once for each selected document. michael@0: - (BOOL)application:(NSApplication*)theApplication printFile:(NSString*)filename michael@0: { michael@0: return NO; michael@0: } michael@0: michael@0: // Create the menu that shows up in the Dock. michael@0: - (NSMenu*)applicationDockMenu:(NSApplication*)sender michael@0: { michael@0: NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; michael@0: michael@0: // Create the NSMenu that will contain the dock menu items. michael@0: NSMenu *menu = [[[NSMenu alloc] initWithTitle:@""] autorelease]; michael@0: [menu setAutoenablesItems:NO]; michael@0: michael@0: // Add application-specific dock menu items. On error, do not insert the michael@0: // dock menu items. michael@0: nsresult rv; michael@0: nsCOMPtr dockSupport = do_GetService("@mozilla.org/widget/macdocksupport;1", &rv); michael@0: if (NS_FAILED(rv) || !dockSupport) michael@0: return menu; michael@0: michael@0: nsCOMPtr dockMenu; michael@0: rv = dockSupport->GetDockMenu(getter_AddRefs(dockMenu)); michael@0: if (NS_FAILED(rv) || !dockMenu) michael@0: return menu; michael@0: michael@0: // Determine if the dock menu items should be displayed. This also gives michael@0: // the menu the opportunity to update itself before display. michael@0: bool shouldShowItems; michael@0: rv = dockMenu->MenuWillOpen(&shouldShowItems); michael@0: if (NS_FAILED(rv) || !shouldShowItems) michael@0: return menu; michael@0: michael@0: // Obtain a copy of the native menu. michael@0: NSMenu * nativeDockMenu; michael@0: rv = dockMenu->GetNativeMenu(reinterpret_cast(&nativeDockMenu)); michael@0: if (NS_FAILED(rv) || !nativeDockMenu) michael@0: return menu; michael@0: michael@0: // Loop through the application-specific dock menu and insert its michael@0: // contents into the dock menu that we are building for Cocoa. michael@0: int numDockMenuItems = [nativeDockMenu numberOfItems]; michael@0: if (numDockMenuItems > 0) { michael@0: if ([menu numberOfItems] > 0) michael@0: [menu addItem:[NSMenuItem separatorItem]]; michael@0: michael@0: for (int i = 0; i < numDockMenuItems; i++) { michael@0: NSMenuItem * itemCopy = [[nativeDockMenu itemAtIndex:i] copy]; michael@0: [menu addItem:itemCopy]; michael@0: [itemCopy release]; michael@0: } michael@0: } michael@0: michael@0: return menu; michael@0: michael@0: NS_OBJC_END_TRY_ABORT_BLOCK_NIL; michael@0: } michael@0: michael@0: // If we don't handle applicationShouldTerminate:, a call to [NSApp terminate:] michael@0: // (from the browser or from the OS) can result in an unclean shutdown. michael@0: - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender michael@0: { michael@0: nsCOMPtr obsServ = michael@0: do_GetService("@mozilla.org/observer-service;1"); michael@0: if (!obsServ) michael@0: return NSTerminateNow; michael@0: michael@0: nsCOMPtr cancelQuit = michael@0: do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID); michael@0: if (!cancelQuit) michael@0: return NSTerminateNow; michael@0: michael@0: cancelQuit->SetData(false); michael@0: obsServ->NotifyObservers(cancelQuit, "quit-application-requested", nullptr); michael@0: michael@0: bool abortQuit; michael@0: cancelQuit->GetData(&abortQuit); michael@0: if (abortQuit) michael@0: return NSTerminateCancel; michael@0: michael@0: nsCOMPtr appService = michael@0: do_GetService("@mozilla.org/toolkit/app-startup;1"); michael@0: if (appService) michael@0: appService->Quit(nsIAppStartup::eForceQuit); michael@0: michael@0: return NSTerminateNow; michael@0: } michael@0: michael@0: - (void)handleAppleEvent:(NSAppleEventDescriptor*)event withReplyEvent:(NSAppleEventDescriptor*)replyEvent michael@0: { michael@0: if (!event) michael@0: return; michael@0: michael@0: AutoAutoreleasePool pool; michael@0: michael@0: bool isGetURLEvent = michael@0: ([event eventClass] == kInternetEventClass && [event eventID] == kAEGetURL); michael@0: if (isGetURLEvent) michael@0: sProcessedGetURLEvent = true; michael@0: michael@0: if (isGetURLEvent || michael@0: ([event eventClass] == 'WWW!' && [event eventID] == 'OURL')) { michael@0: NSString* urlString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; michael@0: michael@0: // don't open chrome URLs michael@0: NSString* schemeString = [[NSURL URLWithString:urlString] scheme]; michael@0: if (!schemeString || michael@0: [schemeString compare:@"chrome" michael@0: options:NSCaseInsensitiveSearch michael@0: range:NSMakeRange(0, [schemeString length])] == NSOrderedSame) { michael@0: return; michael@0: } michael@0: michael@0: // Add the URL to any command line we're currently setting up. michael@0: if (CommandLineServiceMac::AddURLToCurrentCommandLine([urlString UTF8String])) michael@0: return; michael@0: michael@0: nsCOMPtr cmdLine(do_CreateInstance("@mozilla.org/toolkit/command-line;1")); michael@0: if (!cmdLine) { michael@0: NS_ERROR("Couldn't create command line!"); michael@0: return; michael@0: } michael@0: nsCOMPtr workingDir; michael@0: nsresult rv = NS_GetSpecialDirectory(NS_OS_CURRENT_WORKING_DIR, getter_AddRefs(workingDir)); michael@0: if (NS_FAILED(rv)) michael@0: return; michael@0: const char *argv[3] = {nullptr, "-url", [urlString UTF8String]}; michael@0: rv = cmdLine->Init(3, argv, workingDir, nsICommandLine::STATE_REMOTE_EXPLICIT); michael@0: if (NS_FAILED(rv)) michael@0: return; michael@0: rv = cmdLine->Run(); michael@0: } michael@0: else if ([event eventClass] == kCoreEventClass && [event eventID] == kAEOpenDocuments) { michael@0: NSAppleEventDescriptor* fileListDescriptor = [event paramDescriptorForKeyword:keyDirectObject]; michael@0: if (!fileListDescriptor) michael@0: return; michael@0: michael@0: // Descriptor list indexing is one-based... michael@0: NSInteger numberOfFiles = [fileListDescriptor numberOfItems]; michael@0: for (NSInteger i = 1; i <= numberOfFiles; i++) { michael@0: NSString* urlString = [[fileListDescriptor descriptorAtIndex:i] stringValue]; michael@0: if (!urlString) michael@0: continue; michael@0: michael@0: // We need a path, not a URL michael@0: NSURL* url = [NSURL URLWithString:urlString]; michael@0: if (!url) michael@0: continue; michael@0: michael@0: [self application:NSApp openFile:[url path]]; michael@0: } michael@0: } michael@0: } michael@0: michael@0: @end