diff options
Diffstat (limited to 'widget/cocoa/nsCocoaWindow.mm')
-rw-r--r-- | widget/cocoa/nsCocoaWindow.mm | 3881 |
1 files changed, 3881 insertions, 0 deletions
diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm new file mode 100644 index 0000000000..a437504fd1 --- /dev/null +++ b/widget/cocoa/nsCocoaWindow.mm @@ -0,0 +1,3881 @@ +/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "nsCocoaWindow.h" + +#include "NativeKeyBindings.h" +#include "TextInputHandler.h" +#include "nsObjCExceptions.h" +#include "nsCOMPtr.h" +#include "nsWidgetsCID.h" +#include "nsIRollupListener.h" +#include "nsChildView.h" +#include "nsWindowMap.h" +#include "nsAppShell.h" +#include "nsIAppShellService.h" +#include "nsIBaseWindow.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIXULWindow.h" +#include "nsToolkit.h" +#include "nsIDOMWindow.h" +#include "nsPIDOMWindow.h" +#include "nsIDOMElement.h" +#include "nsThreadUtils.h" +#include "nsMenuBarX.h" +#include "nsMenuUtilsX.h" +#include "nsStyleConsts.h" +#include "nsNativeThemeColors.h" +#include "nsNativeThemeCocoa.h" +#include "nsChildView.h" +#include "nsCocoaFeatures.h" +#include "nsIScreenManager.h" +#include "nsIWidgetListener.h" +#include "nsIPresShell.h" +#include "nsScreenCocoa.h" +#include "VibrancyManager.h" + +#include "gfxPlatform.h" +#include "qcms.h" + +#include "mozilla/AutoRestore.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/Preferences.h" +#include <algorithm> + +namespace mozilla { +namespace layers { +class LayerManager; +} // namespace layers +} // namespace mozilla +using namespace mozilla::layers; +using namespace mozilla::widget; +using namespace mozilla; + +int32_t gXULModalLevel = 0; + +// In principle there should be only one app-modal window at any given time. +// But sometimes, despite our best efforts, another window appears above the +// current app-modal window. So we need to keep a linked list of app-modal +// windows. (A non-sheet window that appears above an app-modal window is +// also made app-modal.) See nsCocoaWindow::SetModal(). +nsCocoaWindowList *gGeckoAppModalWindowList = NULL; + +// defined in nsMenuBarX.mm +extern NSMenu* sApplicationMenu; // Application menu shared by all menubars + +// defined in nsChildView.mm +extern BOOL gSomeMenuBarPainted; + +#if !defined(MAC_OS_X_VERSION_10_12) || \ + MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12 + +@interface NSWindow(AutomaticWindowTabbing) ++ (void)setAllowsAutomaticWindowTabbing:(BOOL)allow; +@end + +#endif + +extern "C" { + // CGSPrivate.h + typedef NSInteger CGSConnection; + typedef NSInteger CGSWindow; + typedef NSUInteger CGSWindowFilterRef; + extern CGSConnection _CGSDefaultConnection(void); + extern CGError CGSSetWindowShadowAndRimParameters(const CGSConnection cid, CGSWindow wid, float standardDeviation, float density, int offsetX, int offsetY, unsigned int flags); + extern CGError CGSSetWindowBackgroundBlurRadius(CGSConnection cid, CGSWindow wid, NSUInteger blur); +} + +#define NS_APPSHELLSERVICE_CONTRACTID "@mozilla.org/appshell/appShellService;1" + +NS_IMPL_ISUPPORTS_INHERITED(nsCocoaWindow, Inherited, nsPIWidgetCocoa) + +// A note on testing to see if your object is a sheet... +// |mWindowType == eWindowType_sheet| is true if your gecko nsIWidget is a sheet +// widget - whether or not the sheet is showing. |[mWindow isSheet]| will return +// true *only when the sheet is actually showing*. Choose your test wisely. + +static void RollUpPopups() +{ + nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener(); + NS_ENSURE_TRUE_VOID(rollupListener); + nsCOMPtr<nsIWidget> rollupWidget = rollupListener->GetRollupWidget(); + if (!rollupWidget) + return; + rollupListener->Rollup(0, true, nullptr, nullptr); +} + +nsCocoaWindow::nsCocoaWindow() +: mParent(nullptr) +, mAncestorLink(nullptr) +, mWindow(nil) +, mDelegate(nil) +, mSheetWindowParent(nil) +, mPopupContentView(nil) +, mFullscreenTransitionAnimation(nil) +, mShadowStyle(NS_STYLE_WINDOW_SHADOW_DEFAULT) +, mBackingScaleFactor(0.0) +, mAnimationType(nsIWidget::eGenericWindowAnimation) +, mWindowMadeHere(false) +, mSheetNeedsShow(false) +, mInFullScreenMode(false) +, mInFullScreenTransition(false) +, mModal(false) +, mFakeModal(false) +, mSupportsNativeFullScreen(false) +, mInNativeFullScreenMode(false) +, mIsAnimationSuppressed(false) +, mInReportMoveEvent(false) +, mInResize(false) +, mAlwaysOnTop(false) +, mNumModalDescendents(0) +{ + if ([NSWindow respondsToSelector:@selector(setAllowsAutomaticWindowTabbing:)]) { + // Disable automatic tabbing on 10.12. We need to do this before we + // orderFront any of our windows. + [NSWindow setAllowsAutomaticWindowTabbing:NO]; + } +} + +void nsCocoaWindow::DestroyNativeWindow() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mWindow) + return; + + // We want to unhook the delegate here because we don't want events + // sent to it after this object has been destroyed. + [mWindow setDelegate:nil]; + [mWindow close]; + mWindow = nil; + [mDelegate autorelease]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +nsCocoaWindow::~nsCocoaWindow() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + // Notify the children that we're gone. Popup windows (e.g. tooltips) can + // have nsChildView children. 'kid' is an nsChildView object if and only if + // its 'type' is 'eWindowType_child'. + // childView->ResetParent() can change our list of children while it's + // being iterated, so the way we iterate the list must allow for this. + for (nsIWidget* kid = mLastChild; kid;) { + nsWindowType kidType = kid->WindowType(); + if (kidType == eWindowType_child) { + nsChildView* childView = static_cast<nsChildView*>(kid); + kid = kid->GetPrevSibling(); + childView->ResetParent(); + } else { + nsCocoaWindow* childWindow = static_cast<nsCocoaWindow*>(kid); + childWindow->mParent = nullptr; + childWindow->mAncestorLink = mAncestorLink; + kid = kid->GetPrevSibling(); + } + } + + if (mWindow && mWindowMadeHere) { + DestroyNativeWindow(); + } + + NS_IF_RELEASE(mPopupContentView); + + // Deal with the possiblity that we're being destroyed while running modal. + if (mModal) { + NS_WARNING("Widget destroyed while running modal!"); + --gXULModalLevel; + NS_ASSERTION(gXULModalLevel >= 0, "Weirdness setting modality!"); + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +// Find the screen that overlaps aRect the most, +// if none are found default to the mainScreen. +static NSScreen* +FindTargetScreenForRect(const DesktopIntRect& aRect) +{ + NSScreen *targetScreen = [NSScreen mainScreen]; + NSEnumerator *screenEnum = [[NSScreen screens] objectEnumerator]; + int largestIntersectArea = 0; + while (NSScreen *screen = [screenEnum nextObject]) { + DesktopIntRect screenRect = + nsCocoaUtils::CocoaRectToGeckoRect([screen visibleFrame]); + screenRect = screenRect.Intersect(aRect); + int area = screenRect.width * screenRect.height; + if (area > largestIntersectArea) { + largestIntersectArea = area; + targetScreen = screen; + } + } + return targetScreen; +} + +// fits the rect to the screen that contains the largest area of it, +// or to aScreen if a screen is passed in +// NB: this operates with aRect in desktop pixels +static void +FitRectToVisibleAreaForScreen(DesktopIntRect& aRect, NSScreen* aScreen) +{ + if (!aScreen) { + aScreen = FindTargetScreenForRect(aRect); + } + + DesktopIntRect screenBounds = + nsCocoaUtils::CocoaRectToGeckoRect([aScreen visibleFrame]); + + if (aRect.width > screenBounds.width) { + aRect.width = screenBounds.width; + } + if (aRect.height > screenBounds.height) { + aRect.height = screenBounds.height; + } + + if (aRect.x - screenBounds.x + aRect.width > screenBounds.width) { + aRect.x += screenBounds.width - (aRect.x - screenBounds.x + aRect.width); + } + if (aRect.y - screenBounds.y + aRect.height > screenBounds.height) { + aRect.y += screenBounds.height - (aRect.y - screenBounds.y + aRect.height); + } + + // If the left/top edge of the window is off the screen in either direction, + // then set the window to start at the left/top edge of the screen. + if (aRect.x < screenBounds.x || aRect.x > (screenBounds.x + screenBounds.width)) { + aRect.x = screenBounds.x; + } + if (aRect.y < screenBounds.y || aRect.y > (screenBounds.y + screenBounds.height)) { + aRect.y = screenBounds.y; + } +} + +// Some applications use native popup windows +// (native context menus, native tooltips) +static bool UseNativePopupWindows() +{ +#ifdef MOZ_USE_NATIVE_POPUP_WINDOWS + return true; +#else + return false; +#endif /* MOZ_USE_NATIVE_POPUP_WINDOWS */ +} + +// aRect here is specified in desktop pixels +nsresult +nsCocoaWindow::Create(nsIWidget* aParent, + nsNativeWidget aNativeParent, + const DesktopIntRect& aRect, + nsWidgetInitData* aInitData) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + // Because the hidden window is created outside of an event loop, + // we have to provide an autorelease pool (see bug 559075). + nsAutoreleasePool localPool; + + DesktopIntRect newBounds = aRect; + FitRectToVisibleAreaForScreen(newBounds, nullptr); + + // Set defaults which can be overriden from aInitData in BaseCreate + mWindowType = eWindowType_toplevel; + mBorderStyle = eBorderStyle_default; + + // Ensure that the toolkit is created. + nsToolkit::GetToolkit(); + + Inherited::BaseCreate(aParent, aInitData); + + mParent = aParent; + mAncestorLink = aParent; + + // Applications that use native popups don't want us to create popup windows. + if ((mWindowType == eWindowType_popup) && UseNativePopupWindows()) + return NS_OK; + + nsresult rv = + CreateNativeWindow(nsCocoaUtils::GeckoRectToCocoaRect(newBounds), + mBorderStyle, false); + NS_ENSURE_SUCCESS(rv, rv); + + if (mWindowType == eWindowType_popup) { + if (aInitData->mMouseTransparent) { + [mWindow setIgnoresMouseEvents:YES]; + } else { + [mWindow setIgnoresMouseEvents:NO]; + } + // now we can convert newBounds to device pixels for the window we created, + // as the child view expects a rect expressed in the dev pix of its parent + LayoutDeviceIntRect devRect = + RoundedToInt(newBounds * GetDesktopToDeviceScale()); + return CreatePopupContentView(devRect, aInitData); + } + + mIsAnimationSuppressed = aInitData->mIsAnimationSuppressed; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +nsresult +nsCocoaWindow::Create(nsIWidget* aParent, + nsNativeWidget aNativeParent, + const LayoutDeviceIntRect& aRect, + nsWidgetInitData* aInitData) +{ + DesktopIntRect desktopRect = + RoundedToInt(aRect / GetDesktopToDeviceScale()); + return Create(aParent, aNativeParent, desktopRect, aInitData); +} + +static unsigned int WindowMaskForBorderStyle(nsBorderStyle aBorderStyle) +{ + bool allOrDefault = (aBorderStyle == eBorderStyle_all || + aBorderStyle == eBorderStyle_default); + + /* Apple's docs on NSWindow styles say that "a window's style mask should + * include NSTitledWindowMask if it includes any of the others [besides + * NSBorderlessWindowMask]". This implies that a borderless window + * shouldn't have any other styles than NSBorderlessWindowMask. + */ + if (!allOrDefault && !(aBorderStyle & eBorderStyle_title)) + return NSBorderlessWindowMask; + + unsigned int mask = NSTitledWindowMask; + if (allOrDefault || aBorderStyle & eBorderStyle_close) + mask |= NSClosableWindowMask; + if (allOrDefault || aBorderStyle & eBorderStyle_minimize) + mask |= NSMiniaturizableWindowMask; + if (allOrDefault || aBorderStyle & eBorderStyle_resizeh) + mask |= NSResizableWindowMask; + + return mask; +} + +// If aRectIsFrameRect, aRect specifies the frame rect of the new window. +// Otherwise, aRect.x/y specify the position of the window's frame relative to +// the bottom of the menubar and aRect.width/height specify the size of the +// content rect. +nsresult nsCocoaWindow::CreateNativeWindow(const NSRect &aRect, + nsBorderStyle aBorderStyle, + bool aRectIsFrameRect) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + // We default to NSBorderlessWindowMask, add features if needed. + unsigned int features = NSBorderlessWindowMask; + + // Configure the window we will create based on the window type. + switch (mWindowType) + { + case eWindowType_invisible: + case eWindowType_child: + case eWindowType_plugin: + break; + case eWindowType_popup: + if (aBorderStyle != eBorderStyle_default && mBorderStyle & eBorderStyle_title) { + features |= NSTitledWindowMask; + if (aBorderStyle & eBorderStyle_close) { + features |= NSClosableWindowMask; + } + } + break; + case eWindowType_toplevel: + case eWindowType_dialog: + features = WindowMaskForBorderStyle(aBorderStyle); + break; + case eWindowType_sheet: + if (mParent->WindowType() != eWindowType_invisible && + aBorderStyle & eBorderStyle_resizeh) { + features = NSResizableWindowMask; + } + else { + features = NSMiniaturizableWindowMask; + } + features |= NSTitledWindowMask; + break; + default: + NS_ERROR("Unhandled window type!"); + return NS_ERROR_FAILURE; + } + + NSRect contentRect; + + if (aRectIsFrameRect) { + contentRect = [NSWindow contentRectForFrameRect:aRect styleMask:features]; + } else { + /* + * We pass a content area rect to initialize the native Cocoa window. The + * content rect we give is the same size as the size we're given by gecko. + * The origin we're given for non-popup windows is moved down by the height + * of the menu bar so that an origin of (0,100) from gecko puts the window + * 100 pixels below the top of the available desktop area. We also move the + * origin down by the height of a title bar if it exists. This is so the + * origin that gecko gives us for the top-left of the window turns out to + * be the top-left of the window we create. This is how it was done in + * Carbon. If it ought to be different we'll probably need to look at all + * the callers. + * + * Note: This means that if you put a secondary screen on top of your main + * screen and open a window in the top screen, it'll be incorrectly shifted + * down by the height of the menu bar. Same thing would happen in Carbon. + * + * Note: If you pass a rect with 0,0 for an origin, the window ends up in a + * weird place for some reason. This stops that without breaking popups. + */ + // Compensate for difference between frame and content area height (e.g. title bar). + NSRect newWindowFrame = [NSWindow frameRectForContentRect:aRect styleMask:features]; + + contentRect = aRect; + contentRect.origin.y -= (newWindowFrame.size.height - aRect.size.height); + + if (mWindowType != eWindowType_popup) + contentRect.origin.y -= [[NSApp mainMenu] menuBarHeight]; + } + + // NSLog(@"Top-level window being created at Cocoa rect: %f, %f, %f, %f\n", + // rect.origin.x, rect.origin.y, rect.size.width, rect.size.height); + + Class windowClass = [BaseWindow class]; + // If we have a titlebar on a top-level window, we want to be able to control the + // titlebar color (for unified windows), so use the special ToolbarWindow class. + // Note that we need to check the window type because we mark sheets as + // having titlebars. + if ((mWindowType == eWindowType_toplevel || mWindowType == eWindowType_dialog) && + (features & NSTitledWindowMask)) + windowClass = [ToolbarWindow class]; + // If we're a popup window we need to use the PopupWindow class. + else if (mWindowType == eWindowType_popup) + windowClass = [PopupWindow class]; + // If we're a non-popup borderless window we need to use the + // BorderlessWindow class. + else if (features == NSBorderlessWindowMask) + windowClass = [BorderlessWindow class]; + + // Create the window + mWindow = [[windowClass alloc] initWithContentRect:contentRect styleMask:features + backing:NSBackingStoreBuffered defer:YES]; + + // Make sure that window titles don't leak to disk in private browsing mode + // due to macOS' resume feature. + [mWindow setRestorable:NO]; + [mWindow disableSnapshotRestoration]; + + // setup our notification delegate. Note that setDelegate: does NOT retain. + mDelegate = [[WindowDelegate alloc] initWithGeckoWindow:this]; + [mWindow setDelegate:mDelegate]; + + // Make sure that the content rect we gave has been honored. + NSRect wantedFrame = [mWindow frameRectForContentRect:contentRect]; + if (!NSEqualRects([mWindow frame], wantedFrame)) { + // This can happen when the window is not on the primary screen. + [mWindow setFrame:wantedFrame display:NO]; + } + UpdateBounds(); + + if (mWindowType == eWindowType_invisible) { + [mWindow setLevel:kCGDesktopWindowLevelKey]; + } + + if (mWindowType == eWindowType_popup) { + SetPopupWindowLevel(); + [mWindow setBackgroundColor:[NSColor clearColor]]; + [mWindow setOpaque:NO]; + } else { + // Make sure that regular windows are opaque from the start, so that + // nsChildView::WidgetTypeSupportsAcceleration returns true for them. + [mWindow setOpaque:YES]; + } + + NSWindowCollectionBehavior newBehavior = [mWindow collectionBehavior]; + if (mAlwaysOnTop) { + [mWindow setLevel:NSFloatingWindowLevel]; + newBehavior |= NSWindowCollectionBehaviorCanJoinAllSpaces; + } + [mWindow setCollectionBehavior:newBehavior]; + + [mWindow setContentMinSize:NSMakeSize(60, 60)]; + [mWindow disableCursorRects]; + + // Make sure the window starts out not draggable by the background. + // We will turn it on as necessary. + [mWindow setMovableByWindowBackground:NO]; + + [[WindowDataMap sharedWindowDataMap] ensureDataForWindow:mWindow]; + mWindowMadeHere = true; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsCocoaWindow::CreatePopupContentView(const LayoutDeviceIntRect &aRect, + nsWidgetInitData* aInitData) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + // We need to make our content view a ChildView. + mPopupContentView = new nsChildView(); + if (!mPopupContentView) + return NS_ERROR_FAILURE; + + NS_ADDREF(mPopupContentView); + + nsIWidget* thisAsWidget = static_cast<nsIWidget*>(this); + nsresult rv = mPopupContentView->Create(thisAsWidget, nullptr, aRect, + aInitData); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + NSView* contentView = [mWindow contentView]; + ChildView* childView = (ChildView*)mPopupContentView->GetNativeData(NS_NATIVE_WIDGET); + [childView setFrame:NSZeroRect]; + [childView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [contentView addSubview:childView]; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +void nsCocoaWindow::Destroy() +{ + if (mOnDestroyCalled) + return; + mOnDestroyCalled = true; + + // SetFakeModal(true) is called for non-modal window opened by modal window. + // On Cocoa, it needs corresponding SetFakeModal(false) on destroy to restore + // ancestor windows' state. + if (mFakeModal) { + SetFakeModal(false); + } + + // If we don't hide here we run into problems with panels, this is not ideal. + // (Bug 891424) + Show(false); + + if (mPopupContentView) + mPopupContentView->Destroy(); + + if (mFullscreenTransitionAnimation) { + [mFullscreenTransitionAnimation stopAnimation]; + ReleaseFullscreenTransitionAnimation(); + } + + nsBaseWidget::Destroy(); + // nsBaseWidget::Destroy() calls GetParent()->RemoveChild(this). But we + // don't implement GetParent(), so we need to do the equivalent here. + if (mParent) { + mParent->RemoveChild(this); + } + nsBaseWidget::OnDestroy(); + + if (mInFullScreenMode) { + // On Lion we don't have to mess with the OS chrome when in Full Screen + // mode. But we do have to destroy the native window here (and not wait + // for that to happen in our destructor). We don't switch away from the + // native window's space until the window is destroyed, and otherwise this + // might not happen for several seconds (because at least one object + // holding a reference to ourselves is usually waiting to be garbage- + // collected). See bug 757618. + if (mInNativeFullScreenMode) { + DestroyNativeWindow(); + } else if (mWindow) { + nsCocoaUtils::HideOSChromeOnScreen(false); + } + } +} + +nsIWidget* nsCocoaWindow::GetSheetWindowParent(void) +{ + if (mWindowType != eWindowType_sheet) + return nullptr; + nsCocoaWindow *parent = static_cast<nsCocoaWindow*>(mParent); + while (parent && (parent->mWindowType == eWindowType_sheet)) + parent = static_cast<nsCocoaWindow*>(parent->mParent); + return parent; +} + +void* nsCocoaWindow::GetNativeData(uint32_t aDataType) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSNULL; + + void* retVal = nullptr; + + switch (aDataType) { + // to emulate how windows works, we always have to return a NSView + // for NS_NATIVE_WIDGET + case NS_NATIVE_WIDGET: + case NS_NATIVE_DISPLAY: + retVal = [mWindow contentView]; + break; + + case NS_NATIVE_WINDOW: + retVal = mWindow; + break; + + case NS_NATIVE_GRAPHIC: + // There isn't anything that makes sense to return here, + // and it doesn't matter so just return nullptr. + NS_ERROR("Requesting NS_NATIVE_GRAPHIC on a top-level window!"); + break; + case NS_RAW_NATIVE_IME_CONTEXT: { + retVal = GetPseudoIMEContext(); + if (retVal) { + break; + } + NSView* view = mWindow ? [mWindow contentView] : nil; + if (view) { + retVal = [view inputContext]; + } + // If inputContext isn't available on this window, return this window's + // pointer instead of nullptr since if this returns nullptr, + // IMEStateManager cannot manage composition with TextComposition + // instance. Although, this case shouldn't occur. + if (NS_WARN_IF(!retVal)) { + retVal = this; + } + break; + } + } + + return retVal; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSNULL; +} + +bool nsCocoaWindow::IsVisible() const +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + return (mWindow && ([mWindow isVisibleOrBeingShown] || mSheetNeedsShow)); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(false); +} + +void +nsCocoaWindow::SetModal(bool aState) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mWindow) + return; + + // This is used during startup (outside the event loop) when creating + // the add-ons compatibility checking dialog and the profile manager UI; + // therefore, it needs to provide an autorelease pool to avoid cocoa + // objects leaking. + nsAutoreleasePool localPool; + + mModal = aState; + nsCocoaWindow *ancestor = static_cast<nsCocoaWindow*>(mAncestorLink); + if (aState) { + ++gXULModalLevel; + // When a non-sheet window gets "set modal", make the window(s) that it + // appears over behave as they should. We can't rely on native methods to + // do this, for the following reason: The OS runs modal non-sheet windows + // in an event loop (using [NSApplication runModalForWindow:] or similar + // methods) that's incompatible with the modal event loop in nsXULWindow:: + // ShowModal() (each of these event loops is "exclusive", and can't run at + // the same time as other (similar) event loops). + if (mWindowType != eWindowType_sheet) { + while (ancestor) { + if (ancestor->mNumModalDescendents++ == 0) { + NSWindow *aWindow = ancestor->GetCocoaWindow(); + if (ancestor->mWindowType != eWindowType_invisible) { + [[aWindow standardWindowButton:NSWindowCloseButton] setEnabled:NO]; + [[aWindow standardWindowButton:NSWindowMiniaturizeButton] setEnabled:NO]; + [[aWindow standardWindowButton:NSWindowZoomButton] setEnabled:NO]; + } + } + ancestor = static_cast<nsCocoaWindow*>(ancestor->mParent); + } + [mWindow setLevel:NSModalPanelWindowLevel]; + nsCocoaWindowList *windowList = new nsCocoaWindowList; + if (windowList) { + windowList->window = this; // Don't ADDREF + windowList->prev = gGeckoAppModalWindowList; + gGeckoAppModalWindowList = windowList; + } + } + } + else { + --gXULModalLevel; + NS_ASSERTION(gXULModalLevel >= 0, "Mismatched call to nsCocoaWindow::SetModal(false)!"); + if (mWindowType != eWindowType_sheet) { + while (ancestor) { + if (--ancestor->mNumModalDescendents == 0) { + NSWindow *aWindow = ancestor->GetCocoaWindow(); + if (ancestor->mWindowType != eWindowType_invisible) { + [[aWindow standardWindowButton:NSWindowCloseButton] setEnabled:YES]; + [[aWindow standardWindowButton:NSWindowMiniaturizeButton] setEnabled:YES]; + [[aWindow standardWindowButton:NSWindowZoomButton] setEnabled:YES]; + } + } + NS_ASSERTION(ancestor->mNumModalDescendents >= 0, "Widget hierarchy changed while modal!"); + ancestor = static_cast<nsCocoaWindow*>(ancestor->mParent); + } + if (gGeckoAppModalWindowList) { + NS_ASSERTION(gGeckoAppModalWindowList->window == this, "Widget hierarchy changed while modal!"); + nsCocoaWindowList *saved = gGeckoAppModalWindowList; + gGeckoAppModalWindowList = gGeckoAppModalWindowList->prev; + delete saved; // "window" not ADDREFed + } + if (mWindowType == eWindowType_popup) + SetPopupWindowLevel(); + else + [mWindow setLevel:NSNormalWindowLevel]; + } + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +nsCocoaWindow::SetFakeModal(bool aState) +{ + mFakeModal = aState; + SetModal(aState); +} + +bool +nsCocoaWindow::IsRunningAppModal() +{ + return [NSApp _isRunningAppModal]; +} + +// Hide or show this window +NS_IMETHODIMP nsCocoaWindow::Show(bool bState) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (!mWindow) + return NS_OK; + + // We need to re-execute sometimes in order to bring already-visible + // windows forward. + if (!mSheetNeedsShow && !bState && ![mWindow isVisible]) + return NS_OK; + + // Protect against re-entering. + if (bState && [mWindow isBeingShown]) + return NS_OK; + + [mWindow setBeingShown:bState]; + + nsIWidget* parentWidget = mParent; + nsCOMPtr<nsPIWidgetCocoa> piParentWidget(do_QueryInterface(parentWidget)); + NSWindow* nativeParentWindow = (parentWidget) ? + (NSWindow*)parentWidget->GetNativeData(NS_NATIVE_WINDOW) : nil; + + if (bState && !mBounds.IsEmpty()) { + // Don't try to show a popup when the parent isn't visible or is minimized. + if (mWindowType == eWindowType_popup && nativeParentWindow) { + if (![nativeParentWindow isVisible] || [nativeParentWindow isMiniaturized]) { + return NS_ERROR_FAILURE; + } + } + + if (mPopupContentView) { + // Ensure our content view is visible. We never need to hide it. + mPopupContentView->Show(true); + } + + if (mWindowType == eWindowType_sheet) { + // bail if no parent window (its basically what we do in Carbon) + if (!nativeParentWindow || !piParentWidget) + return NS_ERROR_FAILURE; + + NSWindow* topNonSheetWindow = nativeParentWindow; + + // If this sheet is the child of another sheet, hide the parent so that + // this sheet can be displayed. Leave the parent mSheetNeedsShow alone, + // that is only used to handle sibling sheet contention. The parent will + // return once there are no more child sheets. + bool parentIsSheet = false; + if (NS_SUCCEEDED(piParentWidget->GetIsSheet(&parentIsSheet)) && + parentIsSheet) { + piParentWidget->GetSheetWindowParent(&topNonSheetWindow); + [NSApp endSheet:nativeParentWindow]; + } + + nsCOMPtr<nsIWidget> sheetShown; + if (NS_SUCCEEDED(piParentWidget->GetChildSheet( + true, getter_AddRefs(sheetShown))) && + (!sheetShown || sheetShown == this)) { + // If this sheet is already the sheet actually being shown, don't + // tell it to show again. Otherwise the number of calls to + // [NSApp beginSheet...] won't match up with [NSApp endSheet...]. + if (![mWindow isVisible]) { + mSheetNeedsShow = false; + mSheetWindowParent = topNonSheetWindow; + // Only set contextInfo if our parent isn't a sheet. + NSWindow* contextInfo = parentIsSheet ? nil : mSheetWindowParent; + [TopLevelWindowData deactivateInWindow:mSheetWindowParent]; + [NSApp beginSheet:mWindow + modalForWindow:mSheetWindowParent + modalDelegate:mDelegate + didEndSelector:@selector(didEndSheet:returnCode:contextInfo:) + contextInfo:contextInfo]; + [TopLevelWindowData activateInWindow:mWindow]; + SendSetZLevelEvent(); + } + } + else { + // A sibling of this sheet is active, don't show this sheet yet. + // When the active sheet hides, its brothers and sisters that have + // mSheetNeedsShow set will have their opportunities to display. + mSheetNeedsShow = true; + } + } + else if (mWindowType == eWindowType_popup) { + if (!nsCocoaFeatures::OnMojaveOrLater()) { + // If a popup window is shown after being hidden, it needs to be "reset" + // for it to receive any mouse events aside from mouse-moved events + // (because it was removed from the "window cache" when it was hidden + // -- see below). Setting the window number to -1 and then back to its + // original value seems to accomplish this. The idea was "borrowed" + // from the Java Embedding Plugin. This is fixed on macOS 10.14+. + NSInteger windowNumber = [mWindow windowNumber]; + [mWindow _setWindowNumber:-1]; + [mWindow _setWindowNumber:windowNumber]; + } + // For reasons that aren't yet clear, calls to [NSWindow orderFront:] or + // [NSWindow makeKeyAndOrderFront:] can sometimes trigger "Error (1000) + // creating CGSWindow", which in turn triggers an internal inconsistency + // NSException. These errors shouldn't be fatal. So we need to wrap + // calls to ...orderFront: in TRY blocks. See bmo bug 470864. + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + [[mWindow contentView] setNeedsDisplay:YES]; + [mWindow orderFront:nil]; + NS_OBJC_END_TRY_ABORT_BLOCK; + SendSetZLevelEvent(); + AdjustWindowShadow(); + SetWindowBackgroundBlur(); + // If our popup window is a non-native context menu, tell the OS (and + // other programs) that a menu has opened. This is how the OS knows to + // close other programs' context menus when ours open. + if ([mWindow isKindOfClass:[PopupWindow class]] && + [(PopupWindow*) mWindow isContextMenu]) { + [[NSDistributedNotificationCenter defaultCenter] + postNotificationName:@"com.apple.HIToolbox.beginMenuTrackingNotification" + object:@"org.mozilla.gecko.PopupWindow"]; + } + + // If a parent window was supplied and this is a popup at the parent + // level, set its child window. This will cause the child window to + // appear above the parent and move when the parent does. Setting this + // needs to happen after the _setWindowNumber calls above, otherwise the + // window doesn't focus properly. + if (nativeParentWindow && mPopupLevel == ePopupLevelParent) + [nativeParentWindow addChildWindow:mWindow + ordered:NSWindowAbove]; + } + else { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + if (mWindowType == eWindowType_toplevel && + [mWindow respondsToSelector:@selector(setAnimationBehavior:)]) { + NSWindowAnimationBehavior behavior; + if (mIsAnimationSuppressed) { + behavior = NSWindowAnimationBehaviorNone; + } else { + switch (mAnimationType) { + case nsIWidget::eDocumentWindowAnimation: + behavior = NSWindowAnimationBehaviorDocumentWindow; + break; + default: + NS_NOTREACHED("unexpected mAnimationType value"); + // fall through + case nsIWidget::eGenericWindowAnimation: + behavior = NSWindowAnimationBehaviorDefault; + break; + } + } + [mWindow setAnimationBehavior:behavior]; + } + [mWindow makeKeyAndOrderFront:nil]; + NS_OBJC_END_TRY_ABORT_BLOCK; + SendSetZLevelEvent(); + } + } + else { + // roll up any popups if a top-level window is going away + if (mWindowType == eWindowType_toplevel || mWindowType == eWindowType_dialog) + RollUpPopups(); + + // now get rid of the window/sheet + if (mWindowType == eWindowType_sheet) { + if (mSheetNeedsShow) { + // This is an attempt to hide a sheet that never had a chance to + // be shown. There's nothing to do other than make sure that it + // won't show. + mSheetNeedsShow = false; + } + else { + // get sheet's parent *before* hiding the sheet (which breaks the linkage) + NSWindow* sheetParent = mSheetWindowParent; + + // hide the sheet + [NSApp endSheet:mWindow]; + + [TopLevelWindowData deactivateInWindow:mWindow]; + + nsCOMPtr<nsIWidget> siblingSheetToShow; + bool parentIsSheet = false; + + if (nativeParentWindow && piParentWidget && + NS_SUCCEEDED(piParentWidget->GetChildSheet( + false, getter_AddRefs(siblingSheetToShow))) && + siblingSheetToShow) { + // First, give sibling sheets an opportunity to show. + siblingSheetToShow->Show(true); + } + else if (nativeParentWindow && piParentWidget && + NS_SUCCEEDED(piParentWidget->GetIsSheet(&parentIsSheet)) && + parentIsSheet) { + // Only set contextInfo if the parent of the parent sheet we're about + // to restore isn't itself a sheet. + NSWindow* contextInfo = sheetParent; + nsIWidget* grandparentWidget = nil; + if (NS_SUCCEEDED(piParentWidget->GetRealParent(&grandparentWidget)) && grandparentWidget) { + nsCOMPtr<nsPIWidgetCocoa> piGrandparentWidget(do_QueryInterface(grandparentWidget)); + bool grandparentIsSheet = false; + if (piGrandparentWidget && NS_SUCCEEDED(piGrandparentWidget->GetIsSheet(&grandparentIsSheet)) && + grandparentIsSheet) { + contextInfo = nil; + } + } + // If there are no sibling sheets, but the parent is a sheet, restore + // it. It wasn't sent any deactivate events when it was hidden, so + // don't call through Show, just let the OS put it back up. + [NSApp beginSheet:nativeParentWindow + modalForWindow:sheetParent + modalDelegate:[nativeParentWindow delegate] + didEndSelector:@selector(didEndSheet:returnCode:contextInfo:) + contextInfo:contextInfo]; + } + else { + // Sheet, that was hard. No more siblings or parents, going back + // to a real window. + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + [sheetParent makeKeyAndOrderFront:nil]; + NS_OBJC_END_TRY_ABORT_BLOCK; + } + SendSetZLevelEvent(); + } + } + else { + // If the window is a popup window with a parent window we need to + // unhook it here before ordering it out. When you order out the child + // of a window it hides the parent window. + if (mWindowType == eWindowType_popup && nativeParentWindow) + [nativeParentWindow removeChildWindow:mWindow]; + + [mWindow orderOut:nil]; + + if (!nsCocoaFeatures::OnMojaveOrLater()) { + // Unless it's explicitly removed from NSApp's "window cache", a popup + // window will keep receiving mouse-moved events even after it's been + // "ordered out" (instead of the browser window that was underneath it, + // until you click on that window). This is bmo bug 378645, but it's + // surely an Apple bug. The "window cache" is an undocumented + // subsystem, all of whose methods are included in the NSWindowCache + // category of the NSApplication class (in header files generated using + // class-dump). This workaround was "borrowed" from the Java Embedding + // Plugin (which uses it for a different purpose). This is fixed on + // macOS 10.14+. + if (mWindowType == eWindowType_popup) { + [NSApp _removeWindowFromCache:mWindow]; + } + } + + // If our popup window is a non-native context menu, tell the OS (and + // other programs) that a menu has closed. + if ([mWindow isKindOfClass:[PopupWindow class]] && + [(PopupWindow*) mWindow isContextMenu]) { + [[NSDistributedNotificationCenter defaultCenter] + postNotificationName:@"com.apple.HIToolbox.endMenuTrackingNotification" + object:@"org.mozilla.gecko.PopupWindow"]; + } + } + } + + [mWindow setBeingShown:NO]; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +struct ShadowParams { + float standardDeviation; + float density; + int offsetX; + int offsetY; + unsigned int flags; +}; + +// These numbers have been determined by looking at the results of +// CGSGetWindowShadowAndRimParameters for native window types. +static const ShadowParams kWindowShadowParametersPreYosemite[] = { + { 0.0f, 0.0f, 0, 0, 0 }, // none + { 8.0f, 0.5f, 0, 6, 1 }, // default + { 10.0f, 0.44f, 0, 10, 512 }, // menu + { 8.0f, 0.5f, 0, 6, 1 }, // tooltip + { 4.0f, 0.6f, 0, 4, 512 } // sheet +}; + +static const ShadowParams kWindowShadowParametersPostYosemite[] = { + { 0.0f, 0.0f, 0, 0, 0 }, // none + { 8.0f, 0.5f, 0, 6, 1 }, // default + { 9.882353f, 0.3f, 0, 4, 0 }, // menu + { 3.294118f, 0.2f, 0, 1, 0 }, // tooltip + { 9.882353f, 0.3f, 0, 4, 0 } // sheet +}; + +// This method will adjust the window shadow style for popup windows after +// they have been made visible. Before they're visible, their window number +// might be -1, which is not useful. +// We won't attempt to change the shadow for windows that can acquire key state +// since OS X will reset the shadow whenever that happens. +void +nsCocoaWindow::AdjustWindowShadow() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mWindow || ![mWindow isVisible] || ![mWindow hasShadow] || + [mWindow canBecomeKeyWindow] || [mWindow windowNumber] == -1) + return; + + const ShadowParams& params = nsCocoaFeatures::OnYosemiteOrLater() + ? kWindowShadowParametersPostYosemite[mShadowStyle] + : kWindowShadowParametersPreYosemite[mShadowStyle]; + CGSConnection cid = _CGSDefaultConnection(); + CGSSetWindowShadowAndRimParameters(cid, [mWindow windowNumber], + params.standardDeviation, params.density, + params.offsetX, params.offsetY, + params.flags); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +static const NSUInteger kWindowBackgroundBlurRadius = 4; + +void +nsCocoaWindow::SetWindowBackgroundBlur() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mWindow || ![mWindow isVisible] || [mWindow windowNumber] == -1) + return; + + // Only blur the background of menus and fake sheets. + if (mShadowStyle != NS_STYLE_WINDOW_SHADOW_MENU && + mShadowStyle != NS_STYLE_WINDOW_SHADOW_SHEET) + return; + + CGSConnection cid = _CGSDefaultConnection(); + CGSSetWindowBackgroundBlurRadius(cid, [mWindow windowNumber], kWindowBackgroundBlurRadius); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +nsresult +nsCocoaWindow::ConfigureChildren(const nsTArray<Configuration>& aConfigurations) +{ + if (mPopupContentView) { + mPopupContentView->ConfigureChildren(aConfigurations); + } + return NS_OK; +} + +LayerManager* +nsCocoaWindow::GetLayerManager(PLayerTransactionChild* aShadowManager, + LayersBackend aBackendHint, + LayerManagerPersistence aPersistence) +{ + if (mPopupContentView) { + return mPopupContentView->GetLayerManager(aShadowManager, + aBackendHint, + aPersistence); + } + return nullptr; +} + +nsTransparencyMode nsCocoaWindow::GetTransparencyMode() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + return (!mWindow || [mWindow isOpaque]) ? eTransparencyOpaque : eTransparencyTransparent; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(eTransparencyOpaque); +} + +// This is called from nsMenuPopupFrame when making a popup transparent, or +// from nsChildView::SetTransparencyMode for other window types. +void nsCocoaWindow::SetTransparencyMode(nsTransparencyMode aMode) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mWindow) + return; + + // Transparent windows are only supported on popups. + BOOL isTransparent = aMode == eTransparencyTransparent && + mWindowType == eWindowType_popup; + BOOL currentTransparency = ![mWindow isOpaque]; + if (isTransparent != currentTransparency) { + [mWindow setOpaque:!isTransparent]; + [mWindow setBackgroundColor:(isTransparent ? [NSColor clearColor] : [NSColor whiteColor])]; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +NS_IMETHODIMP nsCocoaWindow::Enable(bool aState) +{ + return NS_OK; +} + +bool nsCocoaWindow::IsEnabled() const +{ + return true; +} + +#define kWindowPositionSlop 20 + +void +nsCocoaWindow::ConstrainPosition(bool aAllowSlop, int32_t *aX, int32_t *aY) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mWindow || ![mWindow screen]) { + return; + } + + nsIntRect screenBounds; + + int32_t width, height; + + NSRect frame = [mWindow frame]; + + // zero size rects confuse the screen manager + width = std::max<int32_t>(frame.size.width, 1); + height = std::max<int32_t>(frame.size.height, 1); + + nsCOMPtr<nsIScreenManager> screenMgr = do_GetService("@mozilla.org/gfx/screenmanager;1"); + if (screenMgr) { + nsCOMPtr<nsIScreen> screen; + screenMgr->ScreenForRect(*aX, *aY, width, height, getter_AddRefs(screen)); + + if (screen) { + screen->GetRectDisplayPix(&(screenBounds.x), &(screenBounds.y), + &(screenBounds.width), &(screenBounds.height)); + } + } + + if (aAllowSlop) { + if (*aX < screenBounds.x - width + kWindowPositionSlop) { + *aX = screenBounds.x - width + kWindowPositionSlop; + } else if (*aX >= screenBounds.x + screenBounds.width - kWindowPositionSlop) { + *aX = screenBounds.x + screenBounds.width - kWindowPositionSlop; + } + + if (*aY < screenBounds.y - height + kWindowPositionSlop) { + *aY = screenBounds.y - height + kWindowPositionSlop; + } else if (*aY >= screenBounds.y + screenBounds.height - kWindowPositionSlop) { + *aY = screenBounds.y + screenBounds.height - kWindowPositionSlop; + } + } else { + if (*aX < screenBounds.x) { + *aX = screenBounds.x; + } else if (*aX >= screenBounds.x + screenBounds.width - width) { + *aX = screenBounds.x + screenBounds.width - width; + } + + if (*aY < screenBounds.y) { + *aY = screenBounds.y; + } else if (*aY >= screenBounds.y + screenBounds.height - height) { + *aY = screenBounds.y + screenBounds.height - height; + } + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void nsCocoaWindow::SetSizeConstraints(const SizeConstraints& aConstraints) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + // Popups can be smaller than (60, 60) + NSRect rect = + (mWindowType == eWindowType_popup) ? NSZeroRect : NSMakeRect(0.0, 0.0, 60, 60); + rect = [mWindow frameRectForContentRect:rect]; + + CGFloat scaleFactor = BackingScaleFactor(); + + SizeConstraints c = aConstraints; + c.mMinSize.width = + std::max(nsCocoaUtils::CocoaPointsToDevPixels(rect.size.width, scaleFactor), + c.mMinSize.width); + c.mMinSize.height = + std::max(nsCocoaUtils::CocoaPointsToDevPixels(rect.size.height, scaleFactor), + c.mMinSize.height); + + NSSize minSize = { + nsCocoaUtils::DevPixelsToCocoaPoints(c.mMinSize.width, scaleFactor), + nsCocoaUtils::DevPixelsToCocoaPoints(c.mMinSize.height, scaleFactor) + }; + [mWindow setMinSize:minSize]; + + NSSize maxSize = { + c.mMaxSize.width == NS_MAXSIZE ? + FLT_MAX : nsCocoaUtils::DevPixelsToCocoaPoints(c.mMaxSize.width, scaleFactor), + c.mMaxSize.height == NS_MAXSIZE ? + FLT_MAX : nsCocoaUtils::DevPixelsToCocoaPoints(c.mMaxSize.height, scaleFactor) + }; + [mWindow setMaxSize:maxSize]; + + nsBaseWidget::SetSizeConstraints(c); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +// Coordinates are desktop pixels +NS_IMETHODIMP nsCocoaWindow::Move(double aX, double aY) +{ + if (!mWindow) { + return NS_OK; + } + + // The point we have is in Gecko coordinates (origin top-left). Convert + // it to Cocoa ones (origin bottom-left). + NSPoint coord = { + static_cast<float>(aX), + static_cast<float>(nsCocoaUtils::FlippedScreenY(NSToIntRound(aY))) + }; + + NSRect frame = [mWindow frame]; + if (frame.origin.x != coord.x || + frame.origin.y + frame.size.height != coord.y) { + [mWindow setFrameTopLeftPoint:coord]; + } + + return NS_OK; +} + +void +nsCocoaWindow::SetSizeMode(nsSizeMode aMode) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mWindow) + return; + + // mSizeMode will be updated in DispatchSizeModeEvent, which will be called + // from a delegate method that handles the state change during one of the + // calls below. + nsSizeMode previousMode = mSizeMode; + + if (aMode == nsSizeMode_Normal) { + if ([mWindow isMiniaturized]) + [mWindow deminiaturize:nil]; + else if (previousMode == nsSizeMode_Maximized && [mWindow isZoomed]) + [mWindow zoom:nil]; + } + else if (aMode == nsSizeMode_Minimized) { + if (![mWindow isMiniaturized]) + [mWindow miniaturize:nil]; + } + else if (aMode == nsSizeMode_Maximized) { + if ([mWindow isMiniaturized]) + [mWindow deminiaturize:nil]; + if (![mWindow isZoomed]) + [mWindow zoom:nil]; + } + else if (aMode == nsSizeMode_Fullscreen) { + if (!mInFullScreenMode) + MakeFullScreen(true); + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +// This has to preserve the window's frame bounds. +// This method requires (as does the Windows impl.) that you call Resize shortly +// after calling HideWindowChrome. See bug 498835 for fixing this. +NS_IMETHODIMP nsCocoaWindow::HideWindowChrome(bool aShouldHide) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (!mWindow || !mWindowMadeHere || + (mWindowType != eWindowType_toplevel && mWindowType != eWindowType_dialog)) + return NS_ERROR_FAILURE; + + BOOL isVisible = [mWindow isVisible]; + + // Remove child windows. + NSArray* childWindows = [mWindow childWindows]; + NSEnumerator* enumerator = [childWindows objectEnumerator]; + NSWindow* child = nil; + while ((child = [enumerator nextObject])) { + [mWindow removeChildWindow:child]; + } + + // Remove the content view. + NSView* contentView = [mWindow contentView]; + [contentView retain]; + [contentView removeFromSuperviewWithoutNeedingDisplay]; + + // Save state (like window title). + NSMutableDictionary* state = [mWindow exportState]; + + // Recreate the window with the right border style. + NSRect frameRect = [mWindow frame]; + DestroyNativeWindow(); + nsresult rv = CreateNativeWindow(frameRect, aShouldHide ? eBorderStyle_none : mBorderStyle, true); + NS_ENSURE_SUCCESS(rv, rv); + + // Re-import state. + [mWindow importState:state]; + + // Reparent the content view. + [mWindow setContentView:contentView]; + [contentView release]; + + // Reparent child windows. + enumerator = [childWindows objectEnumerator]; + while ((child = [enumerator nextObject])) { + [mWindow addChildWindow:child ordered:NSWindowAbove]; + } + + // Show the new window. + if (isVisible) { + bool wasAnimationSuppressed = mIsAnimationSuppressed; + mIsAnimationSuppressed = true; + rv = Show(true); + mIsAnimationSuppressed = wasAnimationSuppressed; + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +class FullscreenTransitionData : public nsISupports +{ +public: + NS_DECL_ISUPPORTS + + explicit FullscreenTransitionData(NSWindow* aWindow) + : mTransitionWindow(aWindow) { } + + NSWindow* mTransitionWindow; + +private: + virtual ~FullscreenTransitionData() + { + [mTransitionWindow close]; + } +}; + +NS_IMPL_ISUPPORTS0(FullscreenTransitionData) + +@interface FullscreenTransitionDelegate : NSObject <NSAnimationDelegate> +{ +@public + nsCocoaWindow* mWindow; + nsIRunnable* mCallback; +} +@end + +@implementation FullscreenTransitionDelegate +- (void)cleanupAndDispatch:(NSAnimation* )animation +{ + [animation setDelegate:nil]; + [self autorelease]; + // The caller should have added ref for us. + NS_DispatchToMainThread(already_AddRefed<nsIRunnable>(mCallback)); +} + +- (void)animationDidEnd:(NSAnimation *)animation +{ + MOZ_ASSERT(animation == mWindow->FullscreenTransitionAnimation(), + "Should be handling the only animation on the window"); + mWindow->ReleaseFullscreenTransitionAnimation(); + [self cleanupAndDispatch:animation]; +} + +- (void)animationDidStop:(NSAnimation *)animation +{ + [self cleanupAndDispatch:animation]; +} +@end + +/* virtual */ bool +nsCocoaWindow::PrepareForFullscreenTransition(nsISupports** aData) +{ + nsCOMPtr<nsIScreen> widgetScreen = GetWidgetScreen(); + nsScreenCocoa* screen = static_cast<nsScreenCocoa*>(widgetScreen.get()); + NSScreen* cocoaScreen = screen->CocoaScreen(); + + NSWindow* win = + [[NSWindow alloc] initWithContentRect:[cocoaScreen frame] + styleMask:NSBorderlessWindowMask + backing:NSBackingStoreBuffered + defer:YES]; + [win setBackgroundColor:[NSColor blackColor]]; + [win setAlphaValue:0]; + [win setIgnoresMouseEvents:YES]; + [win setLevel:NSScreenSaverWindowLevel]; + [win makeKeyAndOrderFront:nil]; + + auto data = new FullscreenTransitionData(win); + *aData = data; + NS_ADDREF(data); + return true; +} + +/* virtual */ void +nsCocoaWindow::PerformFullscreenTransition(FullscreenTransitionStage aStage, + uint16_t aDuration, + nsISupports* aData, + nsIRunnable* aCallback) +{ + auto data = static_cast<FullscreenTransitionData*>(aData); + FullscreenTransitionDelegate* delegate = + [[FullscreenTransitionDelegate alloc] init]; + delegate->mWindow = this; + // Storing already_AddRefed directly could cause static checking fail. + delegate->mCallback = nsCOMPtr<nsIRunnable>(aCallback).forget().take(); + + if (mFullscreenTransitionAnimation) { + [mFullscreenTransitionAnimation stopAnimation]; + ReleaseFullscreenTransitionAnimation(); + } + + NSDictionary* dict = @{ + NSViewAnimationTargetKey: data->mTransitionWindow, + NSViewAnimationEffectKey: aStage == eBeforeFullscreenToggle ? + NSViewAnimationFadeInEffect : NSViewAnimationFadeOutEffect + }; + mFullscreenTransitionAnimation = + [[NSViewAnimation alloc] initWithViewAnimations:@[dict]]; + [mFullscreenTransitionAnimation setDelegate:delegate]; + [mFullscreenTransitionAnimation setDuration:aDuration / 1000.0]; + [mFullscreenTransitionAnimation startAnimation]; +} + +void nsCocoaWindow::EnteredFullScreen(bool aFullScreen, bool aNativeMode) +{ + mInFullScreenTransition = false; + bool wasInFullscreen = mInFullScreenMode; + mInFullScreenMode = aFullScreen; + if (aNativeMode || mInNativeFullScreenMode) { + mInNativeFullScreenMode = aFullScreen; + } + DispatchSizeModeEvent(); + if (mWidgetListener && wasInFullscreen != aFullScreen) { + mWidgetListener->FullscreenChanged(aFullScreen); + } +} + +inline bool +nsCocoaWindow::ShouldToggleNativeFullscreen(bool aFullScreen, + bool aUseSystemTransition) +{ + if (!mSupportsNativeFullScreen) { + // If we cannot use native fullscreen, don't touch it. + return false; + } + if (mInNativeFullScreenMode) { + // If we are using native fullscreen, go ahead to exit it. + return true; + } + if (!aUseSystemTransition) { + // If we do not want the system fullscreen transition, + // don't use the native fullscreen. + return false; + } + // If we are using native fullscreen, we should have returned earlier. + return aFullScreen; +} + +nsresult +nsCocoaWindow::MakeFullScreen(bool aFullScreen, nsIScreen* aTargetScreen) +{ + return DoMakeFullScreen(aFullScreen, false); +} + +NS_IMETHODIMP +nsCocoaWindow::MakeFullScreenWithNativeTransition(bool aFullScreen, + nsIScreen* aTargetScreen) +{ + return DoMakeFullScreen(aFullScreen, true); +} + +nsresult +nsCocoaWindow::DoMakeFullScreen(bool aFullScreen, bool aUseSystemTransition) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (!mWindow) { + return NS_OK; + } + + // We will call into MakeFullScreen redundantly when entering/exiting + // fullscreen mode via OS X controls. When that happens we should just handle + // it gracefully - no need to ASSERT. + if (mInFullScreenMode == aFullScreen) { + return NS_OK; + } + + mInFullScreenTransition = true; + + if (ShouldToggleNativeFullscreen(aFullScreen, aUseSystemTransition)) { + // If we're using native fullscreen mode and our native window is invisible, + // our attempt to go into fullscreen mode will fail with an assertion in + // system code, without [WindowDelegate windowDidFailToEnterFullScreen:] + // ever getting called. To pre-empt this we bail here. See bug 752294. + if (aFullScreen && ![mWindow isVisible]) { + EnteredFullScreen(false); + return NS_OK; + } + MOZ_ASSERT(mInNativeFullScreenMode != aFullScreen, + "We shouldn't have been in native fullscreen."); + // Calling toggleFullScreen will result in windowDid(FailTo)?(Enter|Exit)FullScreen + // to be called from the OS. We will call EnteredFullScreen from those methods, + // where mInFullScreenMode will be set and a sizemode event will be dispatched. + [mWindow toggleFullScreen:nil]; + } else { + NSDisableScreenUpdates(); + // The order here matters. When we exit full screen mode, we need to show the + // Dock first, otherwise the newly-created window won't have its minimize + // button enabled. See bug 526282. + nsCocoaUtils::HideOSChromeOnScreen(aFullScreen); + nsBaseWidget::InfallibleMakeFullScreen(aFullScreen); + NSEnableScreenUpdates(); + EnteredFullScreen(aFullScreen, /* aNativeMode */ false); + } + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +// Coordinates are desktop pixels +nsresult nsCocoaWindow::DoResize(double aX, double aY, + double aWidth, double aHeight, + bool aRepaint, + bool aConstrainToCurrentScreen) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (!mWindow || mInResize) { + return NS_OK; + } + + AutoRestore<bool> reentrantResizeGuard(mInResize); + mInResize = true; + + // ConstrainSize operates in device pixels, so we need to convert using + // the backing scale factor here + CGFloat scale = BackingScaleFactor(); + int32_t width = NSToIntRound(aWidth * scale); + int32_t height = NSToIntRound(aHeight * scale); + ConstrainSize(&width, &height); + + DesktopIntRect newBounds(NSToIntRound(aX), NSToIntRound(aY), + NSToIntRound(width / scale), + NSToIntRound(height / scale)); + + // constrain to the screen that contains the largest area of the new rect + FitRectToVisibleAreaForScreen(newBounds, aConstrainToCurrentScreen ? + [mWindow screen] : nullptr); + + // convert requested bounds into Cocoa coordinate system + NSRect newFrame = nsCocoaUtils::GeckoRectToCocoaRect(newBounds); + + NSRect frame = [mWindow frame]; + BOOL isMoving = newFrame.origin.x != frame.origin.x || + newFrame.origin.y != frame.origin.y; + BOOL isResizing = newFrame.size.width != frame.size.width || + newFrame.size.height != frame.size.height; + + if (!isMoving && !isResizing) { + return NS_OK; + } + + // We ignore aRepaint -- we have to call display:YES, otherwise the + // title bar doesn't immediately get repainted and is displayed in + // the wrong place, leading to a visual jump. + [mWindow setFrame:newFrame display:YES]; + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +// Coordinates are desktop pixels +NS_IMETHODIMP nsCocoaWindow::Resize(double aX, double aY, + double aWidth, double aHeight, + bool aRepaint) +{ + return DoResize(aX, aY, aWidth, aHeight, aRepaint, false); +} + +// Coordinates are desktop pixels +NS_IMETHODIMP nsCocoaWindow::Resize(double aWidth, double aHeight, bool aRepaint) +{ + double invScale = 1.0 / BackingScaleFactor(); + return DoResize(mBounds.x * invScale, mBounds.y * invScale, + aWidth, aHeight, aRepaint, true); +} + +LayoutDeviceIntRect +nsCocoaWindow::GetClientBounds() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + CGFloat scaleFactor = BackingScaleFactor(); + if (!mWindow) { + return nsCocoaUtils::CocoaRectToGeckoRectDevPix(NSZeroRect, scaleFactor); + } + + NSRect r; + if ([mWindow isKindOfClass:[ToolbarWindow class]] && + [(ToolbarWindow*)mWindow drawsContentsIntoWindowFrame]) { + r = [mWindow frame]; + } else { + r = [mWindow contentRectForFrameRect:[mWindow frame]]; + } + + return nsCocoaUtils::CocoaRectToGeckoRectDevPix(r, scaleFactor); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(LayoutDeviceIntRect(0, 0, 0, 0)); +} + +void +nsCocoaWindow::UpdateBounds() +{ + NSRect frame = NSZeroRect; + if (mWindow) { + frame = [mWindow frame]; + } + mBounds = + nsCocoaUtils::CocoaRectToGeckoRectDevPix(frame, BackingScaleFactor()); +} + +LayoutDeviceIntRect +nsCocoaWindow::GetScreenBounds() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + +#ifdef DEBUG + LayoutDeviceIntRect r = nsCocoaUtils::CocoaRectToGeckoRectDevPix([mWindow frame], BackingScaleFactor()); + NS_ASSERTION(mWindow && mBounds == r, "mBounds out of sync!"); +#endif + + return mBounds; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(LayoutDeviceIntRect(0, 0, 0, 0)); +} + +double +nsCocoaWindow::GetDefaultScaleInternal() +{ + return BackingScaleFactor(); +} + +static CGFloat +GetBackingScaleFactor(NSWindow* aWindow) +{ + NSRect frame = [aWindow frame]; + if (frame.size.width > 0 && frame.size.height > 0) { + return nsCocoaUtils::GetBackingScaleFactor(aWindow); + } + + // For windows with zero width or height, the backingScaleFactor method + // is broken - it will always return 2 on a retina macbook, even when + // the window position implies it's on a non-hidpi external display + // (to the extent that a zero-area window can be said to be "on" a + // display at all!) + // And to make matters worse, Cocoa even fires a + // windowDidChangeBackingProperties notification with the + // NSBackingPropertyOldScaleFactorKey key when a window on an + // external display is resized to/from zero height, even though it hasn't + // really changed screens. + + // This causes us to handle popup window sizing incorrectly when the + // popup is resized to zero height (bug 820327) - nsXULPopupManager + // becomes (incorrectly) convinced the popup has been explicitly forced + // to a non-default size and needs to have size attributes attached. + + // Workaround: instead of asking the window, we'll find the screen it is on + // and ask that for *its* backing scale factor. + + // (See bug 853252 and additional comments in windowDidChangeScreen: below + // for further complications this causes.) + + // First, expand the rect so that it actually has a measurable area, + // for FindTargetScreenForRect to use. + if (frame.size.width == 0) { + frame.size.width = 1; + } + if (frame.size.height == 0) { + frame.size.height = 1; + } + + // Then identify the screen it belongs to, and return its scale factor. + NSScreen *screen = + FindTargetScreenForRect(nsCocoaUtils::CocoaRectToGeckoRect(frame)); + return nsCocoaUtils::GetBackingScaleFactor(screen); +} + +CGFloat +nsCocoaWindow::BackingScaleFactor() +{ + if (mBackingScaleFactor > 0.0) { + return mBackingScaleFactor; + } + if (!mWindow) { + return 1.0; + } + mBackingScaleFactor = GetBackingScaleFactor(mWindow); + return mBackingScaleFactor; +} + +void +nsCocoaWindow::BackingScaleFactorChanged() +{ + CGFloat newScale = GetBackingScaleFactor(mWindow); + + // ignore notification if it hasn't really changed (or maybe we have + // disabled HiDPI mode via prefs) + if (mBackingScaleFactor == newScale) { + return; + } + + if (mBackingScaleFactor > 0.0) { + // convert size constraints to the new device pixel coordinate space + double scaleFactor = newScale / mBackingScaleFactor; + mSizeConstraints.mMinSize.width = + NSToIntRound(mSizeConstraints.mMinSize.width * scaleFactor); + mSizeConstraints.mMinSize.height = + NSToIntRound(mSizeConstraints.mMinSize.height * scaleFactor); + if (mSizeConstraints.mMaxSize.width < NS_MAXSIZE) { + mSizeConstraints.mMaxSize.width = + std::min(NS_MAXSIZE, + NSToIntRound(mSizeConstraints.mMaxSize.width * scaleFactor)); + } + if (mSizeConstraints.mMaxSize.height < NS_MAXSIZE) { + mSizeConstraints.mMaxSize.height = + std::min(NS_MAXSIZE, + NSToIntRound(mSizeConstraints.mMaxSize.height * scaleFactor)); + } + } + + mBackingScaleFactor = newScale; + + if (!mWidgetListener || mWidgetListener->GetXULWindow()) { + return; + } + + nsIPresShell* presShell = mWidgetListener->GetPresShell(); + if (presShell) { + presShell->BackingScaleFactorChanged(); + } +} + +int32_t +nsCocoaWindow::RoundsWidgetCoordinatesTo() +{ + if (BackingScaleFactor() == 2.0) { + return 2; + } + return 1; +} + +NS_IMETHODIMP nsCocoaWindow::SetCursor(nsCursor aCursor) +{ + if (mPopupContentView) + return mPopupContentView->SetCursor(aCursor); + + return NS_OK; +} + +NS_IMETHODIMP nsCocoaWindow::SetCursor(imgIContainer* aCursor, + uint32_t aHotspotX, uint32_t aHotspotY) +{ + if (mPopupContentView) + return mPopupContentView->SetCursor(aCursor, aHotspotX, aHotspotY); + + return NS_OK; +} + +NS_IMETHODIMP nsCocoaWindow::SetTitle(const nsAString& aTitle) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + if (!mWindow) + return NS_OK; + + const nsString& strTitle = PromiseFlatString(aTitle); + NSString* title = [NSString stringWithCharacters:reinterpret_cast<const unichar*>(strTitle.get()) + length:strTitle.Length()]; + + if ([mWindow drawsContentsIntoWindowFrame] && ![mWindow wantsTitleDrawn]) { + // Don't cause invalidations. + [mWindow disableSetNeedsDisplay]; + [mWindow setTitle:title]; + [mWindow enableSetNeedsDisplay]; + } else { + [mWindow setTitle:title]; + } + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP nsCocoaWindow::Invalidate(const LayoutDeviceIntRect& aRect) +{ + if (mPopupContentView) { + return mPopupContentView->Invalidate(aRect); + } + + return NS_OK; +} + +// Pass notification of some drag event to Gecko +// +// The drag manager has let us know that something related to a drag has +// occurred in this window. It could be any number of things, ranging from +// a drop, to a drag enter/leave, or a drag over event. The actual event +// is passed in |aMessage| and is passed along to our event hanlder so Gecko +// knows about it. +bool nsCocoaWindow::DragEvent(unsigned int aMessage, mozilla::gfx::Point aMouseGlobal, UInt16 aKeyModifiers) +{ + return false; +} + +NS_IMETHODIMP nsCocoaWindow::SendSetZLevelEvent() +{ + nsWindowZ placement = nsWindowZTop; + nsCOMPtr<nsIWidget> actualBelow; + if (mWidgetListener) + mWidgetListener->ZLevelChanged(true, &placement, nullptr, getter_AddRefs(actualBelow)); + return NS_OK; +} + +NS_IMETHODIMP nsCocoaWindow::GetChildSheet(bool aShown, nsIWidget** _retval) +{ + nsIWidget* child = GetFirstChild(); + + while (child) { + if (child->WindowType() == eWindowType_sheet) { + // if it's a sheet, it must be an nsCocoaWindow + nsCocoaWindow* cocoaWindow = static_cast<nsCocoaWindow*>(child); + if (cocoaWindow->mWindow && + ((aShown && [cocoaWindow->mWindow isVisible]) || + (!aShown && cocoaWindow->mSheetNeedsShow))) { + nsCOMPtr<nsIWidget> widget = cocoaWindow; + widget.forget(_retval); + return NS_OK; + } + } + child = child->GetNextSibling(); + } + + *_retval = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP nsCocoaWindow::GetRealParent(nsIWidget** parent) +{ + *parent = mParent; + return NS_OK; +} + +NS_IMETHODIMP nsCocoaWindow::GetIsSheet(bool* isSheet) +{ + mWindowType == eWindowType_sheet ? *isSheet = true : *isSheet = false; + return NS_OK; +} + +NS_IMETHODIMP nsCocoaWindow::GetSheetWindowParent(NSWindow** sheetWindowParent) +{ + *sheetWindowParent = mSheetWindowParent; + return NS_OK; +} + +// Invokes callback and ProcessEvent methods on Event Listener object +NS_IMETHODIMP +nsCocoaWindow::DispatchEvent(WidgetGUIEvent* event, nsEventStatus& aStatus) +{ + aStatus = nsEventStatus_eIgnore; + + nsCOMPtr<nsIWidget> kungFuDeathGrip(event->mWidget); + mozilla::Unused << kungFuDeathGrip; // Not used within this function + + if (mWidgetListener) + aStatus = mWidgetListener->HandleEvent(event, mUseAttachedEvents); + + return NS_OK; +} + +// aFullScreen should be the window's mInFullScreenMode. We don't have access to that +// from here, so we need to pass it in. mInFullScreenMode should be the canonical +// indicator that a window is currently full screen and it makes sense to keep +// all sizemode logic here. +static nsSizeMode +GetWindowSizeMode(NSWindow* aWindow, bool aFullScreen) { + if (aFullScreen) + return nsSizeMode_Fullscreen; + if ([aWindow isMiniaturized]) + return nsSizeMode_Minimized; + if (([aWindow styleMask] & NSResizableWindowMask) && [aWindow isZoomed]) + return nsSizeMode_Maximized; + return nsSizeMode_Normal; +} + +void +nsCocoaWindow::ReportMoveEvent() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + // Prevent recursion, which can become infinite (see bug 708278). This + // can happen when the call to [NSWindow setFrameTopLeftPoint:] in + // nsCocoaWindow::Move() triggers an immediate NSWindowDidMove notification + // (and a call to [WindowDelegate windowDidMove:]). + if (mInReportMoveEvent) { + return; + } + mInReportMoveEvent = true; + + UpdateBounds(); + + // Dispatch the move event to Gecko + NotifyWindowMoved(mBounds.x, mBounds.y); + + mInReportMoveEvent = false; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +nsCocoaWindow::DispatchSizeModeEvent() +{ + if (!mWindow) { + return; + } + + nsSizeMode newMode = GetWindowSizeMode(mWindow, mInFullScreenMode); + + // Don't dispatch a sizemode event if: + // 1. the window is transitioning to fullscreen + // 2. the new sizemode is the same as the current sizemode + if (mInFullScreenTransition || mSizeMode == newMode) { + return; + } + + mSizeMode = newMode; + if (mWidgetListener) { + mWidgetListener->SizeModeChanged(newMode); + } +} + +void +nsCocoaWindow::ReportSizeEvent() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + UpdateBounds(); + + if (mWidgetListener) { + LayoutDeviceIntRect innerBounds = GetClientBounds(); + mWidgetListener->WindowResized(this, innerBounds.width, innerBounds.height); + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void nsCocoaWindow::SetMenuBar(nsMenuBarX *aMenuBar) +{ + if (mMenuBar) + mMenuBar->SetParent(nullptr); + if (!mWindow) { + mMenuBar = nullptr; + return; + } + mMenuBar = aMenuBar; + + // Only paint for active windows, or paint the hidden window menu bar if no + // other menu bar has been painted yet so that some reasonable menu bar is + // displayed when the app starts up. + id windowDelegate = [mWindow delegate]; + if (mMenuBar && + ((!gSomeMenuBarPainted && nsMenuUtilsX::GetHiddenWindowMenuBar() == mMenuBar) || + (windowDelegate && [windowDelegate toplevelActiveState]))) + mMenuBar->Paint(); +} + +NS_IMETHODIMP nsCocoaWindow::SetFocus(bool aState) +{ + if (!mWindow) + return NS_OK; + + if (mPopupContentView) { + mPopupContentView->SetFocus(aState); + } + else if (aState && ([mWindow isVisible] || [mWindow isMiniaturized])) { + if ([mWindow isMiniaturized]) { + [mWindow deminiaturize:nil]; + } + + [mWindow makeKeyAndOrderFront:nil]; + SendSetZLevelEvent(); + } + + return NS_OK; +} + +LayoutDeviceIntPoint nsCocoaWindow::WidgetToScreenOffset() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + NSRect rect = NSZeroRect; + LayoutDeviceIntRect r; + if (mWindow) { + rect = [mWindow contentRectForFrameRect:[mWindow frame]]; + } + r = nsCocoaUtils::CocoaRectToGeckoRectDevPix(rect, BackingScaleFactor()); + + return r.TopLeft(); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(LayoutDeviceIntPoint(0,0)); +} + +LayoutDeviceIntPoint nsCocoaWindow::GetClientOffset() +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + LayoutDeviceIntRect clientRect = GetClientBounds(); + + return clientRect.TopLeft() - mBounds.TopLeft(); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(LayoutDeviceIntPoint(0, 0)); +} + +LayoutDeviceIntSize +nsCocoaWindow::ClientToWindowSize(const LayoutDeviceIntSize& aClientSize) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + if (!mWindow) + return LayoutDeviceIntSize(0, 0); + + CGFloat backingScale = BackingScaleFactor(); + LayoutDeviceIntRect r(0, 0, aClientSize.width, aClientSize.height); + NSRect rect = nsCocoaUtils::DevPixelsToCocoaPoints(r, backingScale); + + NSRect inflatedRect = [mWindow frameRectForContentRect:rect]; + r = nsCocoaUtils::CocoaRectToGeckoRectDevPix(inflatedRect, backingScale); + return r.Size(); + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(LayoutDeviceIntSize(0,0)); +} + +nsMenuBarX* nsCocoaWindow::GetMenuBar() +{ + return mMenuBar; +} + +void +nsCocoaWindow::CaptureRollupEvents(nsIRollupListener* aListener, + bool aDoCapture) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + gRollupListener = nullptr; + + if (aDoCapture) { + if (![NSApp isActive]) { + // We need to capture mouse event if we aren't + // the active application. We only set this up when needed + // because they cause spurious mouse event after crash + // and gdb sessions. See bug 699538. + nsToolkit::GetToolkit()->RegisterForAllProcessMouseEvents(); + } + gRollupListener = aListener; + + // Sometimes more than one popup window can be visible at the same time + // (e.g. nested non-native context menus, or the test case (attachment + // 276885) for bmo bug 392389, which displays a non-native combo-box in a + // non-native popup window). In these cases the "active" popup window should + // be the topmost -- the (nested) context menu the mouse is currently over, + // or the combo-box's drop-down list (when it's displayed). But (among + // windows that have the same "level") OS X makes topmost the window that + // last received a mouse-down event, which may be incorrect (in the combo- + // box case, it makes topmost the window containing the combo-box). So + // here we fiddle with a non-native popup window's level to make sure the + // "active" one is always above any other non-native popup windows that + // may be visible. + if (mWindow && (mWindowType == eWindowType_popup)) + SetPopupWindowLevel(); + } else { + nsToolkit::GetToolkit()->UnregisterAllProcessMouseEventHandlers(); + + // XXXndeakin this doesn't make sense. + // Why is the new window assumed to be a modal panel? + if (mWindow && (mWindowType == eWindowType_popup)) + [mWindow setLevel:NSModalPanelWindowLevel]; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +NS_IMETHODIMP nsCocoaWindow::GetAttention(int32_t aCycleCount) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + [NSApp requestUserAttention:NSInformationalRequest]; + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +bool +nsCocoaWindow::HasPendingInputEvent() +{ + return nsChildView::DoHasPendingInputEvent(); +} + +void +nsCocoaWindow::SetWindowShadowStyle(int32_t aStyle) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mWindow) + return; + + mShadowStyle = aStyle; + + // Shadowless windows are only supported on popups. + if (mWindowType == eWindowType_popup) + [mWindow setHasShadow:(aStyle != NS_STYLE_WINDOW_SHADOW_NONE)]; + + [mWindow setUseMenuStyle:(aStyle == NS_STYLE_WINDOW_SHADOW_MENU)]; + AdjustWindowShadow(); + SetWindowBackgroundBlur(); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void nsCocoaWindow::SetShowsToolbarButton(bool aShow) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (mWindow) + [mWindow setShowsToolbarButton:aShow]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void nsCocoaWindow::SetShowsFullScreenButton(bool aShow) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mWindow || ![mWindow respondsToSelector:@selector(toggleFullScreen:)] || + mSupportsNativeFullScreen == aShow) { + return; + } + + // If the window is currently in fullscreen mode, then we're going to + // transition out first, then set the collection behavior & toggle + // mSupportsNativeFullScreen, then transtion back into fullscreen mode. This + // prevents us from getting into a conflicting state with MakeFullScreen + // where mSupportsNativeFullScreen would lead us down the wrong path. + bool wasFullScreen = mInFullScreenMode; + + if (wasFullScreen) { + MakeFullScreen(false); + } + + NSWindowCollectionBehavior newBehavior = [mWindow collectionBehavior]; + if (aShow) { + newBehavior |= NSWindowCollectionBehaviorFullScreenPrimary; + } else { + newBehavior &= ~NSWindowCollectionBehaviorFullScreenPrimary; + } + [mWindow setCollectionBehavior:newBehavior]; + mSupportsNativeFullScreen = aShow; + + if (wasFullScreen) { + MakeFullScreen(true); + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void nsCocoaWindow::SetWindowAnimationType(nsIWidget::WindowAnimationType aType) +{ + mAnimationType = aType; +} + +void +nsCocoaWindow::SetDrawsTitle(bool aDrawTitle) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [mWindow setWantsTitleDrawn:aDrawTitle]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void +nsCocoaWindow::SetUseBrightTitlebarForeground(bool aBrightForeground) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [mWindow setUseBrightTitlebarForeground:aBrightForeground]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +NS_IMETHODIMP nsCocoaWindow::SetNonClientMargins(LayoutDeviceIntMargin &margins) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + SetDrawsInTitlebar(margins.top == 0); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +void +nsCocoaWindow::SetWindowTitlebarColor(nscolor aColor, bool aActive) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (!mWindow) + return; + + // If they pass a color with a complete transparent alpha component, use the + // native titlebar appearance. + if (NS_GET_A(aColor) == 0) { + [mWindow setTitlebarColor:nil forActiveWindow:(BOOL)aActive]; + } else { + // Transform from sRGBA to monitor RGBA. This seems like it would make trying + // to match the system appearance lame, so probably we just shouldn't color + // correct chrome. + if (gfxPlatform::GetCMSMode() == eCMSMode_All) { + qcms_transform *transform = gfxPlatform::GetCMSRGBATransform(); + if (transform) { + uint8_t color[3]; + color[0] = NS_GET_R(aColor); + color[1] = NS_GET_G(aColor); + color[2] = NS_GET_B(aColor); + qcms_transform_data(transform, color, color, 1); + aColor = NS_RGB(color[0], color[1], color[2]); + } + } + + [mWindow setTitlebarColor:[NSColor colorWithDeviceRed:NS_GET_R(aColor)/255.0 + green:NS_GET_G(aColor)/255.0 + blue:NS_GET_B(aColor)/255.0 + alpha:NS_GET_A(aColor)/255.0] + forActiveWindow:(BOOL)aActive]; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void nsCocoaWindow::SetDrawsInTitlebar(bool aState) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (mWindow) + [mWindow setDrawsContentsIntoWindowFrame:aState]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +NS_IMETHODIMP nsCocoaWindow::SynthesizeNativeMouseEvent(LayoutDeviceIntPoint aPoint, + uint32_t aNativeMessage, + uint32_t aModifierFlags, + nsIObserver* aObserver) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + AutoObserverNotifier notifier(aObserver, "mouseevent"); + if (mPopupContentView) + return mPopupContentView->SynthesizeNativeMouseEvent(aPoint, aNativeMessage, + aModifierFlags, nullptr); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +void nsCocoaWindow::UpdateThemeGeometries(const nsTArray<ThemeGeometry>& aThemeGeometries) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + if (mPopupContentView) { + return mPopupContentView->UpdateThemeGeometries(aThemeGeometries); + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +void nsCocoaWindow::SetPopupWindowLevel() +{ + if (!mWindow) + return; + + // Floating popups are at the floating level and hide when the window is + // deactivated. + if (mPopupLevel == ePopupLevelFloating) { + [mWindow setLevel:NSFloatingWindowLevel]; + [mWindow setHidesOnDeactivate:YES]; + } + else { + // Otherwise, this is a top-level or parent popup. Parent popups always + // appear just above their parent and essentially ignore the level. + [mWindow setLevel:NSPopUpMenuWindowLevel]; + [mWindow setHidesOnDeactivate:NO]; + } +} + +NS_IMETHODIMP_(void) +nsCocoaWindow::SetInputContext(const InputContext& aContext, + const InputContextAction& aAction) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + mInputContext = aContext; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +NS_IMETHODIMP_(bool) +nsCocoaWindow::ExecuteNativeKeyBinding(NativeKeyBindingsType aType, + const WidgetKeyboardEvent& aEvent, + DoCommandCallback aCallback, + void* aCallbackData) +{ + NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType); + return keyBindings->Execute(aEvent, aCallback, aCallbackData); +} + +@implementation WindowDelegate + +// We try to find a gecko menu bar to paint. If one does not exist, just paint +// the application menu by itself so that a window doesn't have some other +// window's menu bar. ++ (void)paintMenubarForWindow:(NSWindow*)aWindow +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + // make sure we only act on windows that have this kind of + // object as a delegate + id windowDelegate = [aWindow delegate]; + if ([windowDelegate class] != [self class]) + return; + + nsCocoaWindow* geckoWidget = [windowDelegate geckoWidget]; + NS_ASSERTION(geckoWidget, "Window delegate not returning a gecko widget!"); + + nsMenuBarX* geckoMenuBar = geckoWidget->GetMenuBar(); + if (geckoMenuBar) { + geckoMenuBar->Paint(); + } + else { + // sometimes we don't have a native application menu early in launching + if (!sApplicationMenu) + return; + + NSMenu* mainMenu = [NSApp mainMenu]; + NS_ASSERTION([mainMenu numberOfItems] > 0, "Main menu does not have any items, something is terribly wrong!"); + + // Create a new menu bar. + // We create a GeckoNSMenu because all menu bar NSMenu objects should use that subclass for + // key handling reasons. + GeckoNSMenu* newMenuBar = [[GeckoNSMenu alloc] initWithTitle:@"MainMenuBar"]; + + // move the application menu from the existing menu bar to the new one + NSMenuItem* firstMenuItem = [[mainMenu itemAtIndex:0] retain]; + [mainMenu removeItemAtIndex:0]; + [newMenuBar insertItem:firstMenuItem atIndex:0]; + [firstMenuItem release]; + + // set our new menu bar as the main menu + [NSApp setMainMenu:newMenuBar]; + [newMenuBar release]; + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (id)initWithGeckoWindow:(nsCocoaWindow*)geckoWind +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + [super init]; + mGeckoWindow = geckoWind; + mToplevelActiveState = false; + mHasEverBeenZoomed = false; + return self; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)proposedFrameSize +{ + RollUpPopups(); + + return proposedFrameSize; +} + +- (void)windowDidResize:(NSNotification *)aNotification +{ + BaseWindow* window = [aNotification object]; + [window updateTrackingArea]; + + if (!mGeckoWindow) + return; + + // Resizing might have changed our zoom state. + mGeckoWindow->DispatchSizeModeEvent(); + mGeckoWindow->ReportSizeEvent(); +} + +- (void)windowDidChangeScreen:(NSNotification *)aNotification +{ + if (!mGeckoWindow) + return; + + // Because of Cocoa's peculiar treatment of zero-size windows (see comments + // at GetBackingScaleFactor() above), we sometimes have a situation where + // our concept of backing scale (based on the screen where the zero-sized + // window is positioned) differs from Cocoa's idea (always based on the + // Retina screen, AFAICT, even when an external non-Retina screen is the + // primary display). + // + // As a result, if the window was created with zero size on an external + // display, but then made visible on the (secondary) Retina screen, we + // will *not* get a windowDidChangeBackingProperties notification for it. + // This leads to an incorrect GetDefaultScale(), and widget coordinate + // confusion, as per bug 853252. + // + // To work around this, we check for a backing scale mismatch when we + // receive a windowDidChangeScreen notification, as we will receive this + // even if Cocoa was already treating the zero-size window as having + // Retina backing scale. + NSWindow *window = (NSWindow *)[aNotification object]; + if ([window respondsToSelector:@selector(backingScaleFactor)]) { + if (GetBackingScaleFactor(window) != mGeckoWindow->BackingScaleFactor()) { + mGeckoWindow->BackingScaleFactorChanged(); + } + } + + mGeckoWindow->ReportMoveEvent(); +} + +// Lion's full screen mode will bypass our internal fullscreen tracking, so +// we need to catch it when we transition and call our own methods, which in +// turn will fire "fullscreen" events. +- (void)windowDidEnterFullScreen:(NSNotification *)notification +{ + if (!mGeckoWindow) { + return; + } + + mGeckoWindow->EnteredFullScreen(true); + + // On Yosemite, the NSThemeFrame class has two new properties -- + // titlebarView (an NSTitlebarView object) and titlebarContainerView (an + // NSTitlebarContainerView object). These are used to display the titlebar + // in fullscreen mode. In Safari they're not transparent. But in Firefox + // for some reason they are, which causes bug 1069658. The following code + // works around this Apple bug or design flaw. + NSWindow *window = (NSWindow *) [notification object]; + NSView *frameView = [[window contentView] superview]; + NSView *titlebarView = nil; + NSView *titlebarContainerView = nil; + if ([frameView respondsToSelector:@selector(titlebarView)]) { + titlebarView = [frameView titlebarView]; + } + if ([frameView respondsToSelector:@selector(titlebarContainerView)]) { + titlebarContainerView = [frameView titlebarContainerView]; + } + if ([titlebarView respondsToSelector:@selector(setTransparent:)]) { + [titlebarView setTransparent:NO]; + } + if ([titlebarContainerView respondsToSelector:@selector(setTransparent:)]) { + [titlebarContainerView setTransparent:NO]; + } +} + +- (void)windowDidExitFullScreen:(NSNotification *)notification +{ + if (!mGeckoWindow) { + return; + } + + mGeckoWindow->EnteredFullScreen(false); +} + +- (void)windowDidFailToEnterFullScreen:(NSWindow *)window +{ + if (!mGeckoWindow) { + return; + } + + mGeckoWindow->EnteredFullScreen(false); +} + +- (void)windowDidFailToExitFullScreen:(NSWindow *)window +{ + if (!mGeckoWindow) { + return; + } + + mGeckoWindow->EnteredFullScreen(true); +} + +- (void)windowDidBecomeMain:(NSNotification *)aNotification +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + RollUpPopups(); + ChildViewMouseTracker::ReEvaluateMouseEnterState(); + + // [NSApp _isRunningAppModal] will return true if we're running an OS dialog + // app modally. If one of those is up then we want it to retain its menu bar. + if ([NSApp _isRunningAppModal]) + return; + NSWindow* window = [aNotification object]; + if (window) + [WindowDelegate paintMenubarForWindow:window]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)windowDidResignMain:(NSNotification *)aNotification +{ + RollUpPopups(); + ChildViewMouseTracker::ReEvaluateMouseEnterState(); + + // [NSApp _isRunningAppModal] will return true if we're running an OS dialog + // app modally. If one of those is up then we want it to retain its menu bar. + if ([NSApp _isRunningAppModal]) + return; + RefPtr<nsMenuBarX> hiddenWindowMenuBar = nsMenuUtilsX::GetHiddenWindowMenuBar(); + if (hiddenWindowMenuBar) { + // printf("painting hidden window menu bar due to window losing main status\n"); + hiddenWindowMenuBar->Paint(); + } +} + +- (void)windowDidBecomeKey:(NSNotification *)aNotification +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + RollUpPopups(); + ChildViewMouseTracker::ReEvaluateMouseEnterState(); + + NSWindow* window = [aNotification object]; + if ([window isSheet]) + [WindowDelegate paintMenubarForWindow:window]; + + nsChildView* mainChildView = + static_cast<nsChildView*>([[(BaseWindow*)window mainChildView] widget]); + if (mainChildView) { + if (mainChildView->GetInputContext().IsPasswordEditor()) { + TextInputHandler::EnableSecureEventInput(); + } else { + TextInputHandler::EnsureSecureEventInputDisabled(); + } + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)windowDidResignKey:(NSNotification *)aNotification +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + RollUpPopups(); + ChildViewMouseTracker::ReEvaluateMouseEnterState(); + + // If a sheet just resigned key then we should paint the menu bar + // for whatever window is now main. + NSWindow* window = [aNotification object]; + if ([window isSheet]) + [WindowDelegate paintMenubarForWindow:[NSApp mainWindow]]; + + TextInputHandler::EnsureSecureEventInputDisabled(); + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)windowWillMove:(NSNotification *)aNotification +{ + RollUpPopups(); +} + +- (void)windowDidMove:(NSNotification *)aNotification +{ + if (mGeckoWindow) + mGeckoWindow->ReportMoveEvent(); +} + +- (BOOL)windowShouldClose:(id)sender +{ + nsIWidgetListener* listener = mGeckoWindow ? mGeckoWindow->GetWidgetListener() : nullptr; + if (listener) + listener->RequestWindowClose(mGeckoWindow); + return NO; // gecko will do it +} + +- (void)windowWillClose:(NSNotification *)aNotification +{ + RollUpPopups(); +} + +- (void)windowWillMiniaturize:(NSNotification *)aNotification +{ + RollUpPopups(); +} + +- (void)windowDidMiniaturize:(NSNotification *)aNotification +{ + if (mGeckoWindow) + mGeckoWindow->DispatchSizeModeEvent(); +} + +- (void)windowDidDeminiaturize:(NSNotification *)aNotification +{ + if (mGeckoWindow) + mGeckoWindow->DispatchSizeModeEvent(); +} + +- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)proposedFrame +{ + if (!mHasEverBeenZoomed && [window isZoomed]) + return NO; // See bug 429954. + + mHasEverBeenZoomed = YES; + return YES; +} + +- (void)didEndSheet:(NSWindow*)sheet returnCode:(int)returnCode contextInfo:(void*)contextInfo +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + // Note: 'contextInfo' (if it is set) is the window that is the parent of + // the sheet. The value of contextInfo is determined in + // nsCocoaWindow::Show(). If it's set, 'contextInfo' is always the top- + // level window, not another sheet itself. But 'contextInfo' is nil if + // our parent window is also a sheet -- in that case we shouldn't send + // the top-level window any activate events (because it's our parent + // window that needs to get these events, not the top-level window). + [TopLevelWindowData deactivateInWindow:sheet]; + [sheet orderOut:self]; + if (contextInfo) + [TopLevelWindowData activateInWindow:(NSWindow*)contextInfo]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)windowDidChangeBackingProperties:(NSNotification *)aNotification +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + NSWindow *window = (NSWindow *)[aNotification object]; + + if ([window respondsToSelector:@selector(backingScaleFactor)]) { + CGFloat oldFactor = + [[[aNotification userInfo] + objectForKey:@"NSBackingPropertyOldScaleFactorKey"] doubleValue]; + if ([window backingScaleFactor] != oldFactor) { + mGeckoWindow->BackingScaleFactorChanged(); + } + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (nsCocoaWindow*)geckoWidget +{ + return mGeckoWindow; +} + +- (bool)toplevelActiveState +{ + return mToplevelActiveState; +} + +- (void)sendToplevelActivateEvents +{ + if (!mToplevelActiveState && mGeckoWindow) { + nsIWidgetListener* listener = mGeckoWindow->GetWidgetListener(); + if (listener) { + listener->WindowActivated(); + } + mToplevelActiveState = true; + } +} + +- (void)sendToplevelDeactivateEvents +{ + if (mToplevelActiveState && mGeckoWindow) { + nsIWidgetListener* listener = mGeckoWindow->GetWidgetListener(); + if (listener) { + listener->WindowDeactivated(); + } + mToplevelActiveState = false; + } +} + +@end + +static float +GetDPI(NSWindow* aWindow) +{ + NSScreen* screen = [aWindow screen]; + if (!screen) + return 96.0f; + + CGDirectDisplayID displayID = + [[[screen deviceDescription] objectForKey:@"NSScreenNumber"] intValue]; + CGFloat heightMM = ::CGDisplayScreenSize(displayID).height; + size_t heightPx = ::CGDisplayPixelsHigh(displayID); + if (heightMM < 1 || heightPx < 1) { + // Something extremely bogus is going on + return 96.0f; + } + + float dpi = heightPx / (heightMM / MM_PER_INCH_FLOAT); + + // Account for HiDPI mode where Cocoa's "points" do not correspond to real + // device pixels + CGFloat backingScale = GetBackingScaleFactor(aWindow); + + return dpi * backingScale; +} + +@interface NSView(FrameViewMethodSwizzling) +- (NSPoint)FrameView__closeButtonOrigin; +- (NSPoint)FrameView__fullScreenButtonOrigin; +@end + +@implementation NSView(FrameViewMethodSwizzling) + +- (NSPoint)FrameView__closeButtonOrigin +{ + NSPoint defaultPosition = [self FrameView__closeButtonOrigin]; + if ([[self window] isKindOfClass:[ToolbarWindow class]]) { + return [(ToolbarWindow*)[self window] windowButtonsPositionWithDefaultPosition:defaultPosition]; + } + return defaultPosition; +} + +- (NSPoint)FrameView__fullScreenButtonOrigin +{ + NSPoint defaultPosition = [self FrameView__fullScreenButtonOrigin]; + if ([[self window] isKindOfClass:[ToolbarWindow class]]) { + return [(ToolbarWindow*)[self window] fullScreenButtonPositionWithDefaultPosition:defaultPosition]; + } + return defaultPosition; +} + +@end + +static NSMutableSet *gSwizzledFrameViewClasses = nil; + +@interface NSWindow(PrivateSetNeedsDisplayInRectMethod) + - (void)_setNeedsDisplayInRect:(NSRect)aRect; +@end + +// This method is on NSThemeFrame starting with 10.10, but since NSThemeFrame +// is not a public class, we declare the method on NSView instead. We only have +// this declaration in order to avoid compiler warnings. +@interface NSView(PrivateAddKnownSubviewMethod) + - (void)_addKnownSubview:(NSView*)aView positioned:(NSWindowOrderingMode)place relativeTo:(NSView*)otherView; +@end + +#if !defined(MAC_OS_X_VERSION_10_10) || \ + MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_10 + +@interface NSImage(CapInsets) +- (void)setCapInsets:(NSEdgeInsets)capInsets; +@end + +#endif + +#if !defined(MAC_OS_X_VERSION_10_8) || \ + MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_8 + +@interface NSImage(ImageCreationWithDrawingHandler) ++ (NSImage *)imageWithSize:(NSSize)size + flipped:(BOOL)drawingHandlerShouldBeCalledWithFlippedContext + drawingHandler:(BOOL (^)(NSRect dstRect))drawingHandler; +@end + +#endif + +@interface NSView(NSVisualEffectViewSetMaskImage) +- (void)setMaskImage:(NSImage*)image; +@end + +@interface BaseWindow(Private) +- (void)removeTrackingArea; +- (void)cursorUpdated:(NSEvent*)aEvent; +- (void)updateContentViewSize; +- (void)reflowTitlebarElements; +@end + +@implementation BaseWindow + +// The frame of a window is implemented using undocumented NSView subclasses. +// We offset the window buttons by overriding the methods _closeButtonOrigin +// and _fullScreenButtonOrigin on these frame view classes. The class which is +// used for a window is determined in the window's frameViewClassForStyleMask: +// method, so this is where we make sure that we have swizzled the method on +// all encountered classes. ++ (Class)frameViewClassForStyleMask:(NSUInteger)styleMask +{ + Class frameViewClass = [super frameViewClassForStyleMask:styleMask]; + + if (!gSwizzledFrameViewClasses) { + gSwizzledFrameViewClasses = [[NSMutableSet setWithCapacity:3] retain]; + if (!gSwizzledFrameViewClasses) { + return frameViewClass; + } + } + + static IMP our_closeButtonOrigin = + class_getMethodImplementation([NSView class], + @selector(FrameView__closeButtonOrigin)); + static IMP our_fullScreenButtonOrigin = + class_getMethodImplementation([NSView class], + @selector(FrameView__fullScreenButtonOrigin)); + + if (![gSwizzledFrameViewClasses containsObject:frameViewClass]) { + // Either of these methods might be implemented in both a subclass of + // NSFrameView and one of its own subclasses. Which means that if we + // aren't careful we might end up swizzling the same method twice. + // Since method swizzling involves swapping pointers, this would break + // things. + IMP _closeButtonOrigin = + class_getMethodImplementation(frameViewClass, + @selector(_closeButtonOrigin)); + if (_closeButtonOrigin && _closeButtonOrigin != our_closeButtonOrigin) { + nsToolkit::SwizzleMethods(frameViewClass, @selector(_closeButtonOrigin), + @selector(FrameView__closeButtonOrigin)); + } + IMP _fullScreenButtonOrigin = + class_getMethodImplementation(frameViewClass, + @selector(_fullScreenButtonOrigin)); + if (_fullScreenButtonOrigin && + _fullScreenButtonOrigin != our_fullScreenButtonOrigin) { + nsToolkit::SwizzleMethods(frameViewClass, @selector(_fullScreenButtonOrigin), + @selector(FrameView__fullScreenButtonOrigin)); + } + [gSwizzledFrameViewClasses addObject:frameViewClass]; + } + + return frameViewClass; +} + +- (id)initWithContentRect:(NSRect)aContentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)aBufferingType defer:(BOOL)aFlag +{ + mDrawsIntoWindowFrame = NO; + [super initWithContentRect:aContentRect styleMask:aStyle backing:aBufferingType defer:aFlag]; + mState = nil; + mActiveTitlebarColor = nil; + mInactiveTitlebarColor = nil; + mScheduledShadowInvalidation = NO; + mDisabledNeedsDisplay = NO; + mDPI = GetDPI(self); + mTrackingArea = nil; + mDirtyRect = NSZeroRect; + mBeingShown = NO; + mDrawTitle = NO; + mBrightTitlebarForeground = NO; + mUseMenuStyle = NO; + [self updateTrackingArea]; + + return self; +} + +// Returns an autoreleased NSImage. +static NSImage* +GetMenuMaskImage() +{ + CGFloat radius = 4.0f; + NSEdgeInsets insets = { 5, 5, 5, 5 }; + NSSize maskSize = { 12, 12 }; + NSImage* maskImage = [NSImage imageWithSize:maskSize flipped:YES drawingHandler:^BOOL(NSRect dstRect) { + NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:dstRect xRadius:radius yRadius:radius]; + [[NSColor colorWithDeviceWhite:1.0 alpha:1.0] set]; + [path fill]; + return YES; + }]; + [maskImage setCapInsets:insets]; + return maskImage; +} + +- (void)swapOutChildViewWrapper:(NSView*)aNewWrapper +{ + [aNewWrapper setFrame:[[self contentView] frame]]; + NSView* childView = [[self mainChildView] retain]; + [childView removeFromSuperview]; + [aNewWrapper addSubview:childView]; + [childView release]; + [super setContentView:aNewWrapper]; +} + +- (void)setUseMenuStyle:(BOOL)aValue +{ + if (!VibrancyManager::SystemSupportsVibrancy()) { + return; + } + + if (aValue && !mUseMenuStyle) { + // Turn on rounded corner masking. + NSView* effectView = VibrancyManager::CreateEffectView(VibrancyType::MENU, YES); + if ([effectView respondsToSelector:@selector(setMaskImage:)]) { + [effectView setMaskImage:GetMenuMaskImage()]; + } + [self swapOutChildViewWrapper:effectView]; + [effectView release]; + } else if (mUseMenuStyle && !aValue) { + // Turn off rounded corner masking. + NSView* wrapper = [[NSView alloc] initWithFrame:NSZeroRect]; + [self swapOutChildViewWrapper:wrapper]; + [wrapper release]; + } + mUseMenuStyle = aValue; +} + +- (void)setBeingShown:(BOOL)aValue +{ + mBeingShown = aValue; +} + +- (BOOL)isBeingShown +{ + return mBeingShown; +} + +- (BOOL)isVisibleOrBeingShown +{ + return [super isVisible] || mBeingShown; +} + +- (void)disableSetNeedsDisplay +{ + mDisabledNeedsDisplay = YES; +} + +- (void)enableSetNeedsDisplay +{ + mDisabledNeedsDisplay = NO; +} + +- (void)dealloc +{ + [mActiveTitlebarColor release]; + [mInactiveTitlebarColor release]; + [self removeTrackingArea]; + ChildViewMouseTracker::OnDestroyWindow(self); + [super dealloc]; +} + +static const NSString* kStateTitleKey = @"title"; +static const NSString* kStateDrawsContentsIntoWindowFrameKey = @"drawsContentsIntoWindowFrame"; +static const NSString* kStateActiveTitlebarColorKey = @"activeTitlebarColor"; +static const NSString* kStateInactiveTitlebarColorKey = @"inactiveTitlebarColor"; +static const NSString* kStateShowsToolbarButton = @"showsToolbarButton"; +static const NSString* kStateCollectionBehavior = @"collectionBehavior"; + +- (void)importState:(NSDictionary*)aState +{ + [self setTitle:[aState objectForKey:kStateTitleKey]]; + [self setDrawsContentsIntoWindowFrame:[[aState objectForKey:kStateDrawsContentsIntoWindowFrameKey] boolValue]]; + [self setTitlebarColor:[aState objectForKey:kStateActiveTitlebarColorKey] forActiveWindow:YES]; + [self setTitlebarColor:[aState objectForKey:kStateInactiveTitlebarColorKey] forActiveWindow:NO]; + [self setShowsToolbarButton:[[aState objectForKey:kStateShowsToolbarButton] boolValue]]; + [self setCollectionBehavior:[[aState objectForKey:kStateCollectionBehavior] unsignedIntValue]]; +} + +- (NSMutableDictionary*)exportState +{ + NSMutableDictionary* state = [NSMutableDictionary dictionaryWithCapacity:10]; + [state setObject:[self title] forKey:kStateTitleKey]; + [state setObject:[NSNumber numberWithBool:[self drawsContentsIntoWindowFrame]] + forKey:kStateDrawsContentsIntoWindowFrameKey]; + NSColor* activeTitlebarColor = [self titlebarColorForActiveWindow:YES]; + if (activeTitlebarColor) { + [state setObject:activeTitlebarColor forKey:kStateActiveTitlebarColorKey]; + } + NSColor* inactiveTitlebarColor = [self titlebarColorForActiveWindow:NO]; + if (inactiveTitlebarColor) { + [state setObject:inactiveTitlebarColor forKey:kStateInactiveTitlebarColorKey]; + } + [state setObject:[NSNumber numberWithBool:[self showsToolbarButton]] + forKey:kStateShowsToolbarButton]; + [state setObject:[NSNumber numberWithUnsignedInt: [self collectionBehavior]] + forKey:kStateCollectionBehavior]; + return state; +} + +- (void)setDrawsContentsIntoWindowFrame:(BOOL)aState +{ + bool changed = (aState != mDrawsIntoWindowFrame); + mDrawsIntoWindowFrame = aState; + if (changed) { + [self updateContentViewSize]; + [self reflowTitlebarElements]; + } +} + +- (BOOL)drawsContentsIntoWindowFrame +{ + return mDrawsIntoWindowFrame; +} + +- (void)setWantsTitleDrawn:(BOOL)aDrawTitle +{ + mDrawTitle = aDrawTitle; +} + +- (BOOL)wantsTitleDrawn +{ + return mDrawTitle; +} + +- (void)setUseBrightTitlebarForeground:(BOOL)aBrightForeground +{ + mBrightTitlebarForeground = aBrightForeground; + [[self standardWindowButton:NSWindowFullScreenButton] setNeedsDisplay:YES]; +} + +- (BOOL)useBrightTitlebarForeground +{ + return mBrightTitlebarForeground; +} + +// Pass nil here to get the default appearance. +- (void)setTitlebarColor:(NSColor*)aColor forActiveWindow:(BOOL)aActive +{ + [aColor retain]; + if (aActive) { + [mActiveTitlebarColor release]; + mActiveTitlebarColor = aColor; + } else { + [mInactiveTitlebarColor release]; + mInactiveTitlebarColor = aColor; + } +} + +- (NSColor*)titlebarColorForActiveWindow:(BOOL)aActive +{ + return aActive ? mActiveTitlebarColor : mInactiveTitlebarColor; +} + +- (void)deferredInvalidateShadow +{ + if (mScheduledShadowInvalidation || [self isOpaque] || ![self hasShadow]) + return; + + [self performSelector:@selector(invalidateShadow) withObject:nil afterDelay:0]; + mScheduledShadowInvalidation = YES; +} + +- (void)invalidateShadow +{ + [super invalidateShadow]; + mScheduledShadowInvalidation = NO; +} + +- (float)getDPI +{ + return mDPI; +} + +- (NSView*)trackingAreaView +{ + NSView* contentView = [self contentView]; + return [contentView superview] ? [contentView superview] : contentView; +} + +- (ChildView*)mainChildView +{ + NSView *contentView = [self contentView]; + NSView* lastView = [[contentView subviews] lastObject]; + if ([lastView isKindOfClass:[ChildView class]]) { + return (ChildView*)lastView; + } + return nil; +} + +- (void)removeTrackingArea +{ + if (mTrackingArea) { + [[self trackingAreaView] removeTrackingArea:mTrackingArea]; + [mTrackingArea release]; + mTrackingArea = nil; + } +} + +- (void)updateTrackingArea +{ + [self removeTrackingArea]; + + NSView* view = [self trackingAreaView]; + const NSTrackingAreaOptions options = + NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways; + mTrackingArea = [[NSTrackingArea alloc] initWithRect:[view bounds] + options:options + owner:self + userInfo:nil]; + [view addTrackingArea:mTrackingArea]; +} + +- (void)mouseEntered:(NSEvent*)aEvent +{ + ChildViewMouseTracker::MouseEnteredWindow(aEvent); +} + +- (void)mouseExited:(NSEvent*)aEvent +{ + ChildViewMouseTracker::MouseExitedWindow(aEvent); +} + +- (void)mouseMoved:(NSEvent*)aEvent +{ + ChildViewMouseTracker::MouseMoved(aEvent); +} + +- (void)cursorUpdated:(NSEvent*)aEvent +{ + // Nothing to do here, but NSTrackingArea wants us to implement this method. +} + +- (void)_setNeedsDisplayInRect:(NSRect)aRect +{ + // Prevent unnecessary invalidations due to moving NSViews (e.g. for plugins) + if (!mDisabledNeedsDisplay) { + // This method is only called by Cocoa, so when we're here, we know that + // it's available and don't need to check whether our superclass responds + // to the selector. + [super _setNeedsDisplayInRect:aRect]; + mDirtyRect = NSUnionRect(mDirtyRect, aRect); + } +} + +- (NSRect)getAndResetNativeDirtyRect +{ + NSRect dirtyRect = mDirtyRect; + mDirtyRect = NSZeroRect; + return dirtyRect; +} + +- (void)updateContentViewSize +{ + NSRect rect = [self contentRectForFrameRect:[self frame]]; + [[self contentView] setFrameSize:rect.size]; +} + +// Possibly move the titlebar buttons. +- (void)reflowTitlebarElements +{ + NSView *frameView = [[self contentView] superview]; + if ([frameView respondsToSelector:@selector(_tileTitlebarAndRedisplay:)]) { + [frameView _tileTitlebarAndRedisplay:NO]; + } +} + +// Override methods that translate between content rect and frame rect. +- (NSRect)contentRectForFrameRect:(NSRect)aRect +{ + if ([self drawsContentsIntoWindowFrame]) { + return aRect; + } + return [super contentRectForFrameRect:aRect]; +} + +- (NSRect)contentRectForFrameRect:(NSRect)aRect styleMask:(NSUInteger)aMask +{ + if ([self drawsContentsIntoWindowFrame]) { + return aRect; + } + if ([super respondsToSelector:@selector(contentRectForFrameRect:styleMask:)]) { + return [super contentRectForFrameRect:aRect styleMask:aMask]; + } else { + return [NSWindow contentRectForFrameRect:aRect styleMask:aMask]; + } +} + +- (NSRect)frameRectForContentRect:(NSRect)aRect +{ + if ([self drawsContentsIntoWindowFrame]) { + return aRect; + } + return [super frameRectForContentRect:aRect]; +} + +- (NSRect)frameRectForContentRect:(NSRect)aRect styleMask:(NSUInteger)aMask +{ + if ([self drawsContentsIntoWindowFrame]) { + return aRect; + } + if ([super respondsToSelector:@selector(frameRectForContentRect:styleMask:)]) { + return [super frameRectForContentRect:aRect styleMask:aMask]; + } else { + return [NSWindow frameRectForContentRect:aRect styleMask:aMask]; + } +} + +- (void)setContentView:(NSView*)aView +{ + [super setContentView:aView]; + + // Now move the contentView to the bottommost layer so that it's guaranteed + // to be under the window buttons. + NSView* frameView = [aView superview]; + [aView removeFromSuperview]; + if ([frameView respondsToSelector:@selector(_addKnownSubview:positioned:relativeTo:)]) { + // 10.10 prints a warning when we call addSubview on the frame view, so we + // silence the warning by calling a private method instead. + [frameView _addKnownSubview:aView positioned:NSWindowBelow relativeTo:nil]; + } else { + [frameView addSubview:aView positioned:NSWindowBelow relativeTo:nil]; + } +} + +- (NSArray*)titlebarControls +{ + // Return all subviews of the frameView which are not the content view. + NSView* frameView = [[self contentView] superview]; + NSMutableArray* array = [[[frameView subviews] mutableCopy] autorelease]; + [array removeObject:[self contentView]]; + return array; +} + +- (BOOL)respondsToSelector:(SEL)aSelector +{ + // Claim the window doesn't respond to this so that the system + // doesn't steal keyboard equivalents for it. Bug 613710. + if (aSelector == @selector(cancelOperation:)) { + return NO; + } + + return [super respondsToSelector:aSelector]; +} + +- (void) doCommandBySelector:(SEL)aSelector +{ + // We override this so that it won't beep if it can't act. + // We want to control the beeping for missing or disabled + // commands ourselves. + [self tryToPerform:aSelector with:nil]; +} + +- (id)accessibilityAttributeValue:(NSString *)attribute +{ + id retval = [super accessibilityAttributeValue:attribute]; + + // The following works around a problem with Text-to-Speech on OS X 10.7. + // See bug 674612 for more info. + // + // When accessibility is off, AXUIElementCopyAttributeValue(), when called + // on an AXApplication object to get its AXFocusedUIElement attribute, + // always returns an AXWindow object (the actual browser window -- never a + // mozAccessible object). This also happens with accessibility turned on, + // if no other object in the browser window has yet been focused. But if + // the browser window has a title bar (as it currently always does), the + // AXWindow object will always have four "accessible" children, one of which + // is an AXStaticText object (the title bar's "title"; the other three are + // the close, minimize and zoom buttons). This means that (for complicated + // reasons, for which see bug 674612) Text-to-Speech on OS X 10.7 will often + // "speak" the window title, no matter what text is selected, or even if no + // text at all is selected. (This always happens when accessibility is off. + // It doesn't happen in Firefox releases because Apple has (on OS X 10.7) + // special-cased the handling of apps whose CFBundleIdentifier is + // org.mozilla.firefox.) + // + // We work around this problem by only returning AXChildren that are + // mozAccessible object or are one of the titlebar's buttons (which + // instantiate subclasses of NSButtonCell). + if ([retval isKindOfClass:[NSArray class]] && + [attribute isEqualToString:@"AXChildren"]) { + NSMutableArray *holder = [NSMutableArray arrayWithCapacity:10]; + [holder addObjectsFromArray:(NSArray *)retval]; + NSUInteger count = [holder count]; + for (NSInteger i = count - 1; i >= 0; --i) { + id item = [holder objectAtIndex:i]; + // Remove anything from holder that isn't one of the titlebar's buttons + // (which instantiate subclasses of NSButtonCell) or a mozAccessible + // object (or one of its subclasses). + if (![item isKindOfClass:[NSButtonCell class]] && + ![item respondsToSelector:@selector(hasRepresentedView)]) { + [holder removeObjectAtIndex:i]; + } + } + retval = [NSArray arrayWithArray:holder]; + } + + return retval; +} + +@end + +// This class allows us to exercise control over the window's title bar. This +// allows for a "unified toolbar" look without having to extend the content +// area into the title bar. It works like this: +// 1) We set the window's style to textured. +// 2) Because of this, the background color applies to the entire window, including +// the titlebar area. For normal textured windows, the default pattern is a +// "brushed metal" image on Tiger and a unified gradient on Leopard. +// 3) We set the background color to a custom NSColor subclass that knows how tall the window is. +// When -set is called on it, it sets a pattern (with a draw callback) as the fill. In that callback, +// it paints the the titlebar and background colors in the correct areas of the context it's given, +// which will fill the entire window (CG will tile it horizontally for us). +// 4) Whenever the window's main state changes and when [window display] is called, +// Cocoa redraws the titlebar using the patternDraw callback function. +// +// This class also provides us with a pill button to show/hide the toolbar up to 10.6. +// +// Drawing the unified gradient in the titlebar and the toolbar works like this: +// 1) In the style sheet we set the toolbar's -moz-appearance to toolbar. +// 2) When the toolbar is visible and we paint the application chrome +// window, the array that Gecko passes nsChildView::UpdateThemeGeometries +// will contain an entry for the widget type NS_THEME_TOOLBAR. +// 3) nsChildView::UpdateThemeGeometries finds the toolbar frame's ToolbarWindow +// and passes the toolbar frame's height to setUnifiedToolbarHeight. +// 4) If the toolbar height has changed, a titlebar redraw is triggered and the +// upper part of the unified gradient is drawn in the titlebar. +// 5) The lower part of the unified gradient in the toolbar is drawn during +// normal window content painting in nsNativeThemeCocoa::DrawUnifiedToolbar. +// +// Whenever the unified gradient is drawn in the titlebar or the toolbar, both +// titlebar height and toolbar height must be known in order to construct the +// correct gradient. But you can only get from the toolbar frame +// to the containing window - the other direction doesn't work. That's why the +// toolbar height is cached in the ToolbarWindow but nsNativeThemeCocoa can simply +// query the window for its titlebar height when drawing the toolbar. +// +// Note that in drawsContentsIntoWindowFrame mode, titlebar drawing works in a +// completely different way: In that mode, the window's mainChildView will +// cover the titlebar completely and nothing that happens in the window +// background will reach the screen. +@implementation ToolbarWindow + +- (id)initWithContentRect:(NSRect)aContentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)aBufferingType defer:(BOOL)aFlag +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + aStyle = aStyle | NSTexturedBackgroundWindowMask; + if ((self = [super initWithContentRect:aContentRect styleMask:aStyle backing:aBufferingType defer:aFlag])) { + mColor = [[TitlebarAndBackgroundColor alloc] initWithWindow:self]; + // Bypass our guard method below. + [super setBackgroundColor:mColor]; + mBackgroundColor = [[NSColor whiteColor] retain]; + + mUnifiedToolbarHeight = 22.0f; + mWindowButtonsRect = NSZeroRect; + mFullScreenButtonRect = NSZeroRect; + + // setBottomCornerRounded: is a private API call, so we check to make sure + // we respond to it just in case. + if ([self respondsToSelector:@selector(setBottomCornerRounded:)]) + [self setBottomCornerRounded:YES]; + + [self setAutorecalculatesContentBorderThickness:NO forEdge:NSMaxYEdge]; + [self setContentBorderThickness:0.0f forEdge:NSMaxYEdge]; + } + return self; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (void)dealloc +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + [super setBackgroundColor:[NSColor whiteColor]]; + [mColor release]; + [mBackgroundColor release]; + [mTitlebarView release]; + [super dealloc]; + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +- (void)setTitlebarColor:(NSColor*)aColor forActiveWindow:(BOOL)aActive +{ + [super setTitlebarColor:aColor forActiveWindow:aActive]; + [self setTitlebarNeedsDisplayInRect:[self titlebarRect]]; +} + +- (void)setBackgroundColor:(NSColor*)aColor +{ + [aColor retain]; + [mBackgroundColor release]; + mBackgroundColor = aColor; +} + +- (NSColor*)windowBackgroundColor +{ + return mBackgroundColor; +} + +- (void)setTemporaryBackgroundColor +{ + [super setBackgroundColor:[NSColor whiteColor]]; +} + +- (void)restoreBackgroundColor +{ + [super setBackgroundColor:mBackgroundColor]; +} + +- (void)setTitlebarNeedsDisplayInRect:(NSRect)aRect +{ + [self setTitlebarNeedsDisplayInRect:aRect sync:NO]; +} + +- (void)setTitlebarNeedsDisplayInRect:(NSRect)aRect sync:(BOOL)aSync +{ + NSRect titlebarRect = [self titlebarRect]; + NSRect rect = NSIntersectionRect(titlebarRect, aRect); + if (NSIsEmptyRect(rect)) + return; + + NSView* borderView = [[self contentView] superview]; + if (!borderView) + return; + + if (aSync) { + [borderView displayRect:rect]; + } else { + [borderView setNeedsDisplayInRect:rect]; + } +} + +- (NSRect)titlebarRect +{ + CGFloat titlebarHeight = [self titlebarHeight]; + return NSMakeRect(0, [self frame].size.height - titlebarHeight, + [self frame].size.width, titlebarHeight); +} + +// Returns the unified height of titlebar + toolbar. +- (CGFloat)unifiedToolbarHeight +{ + return mUnifiedToolbarHeight; +} + +- (CGFloat)titlebarHeight +{ + // We use the original content rect here, not what we return from + // [self contentRectForFrameRect:], because that would give us a + // titlebarHeight of zero in drawsContentsIntoWindowFrame mode. + NSRect frameRect = [self frame]; + NSRect originalContentRect = [NSWindow contentRectForFrameRect:frameRect styleMask:[self styleMask]]; + return NSMaxY(frameRect) - NSMaxY(originalContentRect); +} + +// Stores the complete height of titlebar + toolbar. +- (void)setUnifiedToolbarHeight:(CGFloat)aHeight +{ + if (aHeight == mUnifiedToolbarHeight) + return; + + mUnifiedToolbarHeight = aHeight; + + if (![self drawsContentsIntoWindowFrame]) { + // Redraw the title bar. If we're inside painting, we'll do it right now, + // otherwise we'll just invalidate it. + BOOL needSyncRedraw = ([NSView focusView] != nil); + [self setTitlebarNeedsDisplayInRect:[self titlebarRect] sync:needSyncRedraw]; + } +} + +// Extending the content area into the title bar works by resizing the +// mainChildView so that it covers the titlebar. +- (void)setDrawsContentsIntoWindowFrame:(BOOL)aState +{ + BOOL stateChanged = ([self drawsContentsIntoWindowFrame] != aState); + [super setDrawsContentsIntoWindowFrame:aState]; + if (stateChanged && [[self delegate] isKindOfClass:[WindowDelegate class]]) { + // Here we extend / shrink our mainChildView. We do that by firing a resize + // event which will cause the ChildView to be resized to the rect returned + // by nsCocoaWindow::GetClientBounds. GetClientBounds bases its return + // value on what we return from drawsContentsIntoWindowFrame. + WindowDelegate *windowDelegate = (WindowDelegate *)[self delegate]; + nsCocoaWindow *geckoWindow = [windowDelegate geckoWidget]; + if (geckoWindow) { + // Re-layout our contents. + geckoWindow->ReportSizeEvent(); + } + + // Resizing the content area causes a reflow which would send a synthesized + // mousemove event to the old mouse position relative to the top left + // corner of the content area. But the mouse has shifted relative to the + // content area, so that event would have wrong position information. So + // we'll send a mouse move event with the correct new position. + ChildViewMouseTracker::ResendLastMouseMoveEvent(); + } +} + +- (void)setWantsTitleDrawn:(BOOL)aDrawTitle +{ + [super setWantsTitleDrawn:aDrawTitle]; + [self setTitlebarNeedsDisplayInRect:[self titlebarRect]]; +} + +- (void)setSheetAttachmentPosition:(CGFloat)aY +{ + CGFloat topMargin = aY - [self titlebarHeight]; + [self setContentBorderThickness:topMargin forEdge:NSMaxYEdge]; +} + +- (void)placeWindowButtons:(NSRect)aRect +{ + if (!NSEqualRects(mWindowButtonsRect, aRect)) { + mWindowButtonsRect = aRect; + [self reflowTitlebarElements]; + } +} + +- (NSPoint)windowButtonsPositionWithDefaultPosition:(NSPoint)aDefaultPosition +{ + NSInteger styleMask = [self styleMask]; + if ([self drawsContentsIntoWindowFrame] && + !(styleMask & NSFullScreenWindowMask) && (styleMask & NSTitledWindowMask)) { + if (NSIsEmptyRect(mWindowButtonsRect)) { + // Empty rect. Let's hide the buttons. + // Position is in non-flipped window coordinates. Using frame's height + // for the vertical coordinate will move the buttons above the window, + // making them invisible. + return NSMakePoint(0, [self frame].size.height); + } + return NSMakePoint(mWindowButtonsRect.origin.x, mWindowButtonsRect.origin.y); + } + return aDefaultPosition; +} + +- (void)placeFullScreenButton:(NSRect)aRect +{ + if (!NSEqualRects(mFullScreenButtonRect, aRect)) { + mFullScreenButtonRect = aRect; + [self reflowTitlebarElements]; + } +} + +- (NSPoint)fullScreenButtonPositionWithDefaultPosition:(NSPoint)aDefaultPosition +{ + if ([self drawsContentsIntoWindowFrame] && !NSIsEmptyRect(mFullScreenButtonRect)) { + return NSMakePoint(std::min(mFullScreenButtonRect.origin.x, aDefaultPosition.x), + std::min(mFullScreenButtonRect.origin.y, aDefaultPosition.y)); + } + return aDefaultPosition; +} + +// Returning YES here makes the setShowsToolbarButton method work even though +// the window doesn't contain an NSToolbar. +- (BOOL)_hasToolbar +{ + return YES; +} + +// Dispatch a toolbar pill button clicked message to Gecko. +- (void)_toolbarPillButtonClicked:(id)sender +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + + RollUpPopups(); + + if ([[self delegate] isKindOfClass:[WindowDelegate class]]) { + WindowDelegate *windowDelegate = (WindowDelegate *)[self delegate]; + nsCocoaWindow *geckoWindow = [windowDelegate geckoWidget]; + if (!geckoWindow) + return; + + nsIWidgetListener* listener = geckoWindow->GetWidgetListener(); + if (listener) + listener->OSToolbarButtonPressed(); + } + + NS_OBJC_END_TRY_ABORT_BLOCK; +} + +// Retain and release "self" to avoid crashes when our widget (and its native +// window) is closed as a result of processing a key equivalent (e.g. +// Command+w or Command+q). This workaround is only needed for a window +// that can become key. +- (BOOL)performKeyEquivalent:(NSEvent*)theEvent +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + NSWindow *nativeWindow = [self retain]; + BOOL retval = [super performKeyEquivalent:theEvent]; + [nativeWindow release]; + return retval; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO); +} + +- (void)sendEvent:(NSEvent *)anEvent +{ + NSEventType type = [anEvent type]; + + switch (type) { + case NSScrollWheel: + case NSLeftMouseDown: + case NSLeftMouseUp: + case NSRightMouseDown: + case NSRightMouseUp: + case NSOtherMouseDown: + case NSOtherMouseUp: + case NSMouseMoved: + case NSLeftMouseDragged: + case NSRightMouseDragged: + case NSOtherMouseDragged: + { + // Drop all mouse events if a modal window has appeared above us. + // This helps make us behave as if the OS were running a "real" modal + // event loop. + id delegate = [self delegate]; + if (delegate && [delegate isKindOfClass:[WindowDelegate class]]) { + nsCocoaWindow *widget = [(WindowDelegate *)delegate geckoWidget]; + if (widget) { + if (gGeckoAppModalWindowList && (widget != gGeckoAppModalWindowList->window)) + return; + if (widget->HasModalDescendents()) + return; + } + } + break; + } + default: + break; + } + + [super sendEvent:anEvent]; +} + +@end + +// Custom NSColor subclass where most of the work takes place for drawing in +// the titlebar area. Not used in drawsContentsIntoWindowFrame mode. +@implementation TitlebarAndBackgroundColor + +- (id)initWithWindow:(ToolbarWindow*)aWindow +{ + if ((self = [super init])) { + mWindow = aWindow; // weak ref to avoid a cycle + } + return self; +} + +static void +DrawNativeTitlebar(CGContextRef aContext, CGRect aTitlebarRect, + CGFloat aUnifiedToolbarHeight, BOOL aIsMain) +{ + nsNativeThemeCocoa::DrawNativeTitlebar(aContext, aTitlebarRect, aUnifiedToolbarHeight, aIsMain, NO); + + // The call to CUIDraw doesn't draw the top pixel strip at some window widths. + // We don't want to have a flickering transparent line, so we overdraw it. + CGContextSetRGBFillColor(aContext, 0.95, 0.95, 0.95, 1); + CGContextFillRect(aContext, CGRectMake(0, CGRectGetMaxY(aTitlebarRect) - 1, + aTitlebarRect.size.width, 1)); +} + +// Pattern draw callback for standard titlebar gradients and solid titlebar colors +static void +TitlebarDrawCallback(void* aInfo, CGContextRef aContext) +{ + ToolbarWindow *window = (ToolbarWindow*)aInfo; + if (![window drawsContentsIntoWindowFrame]) { + NSRect titlebarRect = [window titlebarRect]; + BOOL isMain = [window isMainWindow]; + NSColor *titlebarColor = [window titlebarColorForActiveWindow:isMain]; + if (!titlebarColor) { + // If the titlebar color is nil, draw the default titlebar shading. + DrawNativeTitlebar(aContext, NSRectToCGRect(titlebarRect), + [window unifiedToolbarHeight], isMain); + } else { + // If the titlebar color is not nil, just set and draw it normally. + [NSGraphicsContext saveGraphicsState]; + [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:aContext flipped:NO]]; + [titlebarColor set]; + NSRectFill(titlebarRect); + [NSGraphicsContext restoreGraphicsState]; + } + } +} + +- (void)setFill +{ + float patternWidth = [mWindow frame].size.width; + + CGPatternCallbacks callbacks = {0, &TitlebarDrawCallback, NULL}; + CGPatternRef pattern = CGPatternCreate(mWindow, CGRectMake(0.0f, 0.0f, patternWidth, [mWindow frame].size.height), + CGAffineTransformIdentity, patternWidth, [mWindow frame].size.height, + kCGPatternTilingConstantSpacing, true, &callbacks); + + // Set the pattern as the fill, which is what we were asked to do. All our + // drawing will take place in the patternDraw callback. + CGColorSpaceRef patternSpace = CGColorSpaceCreatePattern(NULL); + CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; + CGContextSetFillColorSpace(context, patternSpace); + CGColorSpaceRelease(patternSpace); + CGFloat component = 1.0f; + CGContextSetFillPattern(context, pattern, &component); + CGPatternRelease(pattern); +} + +- (void)set +{ + [self setFill]; +} + +- (NSString*)colorSpaceName +{ + return NSDeviceRGBColorSpace; +} + +@end + +@implementation PopupWindow + +- (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)styleMask + backing:(NSBackingStoreType)bufferingType defer:(BOOL)deferCreation +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + + mIsContextMenu = false; + return [super initWithContentRect:contentRect styleMask:styleMask + backing:bufferingType defer:deferCreation]; + + NS_OBJC_END_TRY_ABORT_BLOCK_NIL; +} + +- (BOOL)isContextMenu +{ + return mIsContextMenu; +} + +- (void)setIsContextMenu:(BOOL)flag +{ + mIsContextMenu = flag; +} + +- (BOOL)canBecomeMainWindow +{ + // This is overriden because the default is 'yes' when a titlebar is present. + return NO; +} + +@end + +// According to Apple's docs on [NSWindow canBecomeKeyWindow] and [NSWindow +// canBecomeMainWindow], windows without a title bar or resize bar can't (by +// default) become key or main. But if a window can't become key, it can't +// accept keyboard input (bmo bug 393250). And it should also be possible for +// an otherwise "ordinary" window to become main. We need to override these +// two methods to make this happen. +@implementation BorderlessWindow + +- (BOOL)canBecomeKeyWindow +{ + return YES; +} + +- (void)sendEvent:(NSEvent *)anEvent +{ + NSEventType type = [anEvent type]; + + switch (type) { + case NSScrollWheel: + case NSLeftMouseDown: + case NSLeftMouseUp: + case NSRightMouseDown: + case NSRightMouseUp: + case NSOtherMouseDown: + case NSOtherMouseUp: + case NSMouseMoved: + case NSLeftMouseDragged: + case NSRightMouseDragged: + case NSOtherMouseDragged: + { + // Drop all mouse events if a modal window has appeared above us. + // This helps make us behave as if the OS were running a "real" modal + // event loop. + id delegate = [self delegate]; + if (delegate && [delegate isKindOfClass:[WindowDelegate class]]) { + nsCocoaWindow *widget = [(WindowDelegate *)delegate geckoWidget]; + if (widget) { + if (gGeckoAppModalWindowList && (widget != gGeckoAppModalWindowList->window)) + return; + if (widget->HasModalDescendents()) + return; + } + } + break; + } + default: + break; + } + + [super sendEvent:anEvent]; +} + +// Apple's doc on this method says that the NSWindow class's default is not to +// become main if the window isn't "visible" -- so we should replicate that +// behavior here. As best I can tell, the [NSWindow isVisible] method is an +// accurate test of what Apple means by "visibility". +- (BOOL)canBecomeMainWindow +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + if (![self isVisible]) + return NO; + return YES; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO); +} + +// Retain and release "self" to avoid crashes when our widget (and its native +// window) is closed as a result of processing a key equivalent (e.g. +// Command+w or Command+q). This workaround is only needed for a window +// that can become key. +- (BOOL)performKeyEquivalent:(NSEvent*)theEvent +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + + NSWindow *nativeWindow = [self retain]; + BOOL retval = [super performKeyEquivalent:theEvent]; + [nativeWindow release]; + return retval; + + NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO); +} + +@end |