blob: 8b09756b1733f617964ab3bcb6949dfd330f8945 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "chrome/browser/ui/cocoa/browser_window_fullscreen_transition.h"
#include <QuartzCore/QuartzCore.h>
#include <memory>
#include "base/mac/bind_objc_block.h"
#include "base/mac/foundation_util.h"
#include "base/mac/mac_util.h"
#import "base/mac/sdk_forward_declarations.h"
#include "base/macros.h"
#import "chrome/browser/ui/cocoa/browser_window_controller.h"
#import "chrome/browser/ui/cocoa/framed_browser_window.h"
#import "chrome/browser/ui/cocoa/tabs/tab_strip_background_view.h"
namespace {
NSString* const kPrimaryWindowAnimationID = @"PrimaryWindowAnimationID";
NSString* const kSnapshotWindowAnimationID = @"SnapshotWindowAnimationID";
NSString* const kAnimationIDKey = @"AnimationIDKey";
// The fraction of the duration from AppKit's startCustomAnimation methods
// that we want our animation to run in. Yosemite's fraction is smaller
// since its fullscreen transition is significantly slower.
CGFloat const kAnimationDurationFraction = 0.5;
CGFloat const kAnimationDurationFractionYosemite = 0.3;
// This class has two simultaneous animations to resize and reposition layers.
// These animations must use the same timing function, otherwise there will be
// visual discordance.
NSString* TransformAnimationTimingFunction() {
return kCAMediaTimingFunctionEaseInEaseOut;
}
// This class locks and unlocks the FrameBrowserWindow. Its destructor ensures
// that the lock gets released.
class FrameAndStyleLock {
public:
explicit FrameAndStyleLock(FramedBrowserWindow* window) : window_(window) {}
~FrameAndStyleLock() { set_lock(NO); }
void set_lock(bool lock) { [window_ setStyleMaskLock:lock]; }
private:
FramedBrowserWindow* window_; // weak
DISALLOW_COPY_AND_ASSIGN(FrameAndStyleLock);
};
} // namespace
// This view draws a dummy toolbar over the resized content view during
// the exit fullscreen animation. It is removed at the end of the animation.
@interface FullscreenTabStripBackgroundView : NSView {
base::scoped_nsobject<NSColor> windowBackgroundColor_;
}
- (instancetype)initWithFrame:(NSRect)frame background:(NSColor*)color;
@end
@implementation FullscreenTabStripBackgroundView
- (instancetype)initWithFrame:(NSRect)frame background:(NSColor*)color {
if ((self = [super initWithFrame:frame])) {
windowBackgroundColor_.reset([color copy]);
}
return self;
}
// Override this method so that we can paint the toolbar in this view.
// This method first fill itself with the toolbar's background. After that,
// it will paint the window's theme if applicable.
- (void)drawRect:(NSRect)frame {
[windowBackgroundColor_ set];
NSRectFillUsingOperation(frame, NSCompositeDestinationOver);
[FramedBrowserWindow drawWindowThemeInDirtyRect:frame
forView:self
bounds:[self bounds]
forceBlackBackground:NO];
}
@end
@interface BrowserWindowFullscreenTransition ()
<CAAnimationDelegate, CALayerDelegate> {
// Flag to keep track of whether we are entering or exiting fullscreen.
BOOL isEnteringFullscreen_;
// The window which is undergoing the fullscreen transition.
base::scoped_nsobject<FramedBrowserWindow> primaryWindow_;
// The window which is undergoing the fullscreen transition.
BrowserWindowController* controller_; // weak
// A layer that holds a snapshot of the original state of |primaryWindow_|.
base::scoped_nsobject<CALayer> snapshotLayer_;
// A temporary window that holds |snapshotLayer_|.
base::scoped_nsobject<NSWindow> snapshotWindow_;
// The tabstrip background view in the window. During the exit fullscreen
// animation, this view be hidden while a dummy tabstrip background will be
// drawn over the content view.
base::scoped_nsobject<NSView> tabStripBackgroundView_;
// The background color of |primaryWindow_| before the transition began.
base::scoped_nsobject<NSColor> primaryWindowInitialBackgroundColor_;
// Whether |primaryWindow_| was opaque before the transition began.
BOOL primaryWindowInitialOpaque_;
// The initial anchor point of the root layer.
CGPoint initialRootAnchorPoint_;
// The initial origin of the content view.
NSPoint initialContentViewOrigin_;
// Whether the instance is in the process of changing the size of
// |primaryWindow_|.
BOOL changingPrimaryWindowSize_;
// The frame of the |primaryWindow_| before it starts the transition.
NSRect initialFrame_;
// The frame that |primaryWindow_| is expected to have after the transition
// is finished.
NSRect finalFrame_;
// This view draws the tabstrip background during the exit animation.
base::scoped_nsobject<FullscreenTabStripBackgroundView>
fullscreenTabStripBackgroundView_;
// Locks and unlocks the FullSizeContentWindow.
std::unique_ptr<FrameAndStyleLock> lock_;
// Flag that indicates if the animation was completed. Sets to true at the
// end of the animation.
BOOL completedTransition_;
}
// Takes a snapshot of |primaryWindow_| and puts it in |snapshotLayer_|.
- (void)takeSnapshot;
// Creates |snapshotWindow_| and adds |snapshotLayer_| to it.
- (void)makeAndPrepareSnapshotWindow;
// This method has several effects on |primaryWindow_|:
// - Saves current state.
// - Makes window transparent, with clear background.
// - If we are entering fullscreen, it will also:
// - Add NSFullScreenWindowMask style mask.
// - Set the size to the screen's size.
- (void)preparePrimaryWindowForAnimation;
// Applies the fullscreen animation to |snapshotLayer_|.
- (void)animateSnapshotWindowWithDuration:(CGFloat)duration;
// Sets |primaryWindow_|'s frame to the expected frame.
- (void)changePrimaryWindowToFinalFrame;
// Overrides of CAAnimation delegate methods.
- (void)animationDidStart:(CAAnimation*)theAnimation;
- (void)animationDidStop:(CAAnimation*)theAnimation finished:(BOOL)finished;
// Returns the layer of the root view of |window|.
- (CALayer*)rootLayerOfWindow:(NSWindow*)window;
// Convert the point to be relative to the screen the primary window is on.
// This is important because if we're using multiple screens, the coordinate
// system extends to the second screen.
//
// For example, if the screen width is 1440, the second screen's frame origin
// is located at (1440, 0) and any x coordinate on the second screen will be
// >= 1440. If we move a window on the first screen to the same location on
// second screen, the window's frame origin will change from (x, y) to
// (x + 1440, y).
//
// When we animate the window, we want to use (x, y), the coordinates that are
// relative to the second screen. As a result, we use this method to convert a
// NSPoint so that it's relative to the screen it's on.
- (NSPoint)pointRelativeToCurrentScreen:(NSPoint)point;
@end
@implementation BrowserWindowFullscreenTransition
// -------------------------Public Methods----------------------------
- (instancetype)initEnterWithController:(BrowserWindowController*)controller {
DCHECK(controller);
DCHECK([self rootLayerOfWindow:[controller window]]);
if ((self = [super init])) {
controller_ = controller;
FramedBrowserWindow* framedBrowserWindow =
base::mac::ObjCCast<FramedBrowserWindow>([controller window]);
primaryWindow_.reset([framedBrowserWindow retain]);
isEnteringFullscreen_ = YES;
initialFrame_ = [primaryWindow_ frame];
finalFrame_ = [[primaryWindow_ screen] frame];
}
return self;
}
- (instancetype)initExitWithController:(BrowserWindowController*)controller {
DCHECK(controller);
DCHECK([self rootLayerOfWindow:[controller window]]);
if ((self = [super init])) {
controller_ = controller;
FramedBrowserWindow* framedBrowserWindow =
base::mac::ObjCCast<FramedBrowserWindow>([controller window]);
primaryWindow_.reset([framedBrowserWindow retain]);
isEnteringFullscreen_ = NO;
initialFrame_ = [[primaryWindow_ screen] frame];
finalFrame_ = [controller savedRegularWindowFrame];
tabStripBackgroundView_.reset([[controller tabStripBackgroundView] retain]);
lock_.reset(new FrameAndStyleLock(framedBrowserWindow));
}
return self;
}
- (NSArray*)customWindowsForFullScreenTransition {
[self takeSnapshot];
[self makeAndPrepareSnapshotWindow];
return @[ primaryWindow_.get(), snapshotWindow_.get() ];
}
- (BOOL)isTransitionCompleted {
return completedTransition_;
}
- (void)startCustomFullScreenAnimationWithDuration:(NSTimeInterval)duration {
CGFloat durationFraction = base::mac::IsOS10_10()
? kAnimationDurationFractionYosemite
: kAnimationDurationFraction;
CGFloat animationDuration = duration * durationFraction;
[self preparePrimaryWindowForAnimation];
[self animatePrimaryWindowWithDuration:animationDuration];
[self animateSnapshotWindowWithDuration:animationDuration];
}
- (BOOL)shouldWindowBeUnconstrained {
return changingPrimaryWindowSize_;
}
- (NSSize)desiredWindowLayoutSize {
return isEnteringFullscreen_ ? [primaryWindow_ frame].size
: [[primaryWindow_ contentView] bounds].size;
}
- (void)browserWillBeDestroyed {
CALayer* root = [self rootLayerOfWindow:primaryWindow_];
[root removeAllAnimations];
[snapshotLayer_ removeAllAnimations];
controller_ = nil;
}
// -------------------------Private Methods----------------------------
- (void)takeSnapshot {
base::ScopedCFTypeRef<CGImageRef> windowSnapshot(CGWindowListCreateImage(
CGRectNull, kCGWindowListOptionIncludingWindow,
[primaryWindow_ windowNumber], kCGWindowImageBoundsIgnoreFraming));
snapshotLayer_.reset([[CALayer alloc] init]);
[snapshotLayer_ setFrame:NSRectToCGRect([primaryWindow_ frame])];
[snapshotLayer_ setContents:static_cast<id>(windowSnapshot.get())];
[snapshotLayer_ setAnchorPoint:CGPointMake(0, 0)];
CGColorRef colorRef = CGColorCreateGenericRGB(0, 0, 0, 0);
[snapshotLayer_ setBackgroundColor:colorRef];
CGColorRelease(colorRef);
}
- (void)makeAndPrepareSnapshotWindow {
DCHECK(snapshotLayer_);
snapshotWindow_.reset([[NSWindow alloc]
initWithContentRect:[[primaryWindow_ screen] frame]
styleMask:0
backing:NSBackingStoreBuffered
defer:NO]);
[[snapshotWindow_ contentView] setWantsLayer:YES];
[snapshotWindow_ setOpaque:NO];
[snapshotWindow_ setBackgroundColor:[NSColor clearColor]];
[snapshotWindow_ setAnimationBehavior:NSWindowAnimationBehaviorNone];
[[[snapshotWindow_ contentView] layer] addSublayer:snapshotLayer_];
// Compute the frame of the snapshot layer such that the snapshot is
// positioned exactly on top of the original position of |primaryWindow_|.
NSRect snapshotLayerFrame =
[snapshotWindow_ convertRectFromScreen:[primaryWindow_ frame]];
[snapshotLayer_ setFrame:snapshotLayerFrame];
// If the primary window is in fullscreen mode, we can't move the snapshot
// window in front of it. As a result, at the beginning of the transition to
// exit fullscreen, we should order the snapshot window to the front ASAP.
if (isEnteringFullscreen_)
[snapshotWindow_ orderFront:nil];
}
- (void)preparePrimaryWindowForAnimation {
// Save the initial state of the primary window.
primaryWindowInitialBackgroundColor_.reset(
[[primaryWindow_ backgroundColor] copy]);
primaryWindowInitialOpaque_ = [primaryWindow_ isOpaque];
// Make |primaryWindow_| invisible. This must happen before the window is
// resized, since resizing the window will call drawRect: and cause content
// to flash over the entire screen.
[primaryWindow_ setOpaque:NO];
if (isEnteringFullscreen_) {
// As soon as the style mask includes the flag NSFullScreenWindowMask, the
// window is expected to receive fullscreen layout. This must be set before
// the window is resized, as that causes a relayout.
CALayer* root = [self rootLayerOfWindow:primaryWindow_];
root.opacity = 0;
[primaryWindow_
setStyleMask:[primaryWindow_ styleMask] | NSFullScreenWindowMask];
[self changePrimaryWindowToFinalFrame];
} else {
[snapshotWindow_ orderFront:nil];
NSView* contentView = [primaryWindow_ contentView];
NSView* rootView = [contentView superview];
// Since only the content view is resized, the window's background
// must be transparent. This is a hack that forces the layer to remove
// the textured background and replace it with clearColor.
[rootView setWantsLayer:NO];
[primaryWindow_ setBackgroundColor:[NSColor clearColor]];
[primaryWindow_ setStyleMask:[primaryWindow_ styleMask] &
~NSTexturedBackgroundWindowMask];
[rootView setWantsLayer:YES];
CALayer* root = [self rootLayerOfWindow:primaryWindow_];
root.opacity = 0;
// Right before the animation begins, change the contentView size to the
// expected size at the end of the animation. Afterwards, lock the
// |primaryWindow_| so that AppKit will not be able to make unwanted
// changes to it during the animation.
initialContentViewOrigin_ = [[primaryWindow_ contentView] frame].origin;
initialRootAnchorPoint_ = root.anchorPoint;
NSPoint contentViewOrigin =
[self pointRelativeToCurrentScreen:finalFrame_.origin];
NSRect relativeContentFinalFrame =
NSMakeRect(contentViewOrigin.x, contentViewOrigin.y,
finalFrame_.size.width, finalFrame_.size.height);
[[primaryWindow_ contentView] setFrame:relativeContentFinalFrame];
fullscreenTabStripBackgroundView_.reset(
[[FullscreenTabStripBackgroundView alloc]
initWithFrame:finalFrame_
background:primaryWindowInitialBackgroundColor_]);
[fullscreenTabStripBackgroundView_ setFrameOrigin:NSZeroPoint];
[contentView addSubview:fullscreenTabStripBackgroundView_.get()
positioned:NSWindowBelow
relativeTo:nil];
[tabStripBackgroundView_ setHidden:YES];
// Set anchor point to be the center of the content view
CGFloat anchorPointX =
NSMidX(relativeContentFinalFrame) / NSWidth(initialFrame_);
CGFloat anchorPointY =
NSMidY(relativeContentFinalFrame) / NSHeight(initialFrame_);
root.anchorPoint = CGPointMake(anchorPointX, anchorPointY);
lock_->set_lock(YES);
}
}
- (void)animateSnapshotWindowWithDuration:(CGFloat)duration {
// Calculate the frame so that it's relative to the screen.
NSRect finalFrameRelativeToScreen =
[snapshotWindow_ convertRectFromScreen:finalFrame_];
// Move the snapshot layer until it's bottom-left corner is at the the
// bottom-left corner of the expected frame.
CABasicAnimation* positionAnimation =
[CABasicAnimation animationWithKeyPath:@"position"];
positionAnimation.toValue =
[NSValue valueWithPoint:finalFrameRelativeToScreen.origin];
positionAnimation.timingFunction = [CAMediaTimingFunction
functionWithName:TransformAnimationTimingFunction()];
// Resize the bounds until it reaches the expected size at the end of the
// animation.
NSRect finalBounds =
NSMakeRect(0, 0, NSWidth(finalFrame_), NSHeight(finalFrame_));
CABasicAnimation* boundsAnimation =
[CABasicAnimation animationWithKeyPath:@"bounds"];
boundsAnimation.toValue = [NSValue valueWithRect:finalBounds];
boundsAnimation.timingFunction = [CAMediaTimingFunction
functionWithName:TransformAnimationTimingFunction()];
// Fade out the snapshot layer.
CABasicAnimation* opacityAnimation =
[CABasicAnimation animationWithKeyPath:@"opacity"];
opacityAnimation.toValue = @(0.0);
opacityAnimation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
// Fill forwards, and don't remove the animation. When the animation
// completes, the entire window will be removed.
CAAnimationGroup* group = [CAAnimationGroup animation];
group.removedOnCompletion = NO;
group.fillMode = kCAFillModeForwards;
group.animations = @[ positionAnimation, boundsAnimation, opacityAnimation ];
group.duration = duration;
[group setValue:kSnapshotWindowAnimationID forKey:kAnimationIDKey];
group.delegate = self;
[snapshotLayer_ addAnimation:group forKey:nil];
}
- (void)animatePrimaryWindowWithDuration:(CGFloat)duration {
// As soon as the window's root layer is scaled down, the opacity should be
// set back to 1. There are a couple of ways to do this. The easiest is to
// just have a dummy animation as part of the same animation group.
CABasicAnimation* opacityAnimation =
[CABasicAnimation animationWithKeyPath:@"opacity"];
opacityAnimation.fromValue = @(1.0);
opacityAnimation.toValue = @(1.0);
// The root layer's size should start scaled down to the initial size of
// |primaryWindow_|. The animation increases the size until the root layer
// fills the screen.
NSRect initialFrame = initialFrame_;
NSRect endFrame = finalFrame_;
CGFloat xScale = NSWidth(initialFrame) / NSWidth(endFrame);
CGFloat yScale = NSHeight(initialFrame) / NSHeight(endFrame);
CATransform3D initial = CATransform3DMakeScale(xScale, yScale, 1);
CABasicAnimation* transformAnimation =
[CABasicAnimation animationWithKeyPath:@"transform"];
transformAnimation.fromValue = [NSValue valueWithCATransform3D:initial];
// Animate the primary window from its initial position, the center of the
// initial window.
CABasicAnimation* positionAnimation =
[CABasicAnimation animationWithKeyPath:@"position"];
NSPoint centerOfInitialFrame =
NSMakePoint(NSMidX(initialFrame), NSMidY(initialFrame));
NSPoint startingLayerPoint =
[self pointRelativeToCurrentScreen:centerOfInitialFrame];
positionAnimation.fromValue = [NSValue valueWithPoint:startingLayerPoint];
NSPoint endingLayerPoint =
[self pointRelativeToCurrentScreen:NSMakePoint(NSMidX(endFrame),
NSMidY(endFrame))];
positionAnimation.toValue = [NSValue valueWithPoint:endingLayerPoint];
CAAnimationGroup* group = [CAAnimationGroup animation];
group.removedOnCompletion = NO;
group.fillMode = kCAFillModeForwards;
group.animations =
@[ opacityAnimation, positionAnimation, transformAnimation ];
group.timingFunction = [CAMediaTimingFunction
functionWithName:TransformAnimationTimingFunction()];
group.duration = duration;
[group setValue:kPrimaryWindowAnimationID forKey:kAnimationIDKey];
group.delegate = self;
CALayer* root = [self rootLayerOfWindow:primaryWindow_];
[root addAnimation:group forKey:kPrimaryWindowAnimationID];
}
- (void)changePrimaryWindowToFinalFrame {
changingPrimaryWindowSize_ = YES;
[primaryWindow_ setFrame:finalFrame_ display:NO];
changingPrimaryWindowSize_ = NO;
}
- (void)animationDidStart:(CAAnimation*)theAnimation {
// CAAnimationDelegate method added on OSX 10.12.
}
- (void)animationDidStop:(CAAnimation*)theAnimation finished:(BOOL)finished {
NSString* animationID = [theAnimation valueForKey:kAnimationIDKey];
// Remove the snapshot window.
if ([animationID isEqual:kSnapshotWindowAnimationID]) {
[snapshotWindow_ orderOut:nil];
snapshotWindow_.reset();
snapshotLayer_.reset();
return;
}
if ([animationID isEqual:kPrimaryWindowAnimationID]) {
// If we're exiting full screen, we want to set the |primaryWindow_|'s
// frame to the expected frame at the end of the animation. The window's
// lock must also be released.
if (!isEnteringFullscreen_) {
lock_->set_lock(NO);
CALayer* root = [self rootLayerOfWindow:primaryWindow_];
root.anchorPoint = initialRootAnchorPoint_;
NSUInteger styleMask =
([primaryWindow_ styleMask] & ~NSFullScreenWindowMask) |
NSTexturedBackgroundWindowMask;
[primaryWindow_ setStyleMask:styleMask];
NSView* content = [primaryWindow_ contentView];
[content setFrameOrigin:initialContentViewOrigin_];
[self changePrimaryWindowToFinalFrame];
[tabStripBackgroundView_ setHidden:NO];
[fullscreenTabStripBackgroundView_ removeFromSuperview];
}
// Check if the contentView size is correct.
// TODO (spqchan): Currently, a popup window will fail this check since
// AppKit will shift the contentView up right after the window is resized.
// This will create a small janky movement at the end of the animation.
NSSize expectedSize = finalFrame_.size;
NSView* content = [primaryWindow_ contentView];
DCHECK_EQ(NSHeight(content.frame), expectedSize.height);
DCHECK_EQ(NSWidth(content.frame), expectedSize.width);
// Restore the state of the primary window and make it visible again.
[primaryWindow_ setOpaque:primaryWindowInitialOpaque_];
[primaryWindow_ setBackgroundColor:primaryWindowInitialBackgroundColor_];
CALayer* root = [self rootLayerOfWindow:primaryWindow_];
[root removeAnimationForKey:kPrimaryWindowAnimationID];
root.opacity = 1;
completedTransition_ = YES;
if (!isEnteringFullscreen_)
[controller_ exitFullscreenAnimationFinished];
}
}
- (CALayer*)rootLayerOfWindow:(NSWindow*)window {
return [[[window contentView] superview] layer];
}
- (NSPoint)pointRelativeToCurrentScreen:(NSPoint)point {
NSRect screenFrame = [[primaryWindow_ screen] frame];
return NSMakePoint(point.x - screenFrame.origin.x,
point.y - screenFrame.origin.y);
}
@end