/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is a Cocoa widget run loop and event implementation. * * The Initial Developer of the Original Code is Google Inc. * Portions created by the Initial Developer are Copyright (C) 2006 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Mark Mentovai (Original Author) * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ /* * Runs the main native Cocoa run loop, interrupting it as needed to process * Gecko events. */ #import #include "nsAppShell.h" #include "nsCOMPtr.h" #include "nsIFile.h" #include "nsDirectoryServiceDefs.h" #include "nsString.h" #include "nsIRollupListener.h" #include "nsIWidget.h" #include "nsThreadUtils.h" #include "nsIWindowMediator.h" #include "nsServiceManagerUtils.h" #include "nsIInterfaceRequestor.h" #include "nsIWebBrowserChrome.h" #include "nsObjCExceptions.h" #include "nsCocoaUtils.h" #include "nsChildView.h" #include "nsToolkit.h" #include "npapi.h" // defined in nsChildView.mm extern nsIRollupListener * gRollupListener; extern nsIWidget * gRollupWidget; extern PRUint32 gLastModifierState; // defined in nsCocoaWindow.mm extern PRInt32 gXULModalLevel; #ifndef __LP64__ #include void (*WebKit_WebInitForCarbon)() = NULL; // Plugins may exist that use the WebKit framework. Those that are // Carbon-based need to call WebKit's WebInitForCarbon() method. There // currently appears to be only one Carbon WebKit plugin -- // DivXBrowserPlugin (included with the DivX Web Player, // http://www.divx.com/en/downloads/divx/mac). See bug 509130. // // The source-code for WebInitForCarbon() is in the WebKit source tree's // WebKit/mac/Carbon/CarbonUtils.mm file. Among other things it installs // an idle timer on the main event loop, whose target is the PoolCleaner() // function (also in CarbonUtils.mm). WebInitForCarbon() allocates an // NSAutoreleasePool object which it stores in the global sPool variable. // PoolCleaner() periodically releases/drains sPool and creates another // NSAutoreleasePool object to take its place. The intention is to ensure // an autorelease pool is in place for whatever Objective-C code may be // called by WebKit code, and that it periodically gets "cleaned". But // PoolCleaner()'s periodic cleaning has a very bad effect on us -- it // causes objects to be deleted prematurely, so that attempts to access them // cause crashes. This is probably because, when WebInitForCarbon() is // called from a plugin in a Cocoa browser, one or more autorelease pools // are already in place. So, other things being equal, PoolCleaner() should // have a similar effect on any Cocoa app that hosts a Carbon WebKit plugin. // // PoolCleaner() only "works" if the autorelease pool count (returned by // WKGetNSAutoreleasePoolCount(), stored in numPools) is the same as when // sPool was last set. So we can permanently disable it by ensuring that, // when sPool is first set, numPools gets set to a value that it will never // have again until just after the app shell is destroyed. To accomplish // this we need to call WebInitForCarbon() ourselves, before any plugin // calls it (subsequent calls to WebInitForCarbon() (after the first) are // no-ops): We release all of the app shell's autorelease pools (including // mMainPool) just before calling WebInitForCarbon(), then restore mMainPool // just afterwards (before the idle timer has time to call PoolCleaner()). // // WKGetNSAutoreleasePoolCount() only works on OS X 10.5 and below -- not on // OS X 10.6 and above. So PoolCleaner() is always disabled on 10.6 and // above -- we needn't do anything to explicitly disable it. // // WKGetNSAutoreleasePoolCount() is a thin wrapper around the following code: // // unsigned count = NSPushAutoreleasePool(0); // NSPopAutoreleasePool(count); // return count; // // NSPushAutoreleasePool() and NSPopAutoreleasePool() are undocumented // functions from the Foundation framework. On OS X 10.5.X and below their // declarations are (as best I can tell) as follows. ('capacity' is // presumably the initial capacity, in number of items, of the autorelease // pool to be created.) // // unsigned NSPushAutoreleasePool(unsigned capacity); // void NSPopAutoreleasePool(unsigned offset); // // But as of OS X 10.6 these functions appear to have changed as follows: // // AutoreleasePool *NSPushAutoreleasePool(unsigned capacity); // void NSPopAutoreleasePool(AutoreleasePool *aPool); static void InitCarbonWebKit() { if (!WebKit_WebInitForCarbon) { void* webkithandle = dlopen("/System/Library/Frameworks/WebKit.framework/WebKit", RTLD_LAZY); if (webkithandle) *(void **)(&WebKit_WebInitForCarbon) = dlsym(webkithandle, "WebInitForCarbon"); } if (WebKit_WebInitForCarbon) WebKit_WebInitForCarbon(); } #endif // __LP64__ static PRBool gAppShellMethodsSwizzled = PR_FALSE; // List of current Cocoa app-modal windows (nested if more than one). nsCocoaAppModalWindowList *gCocoaAppModalWindowList = NULL; // Push a Cocoa app-modal window onto the top of our list. nsresult nsCocoaAppModalWindowList::PushCocoa(NSWindow *aWindow, NSModalSession aSession) { NS_ENSURE_STATE(aWindow && aSession); mList.AppendElement(nsCocoaAppModalWindowListItem(aWindow, aSession)); return NS_OK; } // Pop the topmost Cocoa app-modal window off our list. aWindow and aSession // are just used to check that it's what we expect it to be. nsresult nsCocoaAppModalWindowList::PopCocoa(NSWindow *aWindow, NSModalSession aSession) { NS_ENSURE_STATE(aWindow && aSession); for (int i = mList.Length(); i > 0; --i) { nsCocoaAppModalWindowListItem &item = mList.ElementAt(i - 1); if (item.mSession) { NS_ASSERTION((item.mWindow == aWindow) && (item.mSession == aSession), "PopCocoa() called without matching call to PushCocoa()!"); mList.RemoveElementAt(i - 1); return NS_OK; } } NS_ERROR("PopCocoa() called without matching call to PushCocoa()!"); return NS_ERROR_FAILURE; } // Push a Gecko-modal window onto the top of our list. nsresult nsCocoaAppModalWindowList::PushGecko(NSWindow *aWindow, nsCocoaWindow *aWidget) { NS_ENSURE_STATE(aWindow && aWidget); mList.AppendElement(nsCocoaAppModalWindowListItem(aWindow, aWidget)); return NS_OK; } // Pop the topmost Gecko-modal window off our list. aWindow and aWidget are // just used to check that it's what we expect it to be. nsresult nsCocoaAppModalWindowList::PopGecko(NSWindow *aWindow, nsCocoaWindow *aWidget) { NS_ENSURE_STATE(aWindow && aWidget); for (int i = mList.Length(); i > 0; --i) { nsCocoaAppModalWindowListItem &item = mList.ElementAt(i - 1); if (item.mWidget) { NS_ASSERTION((item.mWindow == aWindow) && (item.mWidget == aWidget), "PopGecko() called without matching call to PushGecko()!"); mList.RemoveElementAt(i - 1); return NS_OK; } } NS_ERROR("PopGecko() called without matching call to PushGecko()!"); return NS_ERROR_FAILURE; } // The "current session" is normally the "session" corresponding to the // top-most Cocoa app-modal window (both on the screen and in our list). // But because Cocoa app-modal dialog can be "interrupted" by a Gecko-modal // dialog, the top-most Cocoa app-modal dialog may already have finished // (and no longer be visible). In this case we need to check the list for // the "next" visible Cocoa app-modal window (and return its "session"), or // (if no Cocoa app-modal window is visible) return nil. This way we ensure // (as we need to) that all nested Cocoa app-modal sessions are dealt with // before we get to any Gecko-modal session(s). See nsAppShell:: // ProcessNextNativeEvent() below. NSModalSession nsCocoaAppModalWindowList::CurrentSession() { if (![NSApp _isRunningAppModal]) return nil; NSModalSession currentSession = nil; for (int i = mList.Length(); i > 0; --i) { nsCocoaAppModalWindowListItem &item = mList.ElementAt(i - 1); if (item.mSession && [item.mWindow isVisible]) { currentSession = item.mSession; break; } } return currentSession; } // Has a Gecko modal dialog popped up over a Cocoa app-modal dialog? PRBool nsCocoaAppModalWindowList::GeckoModalAboveCocoaModal() { if (mList.IsEmpty()) return PR_FALSE; nsCocoaAppModalWindowListItem &topItem = mList.ElementAt(mList.Length() - 1); return (topItem.mWidget != nsnull); } // AppShellDelegate // // Cocoa bridge class. An object of this class is registered to receive // notifications. // @interface AppShellDelegate : NSObject { @private nsAppShell* mAppShell; } - (id)initWithAppShell:(nsAppShell*)aAppShell; - (void)applicationWillTerminate:(NSNotification*)aNotification; - (void)beginMenuTracking:(NSNotification*)aNotification; @end // nsAppShell implementation NS_IMETHODIMP nsAppShell::ResumeNative(void) { nsresult retval = nsBaseAppShell::ResumeNative(); if (NS_SUCCEEDED(retval) && (mSuspendNativeCount == 0) && mSkippedNativeCallback) { mSkippedNativeCallback = PR_FALSE; ScheduleNativeEventCallback(); } return retval; } nsAppShell::nsAppShell() : mAutoreleasePools(nsnull) , mDelegate(nsnull) , mCFRunLoop(NULL) , mCFRunLoopSource(NULL) , mRunningEventLoop(PR_FALSE) , mStarted(PR_FALSE) , mTerminated(PR_FALSE) , mNotifiedWillTerminate(PR_FALSE) , mSkippedNativeCallback(PR_FALSE) , mHadMoreEventsCount(0) , mRecursionDepth(0) , mNativeEventCallbackDepth(0) , mNativeEventScheduledDepth(0) { // mMainPool sits low on the autorelease pool stack to serve as a catch-all // for autoreleased objects on this thread. Because it won't be popped // until the appshell is destroyed, objects attached to this pool will // be leaked until app shutdown. You probably don't want this! // // Objects autoreleased to this pool may result in warnings in the future. mMainPool = [[NSAutoreleasePool alloc] init]; // A Cocoa event loop is running here if (and only if) we've been embedded // by a Cocoa app (like Camino). mRunningCocoaEmbedded = [NSApp isRunning] ? PR_TRUE : PR_FALSE; } nsAppShell::~nsAppShell() { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; if (mCFRunLoop) { if (mCFRunLoopSource) { ::CFRunLoopRemoveSource(mCFRunLoop, mCFRunLoopSource, kCFRunLoopCommonModes); ::CFRelease(mCFRunLoopSource); } ::CFRelease(mCFRunLoop); } if (mAutoreleasePools) { NS_ASSERTION(::CFArrayGetCount(mAutoreleasePools) == 0, "nsAppShell destroyed without popping all autorelease pools"); ::CFRelease(mAutoreleasePools); } [mDelegate release]; // Cocoa-based embedders (like Camino) call NS_TermEmbedding() (which // destroys us) before their own Cocoa infrastructure is fully shut down. // This infrastructure assumes that various objects which have a retain // count >= 1 will remain in existence, and that an autorelease pool will // still be available. But because mMainPool sits so low on the autorelease // stack, if we release it here there's a good chance that all the // aforementioned objects (including the other autorelease pools) will be // released, and havoc will result. // // So if we've been called from a Cocoa embedder, or in general if we've // been terminated using [NSApplication terminate:], we don't release // mMainPool here. This won't cause leaks, because after [NSApplication // terminate:] sends an NSApplicationWillTerminate notification it calls // [NSApplication _deallocHardCore:], which (after it uses [NSArray // makeObjectsPerformSelector:] to close all remaining windows) calls // [NSAutoreleasePool releaseAllPools] (to release all autorelease pools // on the current thread, which is the main thread). // // Cocoa embedders will almost certainly be terminated using [NSApplication // terminate:]. But we can be called from a Cocoa embedder's will-terminate // notification handler before our own is called (so that // mNotifiedWillTerminate isn't yet TRUE). To avoid this, we also check // mRunningCocoaEmbedded here. See bug 471948. if (!mNotifiedWillTerminate && !mRunningCocoaEmbedded) [mMainPool release]; NS_OBJC_END_TRY_ABORT_BLOCK } // Init // // Loads the nib (see bug 316076c21) and sets up the CFRunLoopSource used to // interrupt the main native run loop. // // public nsresult nsAppShell::Init() { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; // No event loop is running yet (unless Camino is running, or another // embedding app that uses NSApplicationMain()). Avoid autoreleasing // objects to mMainPool. The appshell retains objects it needs to be // long-lived and will release them as appropriate. NSAutoreleasePool* localPool = [[NSAutoreleasePool alloc] init]; // mAutoreleasePools is used as a stack of NSAutoreleasePool objects created // by |this|. CFArray is used instead of NSArray because NSArray wants to // retain each object you add to it, and you can't retain an // NSAutoreleasePool. mAutoreleasePools = ::CFArrayCreateMutable(nsnull, 0, nsnull); NS_ENSURE_STATE(mAutoreleasePools); // Get the path of the nib file, which lives in the GRE location nsCOMPtr nibFile; nsresult rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(nibFile)); NS_ENSURE_SUCCESS(rv, rv); nibFile->AppendNative(NS_LITERAL_CSTRING("res")); nibFile->AppendNative(NS_LITERAL_CSTRING("MainMenu.nib")); nsCAutoString nibPath; rv = nibFile->GetNativePath(nibPath); NS_ENSURE_SUCCESS(rv, rv); // This call initializes NSApplication unless: // 1) we're using xre -- NSApp's already been initialized by // MacApplicationDelegate.mm's EnsureUseCocoaDockAPI(). // 2) Camino is running (or another embedding app that uses // NSApplicationMain()) -- NSApp's already been initialized and // its main run loop is already running. [NSBundle loadNibFile: [NSString stringWithUTF8String:(const char*)nibPath.get()] externalNameTable: [NSDictionary dictionaryWithObject:[NSApplication sharedApplication] forKey:@"NSOwner"] withZone:NSDefaultMallocZone()]; mDelegate = [[AppShellDelegate alloc] initWithAppShell:this]; NS_ENSURE_STATE(mDelegate); // Add a CFRunLoopSource to the main native run loop. The source is // responsible for interrupting the run loop when Gecko events are ready. mCFRunLoop = [[NSRunLoop currentRunLoop] getCFRunLoop]; NS_ENSURE_STATE(mCFRunLoop); ::CFRetain(mCFRunLoop); CFRunLoopSourceContext context; bzero(&context, sizeof(context)); // context.version = 0; context.info = this; context.perform = ProcessGeckoEvents; mCFRunLoopSource = ::CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context); NS_ENSURE_STATE(mCFRunLoopSource); ::CFRunLoopAddSource(mCFRunLoop, mCFRunLoopSource, kCFRunLoopCommonModes); rv = nsBaseAppShell::Init(); #ifndef NP_NO_CARBON NS_InstallPluginKeyEventsHandler(); #endif gCocoaAppModalWindowList = new nsCocoaAppModalWindowList; if (!gAppShellMethodsSwizzled) { nsToolkit::SwizzleMethods([NSApplication class], @selector(beginModalSessionForWindow:), @selector(nsAppShell_NSApplication_beginModalSessionForWindow:)); nsToolkit::SwizzleMethods([NSApplication class], @selector(endModalSession:), @selector(nsAppShell_NSApplication_endModalSession:)); gAppShellMethodsSwizzled = PR_TRUE; } [localPool release]; #ifndef __LP64__ if (!nsToolkit::OnSnowLeopardOrLater()) { [mMainPool release]; InitCarbonWebKit(); mMainPool = [[NSAutoreleasePool alloc] init]; } #endif return rv; NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; } // ProcessGeckoEvents // // The "perform" target of mCFRunLoop, called when mCFRunLoopSource is // signalled from ScheduleNativeEventCallback. // // Arrange for Gecko events to be processed on demand (in response to a call // to ScheduleNativeEventCallback(), if processing of Gecko events via "native // methods" hasn't been suspended). This happens in NativeEventCallback(). // // protected static void nsAppShell::ProcessGeckoEvents(void* aInfo) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; nsAppShell* self = static_cast (aInfo); if (self->mRunningEventLoop) { self->mRunningEventLoop = PR_FALSE; // The run loop may be sleeping -- [NSRunLoop runMode:...] // won't return until it's given a reason to wake up. Awaken it by // posting a bogus event. There's no need to make the event // presentable. // // But _don't_ set windowNumber to '-1' -- that can lead to nasty // wierdness like bmo bug 397039 (a crash in [NSApp sendEvent:] on one of // these fake events, because the -1 has gotten changed into the number // of an actual NSWindow object, and that NSWindow object has just been // destroyed). Setting windowNumber to '0' seems to work fine -- this // seems to prevent the OS from ever trying to associate our bogus event // with a particular NSWindow object. [NSApp postEvent:[NSEvent otherEventWithType:NSApplicationDefined location:NSMakePoint(0,0) modifierFlags:0 timestamp:0 windowNumber:0 context:NULL subtype:0 data1:0 data2:0] atStart:NO]; } if (self->mSuspendNativeCount <= 0) { ++self->mNativeEventCallbackDepth; self->NativeEventCallback(); --self->mNativeEventCallbackDepth; } else { self->mSkippedNativeCallback = PR_TRUE; } // Still needed to fix bug 343033 ("5-10 second delay or hang or crash // when quitting Cocoa Firefox"). [NSApp postEvent:[NSEvent otherEventWithType:NSApplicationDefined location:NSMakePoint(0,0) modifierFlags:0 timestamp:0 windowNumber:0 context:NULL subtype:0 data1:0 data2:0] atStart:NO]; // Normally every call to ScheduleNativeEventCallback() results in // exactly one call to ProcessGeckoEvents(). So each Release() here // normally balances exactly one AddRef() in ScheduleNativeEventCallback(). // But if Exit() is called just after ScheduleNativeEventCallback(), the // corresponding call to ProcessGeckoEvents() will never happen. We check // for this possibility in two different places -- here and in Exit() // itself. If we find here that Exit() has been called (that mTerminated // is PR_TRUE), it's because we've been called recursively, that Exit() was // called from self->NativeEventCallback() above, and that we're unwinding // the recursion. In this case we'll never be called again, and we balance // here any extra calls to ScheduleNativeEventCallback(). // // When ProcessGeckoEvents() is called recursively, it's because of a // call to ScheduleNativeEventCallback() from NativeEventCallback(). We // balance the "extra" AddRefs here (rather than always in Exit()) in order // to ensure that 'self' stays alive until the end of this method. We also // make sure not to finish the balancing until all the recursion has been // unwound. if (self->mTerminated) { PRInt32 releaseCount = 0; if (self->mNativeEventScheduledDepth > self->mNativeEventCallbackDepth) { releaseCount = PR_AtomicSet(&self->mNativeEventScheduledDepth, self->mNativeEventCallbackDepth); } while (releaseCount-- > self->mNativeEventCallbackDepth) self->Release(); } else { // As best we can tell, every call to ProcessGeckoEvents() is triggered // by a call to ScheduleNativeEventCallback(). But we've seen a few // (non-reproducible) cases of double-frees that *might* have been caused // by spontaneous calls (from the OS) to ProcessGeckoEvents(). So we // deal with that possibility here. if (PR_AtomicDecrement(&self->mNativeEventScheduledDepth) < 0) { PR_AtomicSet(&self->mNativeEventScheduledDepth, 0); NS_WARNING("Spontaneous call to ProcessGeckoEvents()!"); } else { self->Release(); } } NS_OBJC_END_TRY_ABORT_BLOCK; } // WillTerminate // // Called by the AppShellDelegate when an NSApplicationWillTerminate // notification is posted. After this method is called, native events should // no longer be processed. The NSApplicationWillTerminate notification is // only posted when [NSApp terminate:] is called, which doesn't happen on a // "normal" application quit. // // public void nsAppShell::WillTerminate() { mNotifiedWillTerminate = PR_TRUE; if (mTerminated) return; mTerminated = PR_TRUE; // Calling [NSApp terminate:] causes (among other things) an // NSApplicationWillTerminate notification to be posted and the main run // loop to die before returning (in the call to [NSApp run]). So this is // our last crack at processing any remaining Gecko events. NS_ProcessPendingEvents(NS_GetCurrentThread()); // Unless we call nsBaseAppShell::Exit() here, it might not get called // at all. nsBaseAppShell::Exit(); } // ScheduleNativeEventCallback // // Called (possibly on a non-main thread) when Gecko has an event that // needs to be processed. The Gecko event needs to be processed on the // main thread, so the native run loop must be interrupted. // // In nsBaseAppShell.cpp, the mNativeEventPending variable is used to // ensure that ScheduleNativeEventCallback() is called no more than once // per call to NativeEventCallback(). ProcessGeckoEvents() can skip its // call to NativeEventCallback() if processing of Gecko events by native // means is suspended (using nsIAppShell::SuspendNative()), which will // suspend calls from nsBaseAppShell::OnDispatchedEvent() to // ScheduleNativeEventCallback(). But when Gecko event processing by // native means is resumed (in ResumeNative()), an extra call is made to // ScheduleNativeEventCallback() (from ResumeNative()). This triggers // another call to ProcessGeckoEvents(), which calls NativeEventCallback(), // and nsBaseAppShell::OnDispatchedEvent() resumes calling // ScheduleNativeEventCallback(). // // protected virtual void nsAppShell::ScheduleNativeEventCallback() { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; if (mTerminated) return; // Each AddRef() here is normally balanced by exactly one Release() in // ProcessGeckoEvents(). But there are exceptions, for which see // ProcessGeckoEvents() and Exit(). NS_ADDREF_THIS(); PR_AtomicIncrement(&mNativeEventScheduledDepth); // This will invoke ProcessGeckoEvents on the main thread. ::CFRunLoopSourceSignal(mCFRunLoopSource); ::CFRunLoopWakeUp(mCFRunLoop); NS_OBJC_END_TRY_ABORT_BLOCK; } // ProcessNextNativeEvent // // If aMayWait is false, process a single native event. If it is true, run // the native run loop until stopped by ProcessGeckoEvents. // // Returns true if more events are waiting in the native event queue. // // But (now that we're using [NSRunLoop runMode:beforeDate:]) it's too // expensive to call ProcessNextNativeEvent() many times in a row (in a // tight loop), so we never return true more than kHadMoreEventsCountMax // times in a row. This doesn't seem to cause native event starvation. // // protected virtual PRBool nsAppShell::ProcessNextNativeEvent(PRBool aMayWait) { PRBool moreEvents = PR_FALSE; NS_OBJC_BEGIN_TRY_ABORT_BLOCK; PRBool eventProcessed = PR_FALSE; NSString* currentMode = nil; if (mTerminated) return PR_FALSE; // We don't want any native events to be processed here (via Gecko) while // Cocoa is displaying an app-modal dialog (as opposed to a window-modal // "sheet" or a Gecko-modal dialog). Otherwise Cocoa event-processing loops // may be interrupted, and inappropriate events may get through to the // browser window(s) underneath. This resolves bmo bugs 419668 and 420967. // // But we need more complex handling (we need to make an exception) if a // Gecko modal dialog is running above the Cocoa app-modal dialog -- for // which see below. if ([NSApp _isRunningAppModal] && (!gCocoaAppModalWindowList || !gCocoaAppModalWindowList->GeckoModalAboveCocoaModal())) return PR_FALSE; PRBool wasRunningEventLoop = mRunningEventLoop; mRunningEventLoop = aMayWait; NSDate* waitUntil = nil; if (aMayWait) waitUntil = [NSDate distantFuture]; NSRunLoop* currentRunLoop = [NSRunLoop currentRunLoop]; do { // No autorelease pool is provided here, because OnProcessNextEvent // and AfterProcessNextEvent are responsible for maintaining it. NS_ASSERTION(mAutoreleasePools && ::CFArrayGetCount(mAutoreleasePools), "No autorelease pool for native event"); // If an event is waiting to be processed, run the main event loop // just long enough to process it. For some reason, using [NSApp // nextEventMatchingMask:...] to dequeue the event and [NSApp sendEvent:] // to "send" it causes trouble, so we no longer do that. (The trouble // was very strange, and only happened while processing Gecko events on // demand (via ProcessGeckoEvents()), as opposed to processing Gecko // events in a tight loop (via nsBaseAppShell::Run()): Particularly in // Camino, mouse-down events sometimes got dropped (or mis-handled), so // that (for example) you sometimes needed to click more than once on a // button to make it work (the zoom button was particularly susceptible). // You also sometimes had to ctrl-click or right-click multiple times to // bring up a context menu.) // Now that we're using [NSRunLoop runMode:beforeDate:], it's too // expensive to call ProcessNextNativeEvent() many times in a row, so we // never return true more than kHadMoreEventsCountMax in a row. I'm not // entirely sure why [NSRunLoop runMode:beforeDate:] is too expensive, // since it and its cousin [NSRunLoop acceptInputForMode:beforeDate:] are // designed to be called in a tight loop. Possibly the problem is due to // combining [NSRunLoop runMode:beforeDate] with [NSApp // nextEventMatchingMask:...]. // We special-case timer events (events of type NSPeriodic) to avoid // starving them. Apple's documentation is very scanty, and it's now // more scanty than it used to be. But it appears that [NSRunLoop // acceptInputForMode:beforeDate:] doesn't process timer events at all, // that it is called from [NSRunLoop runMode:beforeDate:], and that // [NSRunLoop runMode:beforeDate:], though it does process timer events, // doesn't return after doing so. To get around this, when aWait is // PR_FALSE we check for timer events and process them using [NSApp // sendEvent:]. When aWait is PR_TRUE [NSRunLoop runMode:beforeDate:] // will only return on a "real" event. But there's code in // ProcessGeckoEvents() that should (when need be) wake us up by sending // a "fake" "real" event. (See Apple's current doc on [NSRunLoop // runMode:beforeDate:] and a quote from what appears to be an older // version of this doc at // http://lists.apple.com/archives/cocoa-dev/2001/May/msg00559.html.) // If the current mode is something else than NSDefaultRunLoopMode, look // for events in that mode. currentMode = [currentRunLoop currentMode]; if (!currentMode) currentMode = NSDefaultRunLoopMode; NSEvent* nextEvent = nil; // If we're running modal (or not in a Gecko "main" event loop) we still // need to use nextEventMatchingMask and sendEvent -- otherwise (in // Minefield) the modal window (or non-main event loop) won't receive key // events or most mouse events. if ([NSApp _isRunningModal] || !InGeckoMainEventLoop()) { if ((nextEvent = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:waitUntil inMode:currentMode dequeue:YES])) { // If we're in a Cocoa app-modal session that's been interrupted by a // Gecko-modal dialog, send the event to the Cocoa app-modal dialog's // session. This ensures that the app-modal session won't be starved // of events, and fixes bugs 463473 and 442442. (The case of an // ordinary Cocoa app-modal dialog has been dealt with above.) // // Otherwise (if we're in an ordinary Gecko-modal dialog, or if we're // otherwise not in a Gecko main event loop), process the event as // expected. NSModalSession currentAppModalSession = nil; if (gCocoaAppModalWindowList) currentAppModalSession = gCocoaAppModalWindowList->CurrentSession(); if (currentAppModalSession) { [NSApp _modalSession:currentAppModalSession sendEvent:nextEvent]; } else { [NSApp sendEvent:nextEvent]; } eventProcessed = PR_TRUE; } } else { if (aMayWait || (nextEvent = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:nil inMode:currentMode dequeue:NO])) { if (nextEvent && ([nextEvent type] == NSPeriodic)) { nextEvent = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:waitUntil inMode:currentMode dequeue:YES]; [NSApp sendEvent:nextEvent]; } else { [currentRunLoop runMode:currentMode beforeDate:waitUntil]; } eventProcessed = PR_TRUE; } } } while (mRunningEventLoop); if (eventProcessed && (mHadMoreEventsCount < kHadMoreEventsCountMax)) { moreEvents = ([NSApp nextEventMatchingMask:NSAnyEventMask untilDate:nil inMode:currentMode dequeue:NO] != nil); } if (moreEvents) { // Once this reaches kHadMoreEventsCountMax, it will be reset to 0 the // next time through (whether or not we process any events then). ++mHadMoreEventsCount; } else { mHadMoreEventsCount = 0; } mRunningEventLoop = wasRunningEventLoop; NS_OBJC_END_TRY_ABORT_BLOCK; if (!moreEvents) { nsChildView::UpdateCurrentInputEventCount(); } return moreEvents; } // Returns PR_TRUE if Gecko events are currently being processed in its "main" // event loop (or one of its "main" event loops). Returns PR_FALSE if Gecko // events are being processed in a "nested" event loop, or if we're not // running in any sort of Gecko event loop. How we process native events in // ProcessNextNativeEvent() turns on our decision (and if we make the wrong // choice, the result may be a hang). // // We define the "main" event loop(s) as the place (or places) where Gecko // event processing "normally" takes place, and all other Gecko event loops // as "nested". The "nested" event loops are normally processed while a call // from a "main" event loop is on the stack ... but not always. For example, // the Venkman JavaScript debugger runs a "nested" event loop (in jsdService:: // EnterNestedEventLoop()) whenever it breaks into the current script. But // if this happens as the result of the user pressing a key combination, there // won't be any other Gecko event-processing call on the stack (e.g. // NS_ProcessNextEvent() or NS_ProcessPendingEvents()). (In the current // nsAppShell implementation, what counts as the "main" event loop is what // nsBaseAppShell::NativeEventCallback() does to process Gecko events. We // don't currently use nsBaseAppShell::Run().) PRBool nsAppShell::InGeckoMainEventLoop() { if ((gXULModalLevel > 0) || (mRecursionDepth > 0)) return PR_FALSE; if (mNativeEventCallbackDepth <= 0) return PR_FALSE; return PR_TRUE; } // Run // // Overrides the base class's Run() method to call [NSApp run] (which spins // the native run loop until the application quits). Since (unlike the base // class's Run() method) we don't process any Gecko events here, they need // to be processed elsewhere (in NativeEventCallback(), called from // ProcessGeckoEvents()). // // Camino calls [NSApp run] on its own (via NSApplicationMain()), and so // doesn't call nsAppShell::Run(). // // public NS_IMETHODIMP nsAppShell::Run(void) { NS_ASSERTION(!mStarted, "nsAppShell::Run() called multiple times"); if (mStarted) return NS_OK; mStarted = PR_TRUE; NS_OBJC_TRY_ABORT([NSApp run]); return NS_OK; } NS_IMETHODIMP nsAppShell::Exit(void) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; // This method is currently called more than once -- from (according to // mento) an nsAppExitEvent dispatched by nsAppStartup::Quit() and from an // XPCOM shutdown notification that nsBaseAppShell has registered to // receive. So we need to ensure that multiple calls won't break anything. // But we should also complain about it (since it isn't quite kosher). if (mTerminated) { NS_WARNING("nsAppShell::Exit() called redundantly"); return NS_OK; } mTerminated = PR_TRUE; delete gCocoaAppModalWindowList; gCocoaAppModalWindowList = NULL; #ifndef NP_NO_CARBON NS_RemovePluginKeyEventsHandler(); #endif // Quoting from Apple's doc on the [NSApplication stop:] method (from their // doc on the NSApplication class): "If this method is invoked during a // modal event loop, it will break that loop but not the main event loop." // nsAppShell::Exit() shouldn't be called from a modal event loop. So if // it is we complain about it (to users of debug builds) and call [NSApp // stop:] one extra time. (I'm not sure if modal event loops can be nested // -- Apple's docs don't say one way or the other. But the return value // of [NSApp _isRunningModal] doesn't change immediately after a call to // [NSApp stop:], so we have to assume that one extra call to [NSApp stop:] // will do the job.) BOOL cocoaModal = [NSApp _isRunningModal]; NS_ASSERTION(!cocoaModal, "Don't call nsAppShell::Exit() from a modal event loop!"); if (cocoaModal) [NSApp stop:nsnull]; [NSApp stop:nsnull]; // A call to Exit() just after a call to ScheduleNativeEventCallback() // prevents the (normally) matching call to ProcessGeckoEvents() from // happening. If we've been called from ProcessGeckoEvents() (as usually // happens), we take care of it there. But if we have an unbalanced call // to ScheduleNativeEventCallback() and ProcessGeckoEvents() isn't on the // stack, we need to take care of the problem here. if (!mNativeEventCallbackDepth && mNativeEventScheduledDepth) { PRInt32 releaseCount = PR_AtomicSet(&mNativeEventScheduledDepth, 0); while (releaseCount-- > 0) NS_RELEASE_THIS(); } return nsBaseAppShell::Exit(); NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; } // OnProcessNextEvent // // This nsIThreadObserver method is called prior to processing an event. // Set up an autorelease pool that will service any autoreleased Cocoa // objects during this event. This includes native events processed by // ProcessNextNativeEvent. The autorelease pool will be popped by // AfterProcessNextEvent, it is important for these two methods to be // tightly coupled. // // public NS_IMETHODIMP nsAppShell::OnProcessNextEvent(nsIThreadInternal *aThread, PRBool aMayWait, PRUint32 aRecursionDepth) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; mRecursionDepth = aRecursionDepth; NS_ASSERTION(mAutoreleasePools, "No stack on which to store autorelease pool"); NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; ::CFArrayAppendValue(mAutoreleasePools, pool); return nsBaseAppShell::OnProcessNextEvent(aThread, aMayWait, aRecursionDepth); NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; } // AfterProcessNextEvent // // This nsIThreadObserver method is called after event processing is complete. // The Cocoa implementation cleans up the autorelease pool create by the // previous OnProcessNextEvent call. // // public NS_IMETHODIMP nsAppShell::AfterProcessNextEvent(nsIThreadInternal *aThread, PRUint32 aRecursionDepth) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; mRecursionDepth = aRecursionDepth; CFIndex count = ::CFArrayGetCount(mAutoreleasePools); NS_ASSERTION(mAutoreleasePools && count, "Processed an event, but there's no autorelease pool?"); const NSAutoreleasePool* pool = static_cast (::CFArrayGetValueAtIndex(mAutoreleasePools, count - 1)); ::CFArrayRemoveValueAtIndex(mAutoreleasePools, count - 1); [pool release]; return nsBaseAppShell::AfterProcessNextEvent(aThread, aRecursionDepth); NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; } // AppShellDelegate implementation @implementation AppShellDelegate // initWithAppShell: // // Constructs the AppShellDelegate object - (id)initWithAppShell:(nsAppShell*)aAppShell { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; if ((self = [self init])) { mAppShell = aAppShell; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:NSApplicationWillTerminateNotification object:NSApp]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) name:NSApplicationDidBecomeActiveNotification object:NSApp]; [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(beginMenuTracking:) name:@"com.apple.HIToolbox.beginMenuTrackingNotification" object:nil]; } return self; NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } - (void)dealloc { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; [[NSNotificationCenter defaultCenter] removeObserver:self]; [[NSDistributedNotificationCenter defaultCenter] removeObserver:self]; [super dealloc]; NS_OBJC_END_TRY_ABORT_BLOCK; } // applicationWillTerminate: // // Notify the nsAppShell that native event processing should be discontinued. - (void)applicationWillTerminate:(NSNotification*)aNotification { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; mAppShell->WillTerminate(); NS_OBJC_END_TRY_ABORT_BLOCK; } // applicationDidBecomeActive // // Make sure gLastModifierState is updated when we become active (since we // won't have received [ChildView flagsChanged:] messages while inactive). - (void)applicationDidBecomeActive:(NSNotification*)aNotification { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; // [NSEvent modifierFlags] is valid on every kind of event, so we don't need // to worry about getting an NSInternalInconsistencyException here. NSEvent* currentEvent = [NSApp currentEvent]; if (currentEvent) { gLastModifierState = nsCocoaUtils::GetCocoaEventModifierFlags(currentEvent) & NSDeviceIndependentModifierFlagsMask; } NS_OBJC_END_TRY_ABORT_BLOCK; } // beginMenuTracking // // Roll up our context menu (if any) when some other app (or the OS) opens // any sort of menu. But make sure we don't do this for notifications we // send ourselves (whose 'sender' will be @"org.mozilla.gecko.PopupWindow"). - (void)beginMenuTracking:(NSNotification*)aNotification { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; NSString *sender = [aNotification object]; if (!sender || ![sender isEqualToString:@"org.mozilla.gecko.PopupWindow"]) { if (gRollupListener && gRollupWidget) gRollupListener->Rollup(nsnull, nsnull); } NS_OBJC_END_TRY_ABORT_BLOCK; } @end // We hook these methods in order to maintain a list of Cocoa app-modal // windows (and the "sessions" to which they correspond). We need this in // order to deal with the consequences of a Cocoa app-modal dialog being // "interrupted" by a Gecko-modal dialog. See nsCocoaAppModalWindowList:: // CurrentSession() and nsAppShell::ProcessNextNativeEvent() above. @interface NSApplication (MethodSwizzling) - (NSModalSession)nsAppShell_NSApplication_beginModalSessionForWindow:(NSWindow *)aWindow; - (void)nsAppShell_NSApplication_endModalSession:(NSModalSession)aSession; @end @implementation NSApplication (MethodSwizzling) // Called if and only if a Cocoa app-modal session is beginning. Always call // gCocoaAppModalWindowList->PushCocoa() here (if gCocoaAppModalWindowList is // non-nil). - (NSModalSession)nsAppShell_NSApplication_beginModalSessionForWindow:(NSWindow *)aWindow { NSModalSession session = [self nsAppShell_NSApplication_beginModalSessionForWindow:aWindow]; if (gCocoaAppModalWindowList) gCocoaAppModalWindowList->PushCocoa(aWindow, session); return session; } // Called to end any Cocoa modal session (app-modal or otherwise). Only call // gCocoaAppModalWindowList->PopCocoa() when an app-modal session is ending // (and when gCocoaAppModalWindowList is non-nil). - (void)nsAppShell_NSApplication_endModalSession:(NSModalSession)aSession { BOOL wasRunningAppModal = [NSApp _isRunningAppModal]; NSWindow *prevAppModalWindow = [NSApp modalWindow]; [self nsAppShell_NSApplication_endModalSession:aSession]; if (gCocoaAppModalWindowList && wasRunningAppModal && (prevAppModalWindow != [NSApp modalWindow])) gCocoaAppModalWindowList->PopCocoa(prevAppModalWindow, aSession); } @end