diff --git a/widget/cocoa/nsCocoaWindow.h b/widget/cocoa/nsCocoaWindow.h index 0eaf2ed65e7e..2f02932e8974 100644 --- a/widget/cocoa/nsCocoaWindow.h +++ b/widget/cocoa/nsCocoaWindow.h @@ -16,6 +16,7 @@ #include "nsCocoaUtils.h" #include "nsTouchBar.h" #include +#include class nsCocoaWindow; class nsChildView; @@ -262,8 +263,6 @@ class nsCocoaWindow final : public nsBaseWidget, public nsPIWidgetCocoa { virtual void SuppressAnimation(bool aSuppress) override; virtual void HideWindowChrome(bool aShouldHide) override; - void WillEnterFullScreen(bool aFullScreen); - void EnteredFullScreen(bool aFullScreen, bool aNativeMode = true); virtual bool PrepareForFullscreenTransition(nsISupports** aData) override; virtual void PerformFullscreenTransition(FullscreenTransitionStage aStage, uint16_t aDuration, nsISupports* aData, nsIRunnable* aCallback) override; @@ -362,6 +361,25 @@ class nsCocoaWindow final : public nsBaseWidget, public nsPIWidgetCocoa { const ScrollableLayerGuid& aGuid) override; void StopAsyncAutoscroll(const ScrollableLayerGuid& aGuid) override; + // Class method versions of NSWindow/Delegate callbacks which need to + // access object state. + void CocoaWindowWillEnterFullscreen(bool aFullscreen); + void CocoaWindowDidFailFullscreen(bool aAttemptedFullscreen); + void CocoaWindowDidResize(); + void CocoaSendToplevelActivateEvents(); + void CocoaSendToplevelDeactivateEvents(); + + enum class TransitionType { + Windowed, + Fullscreen, + EmulatedFullscreen, + Miniaturize, + Deminiaturize, + Zoom, + }; + void FinishCurrentTransition(); + void FinishCurrentTransitionIfMatching(const TransitionType& aTransition); + protected: virtual ~nsCocoaWindow(); @@ -375,7 +393,6 @@ class nsCocoaWindow final : public nsBaseWidget, public nsPIWidgetCocoa { void DoResize(double aX, double aY, double aWidth, double aHeight, bool aRepaint, bool aConstrainToCurrentScreen); - inline bool ShouldToggleNativeFullscreen(bool aFullScreen, bool aUseSystemTransition); void UpdateFullscreenState(bool aFullScreen, bool aNativeMode); nsresult DoMakeFullScreen(bool aFullScreen, bool aUseSystemTransition); @@ -405,8 +422,33 @@ class nsCocoaWindow final : public nsBaseWidget, public nsPIWidgetCocoa { // this is used for sibling sheet contention only nsSizeMode mSizeMode; bool mInFullScreenMode; - bool mInFullScreenTransition; // true from the request to enter/exit fullscreen - // (MakeFullScreen() call) to EnteredFullScreen() + // Whether we are currently using native fullscreen. It could be false because + // we are in the emulated fullscreen where we do not use the native fullscreen. + bool mInNativeFullScreenMode; + + mozilla::Maybe mTransitionCurrent; + std::queue mTransitionsPending; + + // Sometimes we add a transition that wasn't requested by a caller. We do this + // to manage transitions between states that otherwise would be rejected by + // Cocoa. When we do this, it's useful to know when we are handling an added + // transition because we don't want to send size mode events when they execute. + bool mIsTransitionCurrentAdded = false; + + // Whether we are treating the next resize as the start of a fullscreen transition. + // If we are, which direction are we going: Fullscreen or Windowed. + mozilla::Maybe mUpdateFullscreenOnResize; + + bool IsInTransition() { return mTransitionCurrent.isSome(); } + void QueueTransition(const TransitionType& aTransition); + void ProcessTransitions(); + + bool mInProcessTransitions = false; + + // While running an emulated fullscreen transition, we want to suppress sending + // size mode events due to window resizing. We fix it up at the end when the + // transition is complete. + bool mSuppressSizeModeEvents = false; // Ignore occlusion events caused by displaying the temporary fullscreen // window during the fullscreen transition animation because only focused @@ -416,10 +458,6 @@ class nsCocoaWindow final : public nsBaseWidget, public nsPIWidgetCocoa { bool mModal; bool mFakeModal; - // Whether we are currently using native fullscreen. It could be false because - // we are in the DOM fullscreen where we do not use the native fullscreen. - bool mInNativeFullScreenMode; - bool mIsAnimationSuppressed; bool mInReportMoveEvent; // true if in a call to ReportMoveEvent(). diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm index 2ad88567f923..89265be4a0e3 100644 --- a/widget/cocoa/nsCocoaWindow.mm +++ b/widget/cocoa/nsCocoaWindow.mm @@ -139,11 +139,10 @@ nsCocoaWindow::nsCocoaWindow() mSheetNeedsShow(false), mSizeMode(nsSizeMode_Normal), mInFullScreenMode(false), - mInFullScreenTransition(false), + mInNativeFullScreenMode(false), mIgnoreOcclusionCount(0), mModal(false), mFakeModal(false), - mInNativeFullScreenMode(false), mIsAnimationSuppressed(false), mInReportMoveEvent(false), mInResize(false), @@ -1256,32 +1255,15 @@ void nsCocoaWindow::Move(double aX, double aY) { } void nsCocoaWindow::SetSizeMode(nsSizeMode aMode) { - NS_OBJC_BEGIN_TRY_IGNORE_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 (previousMode == nsSizeMode_Fullscreen) - MakeFullScreen(false); + QueueTransition(TransitionType::Windowed); } else if (aMode == nsSizeMode_Minimized) { - if (![mWindow isMiniaturized]) [mWindow miniaturize:nil]; + QueueTransition(TransitionType::Miniaturize); } else if (aMode == nsSizeMode_Maximized) { - if ([mWindow isMiniaturized]) [mWindow deminiaturize:nil]; - if (![mWindow isZoomed]) [mWindow zoom:nil]; + QueueTransition(TransitionType::Zoom); } else if (aMode == nsSizeMode_Fullscreen) { - if (!mInFullScreenMode) MakeFullScreen(true); + MakeFullScreen(true); } - - NS_OBJC_END_TRY_IGNORE_BLOCK; } // The (work)space switching implementation below was inspired by Phoenix: @@ -1639,18 +1621,33 @@ static bool AlwaysUsesNativeFullScreen() { [mFullscreenTransitionAnimation startAnimation]; } -void nsCocoaWindow::WillEnterFullScreen(bool aFullScreen) { +void nsCocoaWindow::CocoaWindowWillEnterFullscreen(bool aFullscreen) { + MOZ_ASSERT(mUpdateFullscreenOnResize.isNothing()); + + // Ensure that we update our fullscreen state as early as possible, when the resize + // happens. + mUpdateFullscreenOnResize = + Some(aFullscreen ? TransitionType::Fullscreen : TransitionType::Windowed); + if (mWidgetListener) { - mWidgetListener->FullscreenWillChange(aFullScreen); + mWidgetListener->FullscreenWillChange(aFullscreen); } - // Update the state to full screen when we are entering, so that we switch to - // full screen view as soon as possible. - UpdateFullscreenState(aFullScreen, true); } -void nsCocoaWindow::EnteredFullScreen(bool aFullScreen, bool aNativeMode) { - mInFullScreenTransition = false; - UpdateFullscreenState(aFullScreen, aNativeMode); +void nsCocoaWindow::CocoaWindowDidFailFullscreen(bool aAttemptedFullscreen) { + if (mWidgetListener) { + mWidgetListener->FullscreenWillChange(!aAttemptedFullscreen); + } + + // If we already updated our fullscreen state due to a resize, we need to update it again. + if (mUpdateFullscreenOnResize.isNothing()) { + UpdateFullscreenState(!aAttemptedFullscreen, true); + ReportSizeEvent(); + } + + TransitionType transition = + aAttemptedFullscreen ? TransitionType::Fullscreen : TransitionType::Windowed; + FinishCurrentTransitionIfMatching(transition); } void nsCocoaWindow::UpdateFullscreenState(bool aFullScreen, bool aNativeMode) { @@ -1659,8 +1656,14 @@ void nsCocoaWindow::UpdateFullscreenState(bool aFullScreen, bool aNativeMode) { if (aNativeMode || mInNativeFullScreenMode) { mInNativeFullScreenMode = aFullScreen; } + + if (aFullScreen == wasInFullscreen) { + return; + } + DispatchSizeModeEvent(); - if (mWidgetListener && wasInFullscreen != aFullScreen) { + + if (mWidgetListener) { mWidgetListener->FullscreenChanged(aFullScreen); } @@ -1671,29 +1674,6 @@ void nsCocoaWindow::UpdateFullscreenState(bool aFullScreen, bool aNativeMode) { } } -inline bool nsCocoaWindow::ShouldToggleNativeFullscreen(bool aFullScreen, - bool aUseSystemTransition) { - // First check if this window supports entering native fullscreen. - // This is set based on the macnativefullscreen attribute on the window's - // document element. - NSWindowCollectionBehavior colBehavior = [mWindow collectionBehavior]; - if (!(colBehavior & NSWindowCollectionBehaviorFullScreenPrimary)) { - 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) { return DoMakeFullScreen(aFullScreen, AlwaysUsesNativeFullScreen()); } @@ -1703,45 +1683,204 @@ nsresult nsCocoaWindow::MakeFullScreenWithNativeTransition(bool aFullScreen) { } nsresult nsCocoaWindow::DoMakeFullScreen(bool aFullScreen, bool aUseSystemTransition) { - NS_OBJC_BEGIN_TRY_BLOCK_RETURN; - 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)) { - 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 { - if (mWidgetListener) { - mWidgetListener->FullscreenWillChange(aFullScreen); - } - 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); + // Figure out what type of transition is being requested. + TransitionType transition = TransitionType::Windowed; + if (aFullScreen) { + // Decide whether to use fullscreen or emulated fullscreen. + transition = (aUseSystemTransition && + (mWindow.collectionBehavior & NSWindowCollectionBehaviorFullScreenPrimary)) + ? TransitionType::Fullscreen + : TransitionType::EmulatedFullscreen; } + QueueTransition(transition); return NS_OK; +} - NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); +void nsCocoaWindow::QueueTransition(const TransitionType& aTransition) { + mTransitionsPending.push(aTransition); + ProcessTransitions(); +} + +void nsCocoaWindow::ProcessTransitions() { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK + + if (mInProcessTransitions) { + return; + } + + mInProcessTransitions = true; + + // Start a loop that will continue as long as we have transitions to process + // and we aren't waiting on an asynchronous transition to complete. Any + // transition that starts something async will `continue` this loop to exit. + while (!mTransitionsPending.empty() && !IsInTransition()) { + TransitionType nextTransition = mTransitionsPending.front(); + + // We have to check for some incompatible transition states, and if we find one, + // instead perform an alternative transition and leave the queue untouched. If + // we add one of these transitions, we set mIsTransitionCurrentAdded because we + // don't want to confuse listeners who are expecting to receive exactly one + // event when the requested transition has completed. + switch (nextTransition) { + case TransitionType::Fullscreen: + case TransitionType::EmulatedFullscreen: + case TransitionType::Windowed: + case TransitionType::Zoom: + // These can't handle miniaturized windows, so deminiaturize first. + if (mWindow.miniaturized) { + mTransitionCurrent = Some(TransitionType::Deminiaturize); + mIsTransitionCurrentAdded = true; + } + break; + case TransitionType::Miniaturize: + // This can't handle fullscreen, so go to windowed first. + if (mInFullScreenMode) { + mTransitionCurrent = Some(TransitionType::Windowed); + mIsTransitionCurrentAdded = true; + } + break; + default: + break; + } + + // If mTransitionCurrent is still empty, then we use the nextTransition and pop + // the queue. + if (mTransitionCurrent.isNothing()) { + mTransitionCurrent = Some(nextTransition); + mTransitionsPending.pop(); + } + + switch (*mTransitionCurrent) { + case TransitionType::Fullscreen: { + if (!mInFullScreenMode) { + // This triggers an async animation, so continue. + [mWindow toggleFullScreen:nil]; + continue; + } + break; + } + + case TransitionType::EmulatedFullscreen: { + if (!mInFullScreenMode) { + // This can be done synchronously. + if (mWidgetListener) { + mWidgetListener->FullscreenWillChange(true); + } + NSDisableScreenUpdates(); + mSuppressSizeModeEvents = true; + // 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(true); + nsBaseWidget::InfallibleMakeFullScreen(true); + mSuppressSizeModeEvents = false; + NSEnableScreenUpdates(); + UpdateFullscreenState(true, false); + } + break; + } + + case TransitionType::Windowed: { + if (mInFullScreenMode) { + if (mInNativeFullScreenMode) { + // This triggers an async animation, so continue. + [mWindow toggleFullScreen:nil]; + continue; + } else { + // This can be done synchronously. + if (mWidgetListener) { + mWidgetListener->FullscreenWillChange(false); + } + NSDisableScreenUpdates(); + mSuppressSizeModeEvents = true; + // 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(false); + nsBaseWidget::InfallibleMakeFullScreen(false); + mSuppressSizeModeEvents = false; + NSEnableScreenUpdates(); + UpdateFullscreenState(false, false); + } + } else if (mWindow.zoomed) { + [mWindow zoom:nil]; + } + break; + } + + case TransitionType::Miniaturize: + if (!mWindow.miniaturized) { + // This triggers an async animation, so continue. + [mWindow miniaturize:nil]; + continue; + } + break; + + case TransitionType::Deminiaturize: + if (mWindow.miniaturized) { + // This triggers an async animation, so continue. + [mWindow deminiaturize:nil]; + continue; + } + break; + + case TransitionType::Zoom: + if (!mWindow.zoomed) { + [mWindow zoom:nil]; + } + break; + + default: + break; + } + + mTransitionCurrent.reset(); + mIsTransitionCurrentAdded = false; + } + + mInProcessTransitions = false; + + // When we finish processing transitions, dispatch a size mode event to cover the + // cases where an inserted transition suppressed one, and the original transition + // never sent one because it detected it was at the desired state when it ran. If + // we've already sent a size mode event, then this will be a no-op. + if (!IsInTransition()) { + DispatchSizeModeEvent(); + } + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +void nsCocoaWindow::FinishCurrentTransition() { + mTransitionCurrent.reset(); + mIsTransitionCurrentAdded = false; + ProcessTransitions(); +} + +void nsCocoaWindow::FinishCurrentTransitionIfMatching(const TransitionType& aTransition) { + // We've just finished some transition activity, and we're not sure whether it was + // triggered programmatically, or by the user. If it matches our current transition, + // then assume it was triggered programmatically and we can clean up that transition + // and start processing transitions again. + + // Whether programmatic or user-initiated, we send out a size mode event. + DispatchSizeModeEvent(); + + if (mTransitionCurrent.isSome() && (*mTransitionCurrent == aTransition)) { + // This matches our current transition. Since this function is called from + // nsWindowDelegate transition callbacks, we want to make sure those callbacks are + // all the way done before we start processing more transitions. To accomplish this, + // we dispatch our cleanup to happen on the next event loop. Doing this will ensure + // that any async native transition methods we call (like toggleFullscreen) will + // succeed. + NS_DispatchToCurrentThread(NewRunnableMethod("FinishCurrentTransition", this, + &nsCocoaWindow::FinishCurrentTransition)); + } } // Coordinates are desktop pixels @@ -2096,12 +2235,12 @@ void nsCocoaWindow::DispatchSizeModeEvent() { return; } - nsSizeMode newMode = GetWindowSizeMode(mWindow, mInFullScreenMode); + if (mSuppressSizeModeEvents || mIsTransitionCurrentAdded) { + return; + } - // 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) { + nsSizeMode newMode = GetWindowSizeMode(mWindow, mInFullScreenMode); + if (mSizeMode == newMode) { return; } @@ -2138,7 +2277,6 @@ void nsCocoaWindow::ReportSizeEvent() { NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; UpdateBounds(); - if (mWidgetListener) { LayoutDeviceIntRect innerBounds = GetClientBounds(); mWidgetListener->WindowResized(this, innerBounds.width, innerBounds.height); @@ -2665,19 +2803,50 @@ already_AddRefed nsIWidget::CreateChildWindow() { - (NSSize)windowWillResize:(NSWindow*)sender toSize:(NSSize)proposedFrameSize { RollUpPopups(); - return proposedFrameSize; } +void nsCocoaWindow::CocoaSendToplevelActivateEvents() { + if (mWidgetListener) { + mWidgetListener->WindowActivated(); + } +} + +void nsCocoaWindow::CocoaSendToplevelDeactivateEvents() { + if (mWidgetListener) { + mWidgetListener->WindowDeactivated(); + } +} + +void nsCocoaWindow::CocoaWindowDidResize() { + // It's important to update our bounds before we trigger any listeners. This + // ensures that our bounds are correct when GetScreenBounds is called. + UpdateBounds(); + + if (mUpdateFullscreenOnResize.isSome()) { + // Act as if the native fullscreen transition is complete, doing everything other + // than actually clearing the transition state, which will happen when one of the + // appropriate windowDid delegate methods is called. + bool toFullscreen = (*mUpdateFullscreenOnResize == TransitionType::Fullscreen); + mUpdateFullscreenOnResize.reset(); + + UpdateFullscreenState(toFullscreen, true); + ReportSizeEvent(); + return; + } + + // Resizing might have changed our zoom state. + DispatchSizeModeEvent(); + ReportSizeEvent(); +} + - (void)windowDidResize:(NSNotification*)aNotification { BaseWindow* window = [aNotification object]; [window updateTrackingArea]; if (!mGeckoWindow) return; - // Resizing might have changed our zoom state. - mGeckoWindow->DispatchSizeModeEvent(); - mGeckoWindow->ReportSizeEvent(); + mGeckoWindow->CocoaWindowDidResize(); } - (void)windowDidChangeScreen:(NSNotification*)aNotification { @@ -2714,20 +2883,13 @@ already_AddRefed nsIWidget::CreateChildWindow() { if (!mGeckoWindow) { return; } - - mGeckoWindow->WillEnterFullScreen(true); + mGeckoWindow->CocoaWindowWillEnterFullscreen(true); } // 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 @@ -2750,38 +2912,40 @@ already_AddRefed nsIWidget::CreateChildWindow() { if ([titlebarContainerView respondsToSelector:@selector(setTransparent:)]) { [titlebarContainerView setTransparent:NO]; } + + if (!mGeckoWindow) { + return; + } + + mGeckoWindow->FinishCurrentTransitionIfMatching(nsCocoaWindow::TransitionType::Fullscreen); } - (void)windowWillExitFullScreen:(NSNotification*)notification { if (!mGeckoWindow) { return; } - - mGeckoWindow->WillEnterFullScreen(false); + mGeckoWindow->CocoaWindowWillEnterFullscreen(false); } - (void)windowDidExitFullScreen:(NSNotification*)notification { if (!mGeckoWindow) { return; } - - mGeckoWindow->EnteredFullScreen(false); + mGeckoWindow->FinishCurrentTransitionIfMatching(nsCocoaWindow::TransitionType::Windowed); } - (void)windowDidFailToEnterFullScreen:(NSWindow*)window { if (!mGeckoWindow) { return; } - - mGeckoWindow->EnteredFullScreen(false); + mGeckoWindow->CocoaWindowDidFailFullscreen(true); } - (void)windowDidFailToExitFullScreen:(NSWindow*)window { if (!mGeckoWindow) { return; } - - mGeckoWindow->EnteredFullScreen(true); + mGeckoWindow->CocoaWindowDidFailFullscreen(false); } - (void)windowDidBecomeMain:(NSNotification*)aNotification { @@ -2883,11 +3047,17 @@ already_AddRefed nsIWidget::CreateChildWindow() { } - (void)windowDidMiniaturize:(NSNotification*)aNotification { - if (mGeckoWindow) mGeckoWindow->DispatchSizeModeEvent(); + if (!mGeckoWindow) { + return; + } + mGeckoWindow->FinishCurrentTransitionIfMatching(nsCocoaWindow::TransitionType::Miniaturize); } - (void)windowDidDeminiaturize:(NSNotification*)aNotification { - if (mGeckoWindow) mGeckoWindow->DispatchSizeModeEvent(); + if (!mGeckoWindow) { + return; + } + mGeckoWindow->FinishCurrentTransitionIfMatching(nsCocoaWindow::TransitionType::Deminiaturize); } - (BOOL)windowShouldZoom:(NSWindow*)window toFrame:(NSRect)proposedFrame { @@ -2956,20 +3126,15 @@ already_AddRefed nsIWidget::CreateChildWindow() { - (void)sendToplevelActivateEvents { if (!mToplevelActiveState && mGeckoWindow) { - nsIWidgetListener* listener = mGeckoWindow->GetWidgetListener(); - if (listener) { - listener->WindowActivated(); - } + mGeckoWindow->CocoaSendToplevelActivateEvents(); + mToplevelActiveState = true; } } - (void)sendToplevelDeactivateEvents { if (mToplevelActiveState && mGeckoWindow) { - nsIWidgetListener* listener = mGeckoWindow->GetWidgetListener(); - if (listener) { - listener->WindowDeactivated(); - } + mGeckoWindow->CocoaSendToplevelDeactivateEvents(); mToplevelActiveState = false; } }