diff -r 000000000000 -r 6474c204b198 widget/cocoa/nsAppShell.mm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/widget/cocoa/nsAppShell.mm Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,1043 @@ +/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Runs the main native Cocoa run loop, interrupting it as needed to process + * Gecko events. + */ + +#import +#include + +#include "CustomCocoaEvents.h" +#include "mozilla/WidgetTraceEvent.h" +#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 "nsCocoaFeatures.h" +#include "nsCocoaUtils.h" +#include "nsChildView.h" +#include "nsToolkit.h" +#include "TextInputHandler.h" +#include "mozilla/HangMonitor.h" +#include "GeckoProfiler.h" +#include "pratom.h" + +#include "npapi.h" + +using namespace mozilla::widget; + +// defined in nsCocoaWindow.mm +extern int32_t gXULModalLevel; + +static bool gAppShellMethodsSwizzled = 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? +bool nsCocoaAppModalWindowList::GeckoModalAboveCocoaModal() +{ + if (mList.IsEmpty()) + return false; + + nsCocoaAppModalWindowListItem &topItem = mList.ElementAt(mList.Length() - 1); + + return (topItem.mWidget != nullptr); +} + +@implementation GeckoNSApplication + +- (void)sendEvent:(NSEvent *)anEvent +{ + mozilla::HangMonitor::NotifyActivity(); + if ([anEvent type] == NSApplicationDefined && + [anEvent subtype] == kEventSubtypeTrace) { + mozilla::SignalTracerThread(); + return; + } + [super sendEvent:anEvent]; +} + +- (NSEvent*)nextEventMatchingMask:(NSUInteger)mask + untilDate:(NSDate*)expiration + inMode:(NSString*)mode + dequeue:(BOOL)flag +{ + if (expiration) { + mozilla::HangMonitor::Suspend(); + } + return [super nextEventMatchingMask:mask + untilDate:expiration inMode:mode dequeue:flag]; +} + +@end + + +// 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 = false; + ScheduleNativeEventCallback(); + } + return retval; +} + +nsAppShell::nsAppShell() +: mAutoreleasePools(nullptr) +, mDelegate(nullptr) +, mCFRunLoop(NULL) +, mCFRunLoopSource(NULL) +, mRunningEventLoop(false) +, mStarted(false) +, mTerminated(false) +, mSkippedNativeCallback(false) +, mHadMoreEventsCount(0) +, mRecursionDepth(0) +, mNativeEventCallbackDepth(0) +, mNativeEventScheduledDepth(0) +{ + // A Cocoa event loop is running here if (and only if) we've been embedded + // by a Cocoa app (like Camino). + mRunningCocoaEmbedded = [NSApp isRunning] ? true : 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]; + + NS_OBJC_END_TRY_ABORT_BLOCK +} + +// An undocumented CoreGraphics framework method, present in the same form +// since at least OS X 10.5. +extern "C" CGError CGSSetDebugOptions(int options); + +// 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()). + 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(nullptr, 0, nullptr); + 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")); + + nsAutoCString 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:[GeckoNSApplication 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 __LP64__ + TextInputHandler::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:)); + // We should only replace the original terminate: method if we're not + // running in a Cocoa embedder (like Camino). See bug 604901. + if (!mRunningCocoaEmbedded) { + nsToolkit::SwizzleMethods([NSApplication class], @selector(terminate:), + @selector(nsAppShell_NSApplication_terminate:)); + } + gAppShellMethodsSwizzled = true; + } + + if (nsCocoaFeatures::OnYosemiteOrLater()) { + // Explicitly turn off CGEvent logging. This works around bug 1092855. + // If there are already CGEvents in the log, turning off logging also + // causes those events to be written to disk. But at this point no + // CGEvents have yet been processed. CGEvents are events (usually + // input events) pulled from the WindowServer. An option of 0x80000008 + // turns on CGEvent logging. + CGSSetDebugOptions(0x80000007); + } + + [localPool release]; + + 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; + PROFILER_LABEL("Events", "ProcessGeckoEvents"); + nsAppShell* self = static_cast (aInfo); + + if (self->mRunningEventLoop) { + self->mRunningEventLoop = 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:kEventSubtypeNone + data1:0 + data2:0] + atStart:NO]; + } + + if (self->mSuspendNativeCount <= 0) { + ++self->mNativeEventCallbackDepth; + self->NativeEventCallback(); + --self->mNativeEventCallbackDepth; + } else { + self->mSkippedNativeCallback = 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:kEventSubtypeNone + 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 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) { + int32_t releaseCount = 0; + if (self->mNativeEventScheduledDepth > self->mNativeEventCallbackDepth) { + releaseCount = PR_ATOMIC_SET(&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_ATOMIC_DECREMENT(&self->mNativeEventScheduledDepth) < 0) { + PR_ATOMIC_SET(&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() +{ + if (mTerminated) + return; + + // Make sure that the nsAppExitEvent posted by nsAppStartup::Quit() (called + // from [MacApplicationDelegate applicationShouldTerminate:]) gets run. + NS_ProcessPendingEvents(NS_GetCurrentThread()); + + mTerminated = true; +} + +// 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_ATOMIC_INCREMENT(&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 +bool +nsAppShell::ProcessNextNativeEvent(bool aMayWait) +{ + bool moreEvents = false; + + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + bool eventProcessed = false; + NSString* currentMode = nil; + + if (mTerminated) + return 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 false; + + bool 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 + // false we check for timer events and process them using [NSApp + // sendEvent:]. When aWait is 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 (aMayWait) { + mozilla::HangMonitor::Suspend(); + } + + // 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. + // + // Add aMayWait to minimize the number of calls to -[NSApp sendEvent:] + // made from nsAppShell::ProcessNextNativeEvent() (and indirectly from + // nsBaseAppShell::OnProcessNextEvent()), to work around bug 959281. + if ([NSApp _isRunningModal] || (aMayWait && !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(); + + mozilla::HangMonitor::NotifyActivity(); + + if (currentAppModalSession) { + [NSApp _modalSession:currentAppModalSession sendEvent:nextEvent]; + } else { + [NSApp sendEvent:nextEvent]; + } + eventProcessed = 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 = 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 true if Gecko events are currently being processed in its "main" +// event loop (or one of its "main" event loops). Returns 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().) +bool +nsAppShell::InGeckoMainEventLoop() +{ + if ((gXULModalLevel > 0) || (mRecursionDepth > 0)) + return false; + if (mNativeEventCallbackDepth <= 0) + return false; + return 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 || mTerminated) + return NS_OK; + + mStarted = 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 = true; + + delete gCocoaAppModalWindowList; + gCocoaAppModalWindowList = NULL; + +#ifndef __LP64__ + TextInputHandler::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:nullptr]; + [NSApp stop:nullptr]; + + // 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) { + int32_t releaseCount = PR_ATOMIC_SET(&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, bool aMayWait, + uint32_t 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, + uint32_t aRecursionDepth, + bool aEventWasProcessed) +{ + 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, + aEventWasProcessed); + + 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 TextInputHandler::sLastModifierState 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) { + TextInputHandler::sLastModifierState = + [currentEvent modifierFlags] & 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"]) { + nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener(); + nsCOMPtr rollupWidget = rollupListener->GetRollupWidget(); + if (rollupWidget) + rollupListener->Rollup(0, nullptr, nullptr); + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +@end + +// We hook beginModalSessionForWindow: and endModalSession: 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. +// +// We hook terminate: in order to make OS-initiated termination work nicely +// with Gecko's shutdown sequence. (Two ways to trigger OS-initiated +// termination: 1) Quit from the Dock menu; 2) Log out from (or shut down) +// your computer while the browser is active.) +@interface NSApplication (MethodSwizzling) +- (NSModalSession)nsAppShell_NSApplication_beginModalSessionForWindow:(NSWindow *)aWindow; +- (void)nsAppShell_NSApplication_endModalSession:(NSModalSession)aSession; +- (void)nsAppShell_NSApplication_terminate:(id)sender; +@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); +} + +// Called by the OS after [MacApplicationDelegate applicationShouldTerminate:] +// has returned NSTerminateNow. This method "subclasses" and replaces the +// OS's original implementation. The only thing the orginal method does which +// we need is that it posts NSApplicationWillTerminateNotification. Everything +// else is unneeded (because it's handled elsewhere), or actively interferes +// with Gecko's shutdown sequence. For example the original terminate: method +// causes the app to exit() inside [NSApp run] (called from nsAppShell::Run() +// above), which means that nothing runs after the call to nsAppStartup::Run() +// in XRE_Main(), which in particular means that ScopedXPCOMStartup's destructor +// and NS_ShutdownXPCOM() never get called. +- (void)nsAppShell_NSApplication_terminate:(id)sender +{ + [[NSNotificationCenter defaultCenter] postNotificationName:NSApplicationWillTerminateNotification + object:NSApp]; +} + +@end