Mozilla/mozilla/widget/src/cocoa/nsAppShell.mm
smichaud%pobox.com 7a5f5844ca Fix several issues with popup windows in Cocoa widgets. b=387164 r=josh+cbarrett sr=pink
git-svn-id: svn://10.0.0.236/trunk@230130 18797224-902f-48f8-a5cc-f745e15eee43
2007-07-17 20:29:39 +00:00

495 lines
16 KiB
Plaintext

/* -*- 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 <mark@moxienet.com> (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 <Cocoa/Cocoa.h>
#include "nsAppShell.h"
#include "nsCOMPtr.h"
#include "nsIFile.h"
#include "nsDirectoryServiceDefs.h"
#include "nsString.h"
#include "nsIRollupListener.h"
#include "nsIWidget.h"
// defined in nsChildView.mm
extern nsIRollupListener * gRollupListener;
extern nsIWidget * gRollupWidget;
// AppShellDelegate
//
// Cocoa bridge class. An object of this class is used as an NSPort
// delegate called on the main thread when Gecko wants to interrupt
// the native run loop.
//
@interface AppShellDelegate : NSObject
{
@private
nsAppShell* mAppShell;
nsresult mRunRV;
}
- (id)initWithAppShell:(nsAppShell*)aAppShell;
- (void)handlePortMessage:(NSPortMessage*)aPortMessage;
- (void)runAppShell;
- (nsresult)rvFromRun;
- (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)
, mPort(nil)
, mDelegate(nil)
, mRunningEventLoop(PR_FALSE)
, mTerminated(PR_FALSE)
, mSkippedNativeCallback(PR_FALSE)
{
// 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];
}
nsAppShell::~nsAppShell()
{
if (mAutoreleasePools) {
NS_ASSERTION(::CFArrayGetCount(mAutoreleasePools) == 0,
"nsAppShell destroyed without popping all autorelease pools");
::CFRelease(mAutoreleasePools);
}
if (mPort) {
[[NSRunLoop currentRunLoop] removePort:mPort forMode:NSDefaultRunLoopMode];
[mPort release];
}
[mDelegate release];
[mMainPool release];
}
// Init
//
// Loads the nib (see bug 316076c21) and sets up the NSPort used to
// interrupt the main Cocoa event loop.
//
// public
nsresult
nsAppShell::Init()
{
// No event loop is running yet. 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<nsIFile> 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.
[NSBundle loadNibFile:
[NSString stringWithUTF8String:(const char*)nibPath.get()]
externalNameTable:
[NSDictionary dictionaryWithObject:[NSApplication sharedApplication]
forKey:@"NSOwner"]
withZone:NSDefaultMallocZone()];
// A message will be sent through mPort to mDelegate on the main thread
// to interrupt the run loop while it is running.
mDelegate = [[AppShellDelegate alloc] initWithAppShell:this];
NS_ENSURE_STATE(mDelegate);
mPort = [[NSPort port] retain];
NS_ENSURE_STATE(mPort);
[mPort setDelegate:mDelegate];
[[NSRunLoop currentRunLoop] addPort:mPort forMode:NSDefaultRunLoopMode];
rv = nsBaseAppShell::Init();
[localPool release];
return rv;
}
// ProcessGeckoEvents
//
// Arrange for Gecko events to be processed. They will either be processed
// after the main run loop returns (if we own the run loop) or on
// NativeEventCallback (if an embedder owns the loop).
//
// Called by -[AppShellDelegate handlePortMessage:] after mPort signals as a
// result of a ScheduleNativeEventCallback call. This method is public only
// because it needs to be called by that Objective-C fragment, and C++ can't
// make |friend|s with Objective-C.
//
// public
void
nsAppShell::ProcessGeckoEvents()
{
if (mRunningEventLoop) {
mRunningEventLoop = PR_FALSE;
// The run loop is sleeping. [NSApp nextEventMatchingMask:...] 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.
[NSApp postEvent:[NSEvent otherEventWithType:NSApplicationDefined
location:NSMakePoint(0,0)
modifierFlags:0
timestamp:0
windowNumber:-1
context:NULL
subtype:0
data1:0
data2:0]
atStart:NO];
}
if (mSuspendNativeCount <= 0) {
NativeEventCallback();
} else {
mSkippedNativeCallback = PR_TRUE;
}
[NSApp postEvent:[NSEvent otherEventWithType:NSApplicationDefined
location:NSMakePoint(0,0)
modifierFlags:0
timestamp:0
windowNumber:-1
context:NULL
subtype:0
data1:0
data2:0]
atStart:NO];
}
// WillTerminate
//
// Called by the AppShellDelegate when an NSApplicationWillTerminate
// notification is posted. After this method is called, native events should
// no longer be processed.
//
// public
void
nsAppShell::WillTerminate()
{
mTerminated = PR_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.
//
// protected virtual
void
nsAppShell::ScheduleNativeEventCallback()
{
NS_ADDREF(this);
void* self = static_cast<void*>(this);
NSData* data = [[NSData alloc] initWithBytes:&self length:sizeof(this)];
NSArray* components = [[NSArray alloc] initWithObjects:&data count:1];
// This will invoke [mDelegate handlePortMessage:message] on the main thread.
NSPortMessage* message = [[NSPortMessage alloc] initWithSendPort:mPort
receivePort:nil
components:components];
[message sendBeforeDate:[NSDate distantFuture]];
[message release];
[components release];
[data release];
}
// 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.
//
// protected virtual
PRBool
nsAppShell::ProcessNextNativeEvent(PRBool aMayWait)
{
PRBool eventProcessed = PR_FALSE;
if (mTerminated)
return eventProcessed;
PRBool wasRunningEventLoop = mRunningEventLoop;
mRunningEventLoop = aMayWait;
NSDate* waitUntil = nil;
if (aMayWait)
waitUntil = [NSDate distantFuture];
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 (NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask
untilDate:waitUntil
inMode:NSDefaultRunLoopMode
dequeue:YES]) {
[NSApp sendEvent:event];
// Additional processing that [NSApp run] does after each event.
NSEventType type = [event type];
if (type != NSPeriodic && type != NSMouseMoved) {
[[NSApp servicesMenu] update];
[[NSApp windowsMenu] update];
[[NSApp mainMenu] update];
}
[NSApp updateWindows];
eventProcessed = PR_TRUE;
}
} while (mRunningEventLoop);
mRunningEventLoop = wasRunningEventLoop;
return eventProcessed;
}
// Run
//
// Overrides the base class' Run method to ensure that [NSApp run] has been
// called. When [NSApp run] has not yet been called, this method calls it
// after arranging for a selector to be called from the run loop. That
// selector is responsible for calling Run again. At that point, because
// [NSApp run] has been called, the base class' method is called.
//
// The runAppShell selector will call [NSApp stop:] as soon as the real
// Run method finishes. The real Run method's return value is saved so
// that it may properly be returned.
//
// public
NS_IMETHODIMP
nsAppShell::Run(void)
{
if (![NSApp isRunning]) {
[mDelegate performSelector:@selector(runAppShell)
withObject:nil
afterDelay:0];
[NSApp run];
return [mDelegate rvFromRun];
}
return nsBaseAppShell::Run();
}
// 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_ASSERTION(mAutoreleasePools,
"No stack on which to store autorelease pool");
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
::CFArrayAppendValue(mAutoreleasePools, pool);
return nsBaseAppShell::OnProcessNextEvent(aThread, aMayWait, aRecursionDepth);
}
// 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)
{
CFIndex count = ::CFArrayGetCount(mAutoreleasePools);
NS_ASSERTION(mAutoreleasePools && count,
"Processed an event, but there's no autorelease pool?");
NSAutoreleasePool* pool = static_cast<const NSAutoreleasePool*>
(::CFArrayGetValueAtIndex(mAutoreleasePools,
count - 1));
::CFArrayRemoveValueAtIndex(mAutoreleasePools, count - 1);
[pool release];
return nsBaseAppShell::AfterProcessNextEvent(aThread, aRecursionDepth);
}
// AppShellDelegate implementation
@implementation AppShellDelegate
// initWithAppShell:
//
// Constructs the AppShellDelegate object
- (id)initWithAppShell:(nsAppShell*)aAppShell
{
if ((self = [self init])) {
mAppShell = aAppShell;
mRunRV = NS_ERROR_NOT_INITIALIZED;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillTerminate:)
name:NSApplicationWillTerminateNotification
object:NSApp];
[[NSDistributedNotificationCenter defaultCenter] addObserver:self
selector:@selector(beginMenuTracking:)
name:@"com.apple.HIToolbox.beginMenuTrackingNotification"
object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}
// handlePortMessage:
//
// The selector called on the delegate object when nsAppShell::mPort is sent an
// NSPortMessage by ScheduleNativeEventCallback. Call into the nsAppShell
// object for access to mRunningEventLoop and NativeEventCallback.
//
- (void)handlePortMessage:(NSPortMessage*)aPortMessage
{
NSData* data = [[aPortMessage components] objectAtIndex:0];
nsAppShell* appShell = *static_cast<nsAppShell* const*>([data bytes]);
appShell->ProcessGeckoEvents();
NS_RELEASE(appShell);
}
// runAppShell
//
// Runs the nsAppShell, and immediately stops the Cocoa run loop when
// nsAppShell::Run is done, saving its return value.
- (void)runAppShell
{
mRunRV = mAppShell->Run();
[NSApp stop:self];
return;
}
// rvFromRun
//
// Returns the nsresult return value saved by runAppShell.
- (nsresult)rvFromRun
{
return mRunRV;
}
// applicationWillTerminate:
//
// Notify the nsAppShell that native event processing should be discontinued.
- (void)applicationWillTerminate:(NSNotification*)aNotification
{
mAppShell->WillTerminate();
}
// 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
{
NSString *sender = [aNotification object];
if (!sender || ![sender isEqualToString:@"org.mozilla.gecko.PopupWindow"]) {
if (gRollupListener && gRollupWidget)
gRollupListener->Rollup();
}
}
@end