blob: 2067c7a1c4117cf07b3bb6a6bb437b7cfcd0ea81 [file] [log] [blame]
// Copyright (c) 2012 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/tab_contents/tab_contents_controller.h"
#include <stdint.h>
#include <utility>
#include "base/feature_list.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/mac/scoped_nsobject.h"
#include "base/macros.h"
#include "chrome/browser/devtools/devtools_window.h"
#import "chrome/browser/themes/theme_properties.h"
#import "chrome/browser/themes/theme_service.h"
#include "chrome/browser/ui/cocoa/fullscreen_placeholder_view.h"
#include "chrome/browser/ui/cocoa/separate_fullscreen_window.h"
#import "chrome/browser/ui/cocoa/themed_window.h"
#import "chrome/browser/ui/cocoa/touchbar/web_textfield_touch_bar_controller.h"
#include "chrome/browser/ui/exclusive_access/fullscreen_within_tab_helper.h"
#include "chrome/browser/ui/view_ids.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/theme_resources.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "skia/ext/skia_utils_mac.h"
#include "ui/base/cocoa/animation_utils.h"
#import "ui/base/cocoa/touch_bar_forward_declarations.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/scrollbar_size.h"
using content::WebContents;
using content::WebContentsObserver;
// FullscreenObserver is used by TabContentsController to monitor for the
// showing/destruction of fullscreen render widgets. When notified,
// TabContentsController will alter its child view hierarchy to either embed a
// fullscreen render widget view or restore the normal WebContentsView render
// view. The embedded fullscreen render widget will fill the user's screen in
// the case where TabContentsController's NSView is a subview of a browser
// window that has been toggled into fullscreen mode (e.g., via
// FullscreenController).
class FullscreenObserver : public WebContentsObserver {
public:
explicit FullscreenObserver(TabContentsController* controller)
: controller_(controller) {}
void Observe(content::WebContents* new_web_contents) {
WebContentsObserver::Observe(new_web_contents);
}
WebContents* web_contents() const {
return WebContentsObserver::web_contents();
}
void DidShowFullscreenWidget() override {
[controller_ toggleFullscreenWidget:YES];
}
void DidDestroyFullscreenWidget() override {
[controller_ toggleFullscreenWidget:NO];
}
void DidToggleFullscreenModeForTab(bool entered_fullscreen,
bool will_cause_resize) override {
[controller_ toggleFullscreenWidget:entered_fullscreen];
}
private:
TabContentsController* const controller_;
DISALLOW_COPY_AND_ASSIGN(FullscreenObserver);
};
@interface TabContentsController (TabContentsContainerViewDelegate)
- (BOOL)contentsInFullscreenCaptureMode;
// Computes and returns the frame to use for the contents view using the size of
// |container| as the target size.
- (NSRect)frameForContentsViewIn:(NSView*)container;
// Returns YES if the content view should be resized.
- (BOOL)shouldResizeContentView;
// Returns YES if the content view is inside a popup.
- (BOOL)isPopup;
@end
// An NSView with special-case handling for when the contents view does not
// expand to fill the entire tab contents area. See 'AutoEmbedFullscreen mode'
// in header file comments.
@interface TabContentsContainerView : NSView {
@private
TabContentsController* delegate_; // weak
}
- (void)updateBackgroundColorFromWindowTheme:(NSWindow*)window;
@end
@implementation TabContentsContainerView
- (id)initWithDelegate:(TabContentsController*)delegate {
if ((self = [super initWithFrame:NSZeroRect])) {
delegate_ = delegate;
ScopedCAActionDisabler disabler;
base::scoped_nsobject<CALayer> layer([[CALayer alloc] init]);
[self setLayer:layer];
[self setWantsLayer:YES];
}
return self;
}
// Called by the delegate during dealloc to invalidate the pointer held by this
// view.
- (void)delegateDestroyed {
delegate_ = nil;
}
// Override auto-resizing logic to query the delegate for the exact frame to
// use for the contents view.
// TODO(spqchan): The popup check is a temporary solution to fix the regression
// issue described in crbug.com/604288. This method doesn't really affect
// fullscreen if the content is inside a normal browser window, but would
// cause a flash fullscreen widget to blow up if it's inside a popup.
- (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize {
NSView* const contentsView =
[[self subviews] count] > 0 ? [[self subviews] objectAtIndex:0] : nil;
if (!contentsView || [contentsView autoresizingMask] == NSViewNotSizable ||
!delegate_ ||
(![delegate_ shouldResizeContentView] && [delegate_ isPopup])) {
return;
}
ScopedCAActionDisabler disabler;
[contentsView setFrame:[delegate_ frameForContentsViewIn:self]];
}
// Update the background layer's color whenever the view needs to repaint.
- (void)setNeedsDisplayInRect:(NSRect)rect {
[super setNeedsDisplayInRect:rect];
[self updateBackgroundColorFromWindowTheme:[self window]];
}
- (void)updateBackgroundColorFromWindowTheme:(NSWindow*)window {
// This view is sometimes flashed into visibility (e.g, when closing
// windows or opening new tabs), so ensure that the flash be the theme
// background color in those cases.
const ThemeProvider* theme = [window themeProvider];
if (!theme)
return;
SkColor skBackgroundColor =
theme->GetColor(ThemeProperties::COLOR_NTP_BACKGROUND);
// If the page is in fullscreen tab capture mode, change the background color
// to be a dark tint of the new tab page's background color.
if ([delegate_ contentsInFullscreenCaptureMode]) {
const int kBackgroundDivisor = 5;
skBackgroundColor =
SkColorSetARGB(SkColorGetA(skBackgroundColor),
SkColorGetR(skBackgroundColor) / kBackgroundDivisor,
SkColorGetG(skBackgroundColor) / kBackgroundDivisor,
SkColorGetB(skBackgroundColor) / kBackgroundDivisor);
}
ScopedCAActionDisabler disabler;
base::ScopedCFTypeRef<CGColorRef> cgBackgroundColor(
skia::CGColorCreateFromSkColor(skBackgroundColor));
[[self layer] setBackgroundColor:cgBackgroundColor];
}
- (void)viewWillMoveToWindow:(NSWindow*)newWindow {
[self updateBackgroundColorFromWindowTheme:newWindow];
}
- (ViewID)viewID {
return VIEW_ID_TAB_CONTAINER;
}
- (BOOL)acceptsFirstResponder {
return [[self subviews] count] > 0 &&
[[[self subviews] objectAtIndex:0] acceptsFirstResponder];
}
// When receiving a click-to-focus in the solid color area surrounding the
// WebContents' native view, immediately transfer focus to WebContents' native
// view.
- (BOOL)becomeFirstResponder {
if (![self acceptsFirstResponder])
return NO;
return [[self window] makeFirstResponder:[[self subviews] objectAtIndex:0]];
}
- (BOOL)canBecomeKeyView {
return NO; // Tab/Shift-Tab should focus the subview, not this view.
}
@end // @implementation TabContentsContainerView
@interface TabContentsController (
SeparateFullscreenWindowDelegate)<NSWindowDelegate>
- (NSView*)createScreenshotView;
- (NSWindow*)createSeparateWindowForTab:(content::WebContents*)separatedTab;
@end
@implementation TabContentsController
@synthesize webContents = contents_;
@synthesize blockFullscreenResize = blockFullscreenResize_;
- (id)initWithContents:(WebContents*)contents isPopup:(BOOL)popup {
if ((self = [super initWithNibName:nil bundle:nil])) {
fullscreenObserver_.reset(new FullscreenObserver(self));
[self changeWebContents:contents];
isPopup_ = popup;
touchBarController_.reset([[WebTextfieldTouchBarController alloc]
initWithTabContentsController:self]);
}
return self;
}
- (void)dealloc {
[static_cast<TabContentsContainerView*>([self view]) delegateDestroyed];
// Make sure the contents view has been removed from the container view to
// allow objects to be released.
[[self view] removeFromSuperview];
[super dealloc];
}
- (void)loadView {
base::scoped_nsobject<NSView> view(
[[TabContentsContainerView alloc] initWithDelegate:self]);
[view setAutoresizingMask:NSViewHeightSizable|NSViewWidthSizable];
[self setView:view];
}
- (NSTouchBar*)makeTouchBar API_AVAILABLE(macos(10.12.2)) {
return [touchBarController_ makeTouchBar];
}
- (void)ensureContentsVisibleInSuperview:(NSView*)superview {
if (!contents_)
return;
ScopedCAActionDisabler disabler;
NSView* contentsContainer = [self view];
NSArray* subviews = [contentsContainer subviews];
NSView* contentsNativeView;
content::RenderWidgetHostView* const fullscreenView =
isEmbeddingFullscreenWidget_ ?
contents_->GetFullscreenRenderWidgetHostView() : NULL;
if (fullscreenPlaceholderView_) {
contentsNativeView = fullscreenPlaceholderView_;
} else if (fullscreenView) {
contentsNativeView = fullscreenView->GetNativeView();
} else {
isEmbeddingFullscreenWidget_ = NO;
contentsNativeView = contents_->GetNativeView();
}
if ([self shouldResizeContentView])
[contentsNativeView setFrame:[self frameForContentsViewIn:superview]];
if ([subviews count] == 0) {
[contentsContainer addSubview:contentsNativeView];
} else if ([subviews objectAtIndex:0] != contentsNativeView) {
[contentsContainer replaceSubview:[subviews objectAtIndex:0]
with:contentsNativeView];
}
[contentsNativeView setAutoresizingMask:NSViewNotSizable];
[contentsContainer setFrame:[superview bounds]];
[superview addSubview:contentsContainer];
[contentsNativeView setAutoresizingMask:NSViewWidthSizable|
NSViewHeightSizable];
[contentsContainer setNeedsDisplay:YES];
}
- (void)updateFullscreenWidgetFrame {
// This should only apply if a fullscreen widget is embedded.
if (!isEmbeddingFullscreenWidget_ || blockFullscreenResize_)
return;
content::RenderWidgetHostView* const fullscreenView =
contents_->GetFullscreenRenderWidgetHostView();
if (fullscreenView) {
[fullscreenView->GetNativeView()
setFrame:[self frameForContentsViewIn:[self view]]];
}
}
- (void)changeWebContents:(WebContents*)newContents {
contents_ = newContents;
fullscreenObserver_->Observe(contents_);
isEmbeddingFullscreenWidget_ =
contents_ && contents_->GetFullscreenRenderWidgetHostView();
}
// Returns YES if the tab represented by this controller is the front-most.
- (BOOL)isCurrentTab {
// We're the current tab if we're in the view hierarchy, otherwise some other
// tab is.
return [[self view] superview] ? YES : NO;
}
- (void)willBecomeUnselectedTab {
// The RWHV is ripped out of the view hierarchy on tab switches, so it never
// formally resigns first responder status. Handle this by explicitly sending
// a Blur() message to the renderer, but only if the RWHV currently has focus.
content::RenderViewHost* rvh = [self webContents]->GetRenderViewHost();
if (rvh) {
if (rvh->GetWidget()->GetView() &&
rvh->GetWidget()->GetView()->HasFocus()) {
rvh->GetWidget()->Blur();
return;
}
WebContents* devtools = DevToolsWindow::GetInTabWebContents(
[self webContents], NULL);
if (devtools) {
content::RenderViewHost* devtoolsView = devtools->GetRenderViewHost();
if (devtoolsView && devtoolsView->GetWidget()->GetView() &&
devtoolsView->GetWidget()->GetView()->HasFocus()) {
devtoolsView->GetWidget()->Blur();
}
}
}
}
- (void)willBecomeSelectedTab {
// Do not explicitly call Focus() here, as the RWHV may not actually have
// focus (for example, if the omnibox has focus instead). The WebContents
// logic will restore focus to the appropriate view.
}
- (void)tabDidChange:(WebContents*)updatedContents {
// Calling setContentView: here removes any first responder status
// the view may have, so avoid changing the view hierarchy unless
// the view is different.
if ([self webContents] != updatedContents) {
[self changeWebContents:updatedContents];
[self ensureContentsVisibleInSuperview:[[self view] superview]];
}
}
- (void)toggleFullscreenWidget:(BOOL)enterFullscreen {
isEmbeddingFullscreenWidget_ = enterFullscreen &&
contents_ && contents_->GetFullscreenRenderWidgetHostView();
if (base::FeatureList::IsEnabled(features::kContentFullscreen)) {
if (enterFullscreen) {
fullscreenPlaceholderView_ = [self createScreenshotView];
separateFullscreenWindow_ = [self createSeparateWindowForTab:contents_];
[separateFullscreenWindow_ makeKeyAndOrderFront:nil];
[separateFullscreenWindow_ toggleFullScreen:nil];
} else {
[separateFullscreenWindow_ close];
}
}
[self ensureContentsVisibleInSuperview:[[self view] superview]];
}
- (BOOL)contentsInFullscreenCaptureMode {
// Note: Grab a known-valid WebContents pointer from |fullscreenObserver_|.
content::WebContents* const wc = fullscreenObserver_->web_contents();
if (!wc || !wc->IsBeingCaptured() || wc->GetPreferredSize().IsEmpty() ||
!(isEmbeddingFullscreenWidget_ ||
(wc->GetDelegate() &&
wc->GetDelegate()->IsFullscreenForTabOrPending(wc)))) {
return NO;
}
return YES;
}
- (NSRect)frameForContentsViewIn:(NSView*)container {
gfx::Rect rect([container bounds]);
// In most cases, the contents view is simply sized to fill the container
// view's bounds. Only WebContentses that are in fullscreen mode and being
// screen-captured will engage the special layout/sizing behavior.
if (![self contentsInFullscreenCaptureMode])
return NSRectFromCGRect(rect.ToCGRect());
// Size the contents view to the capture video resolution and center it. If
// the container view is not large enough to fit it at the preferred size,
// scale down to fit (preserving aspect ratio).
content::WebContents* const wc = fullscreenObserver_->web_contents();
const gfx::Size captureSize = wc->GetPreferredSize();
if (captureSize.width() <= rect.width() &&
captureSize.height() <= rect.height()) {
// No scaling, just centering.
rect.ClampToCenteredSize(captureSize);
} else {
// Scale down, preserving aspect ratio, and center.
// TODO(miu): This is basically media::ComputeLetterboxRegion(), and it
// looks like others have written this code elsewhere. Let's consolidate
// into a shared function ui/gfx/geometry or around there.
const int64_t x = static_cast<int64_t>(captureSize.width()) * rect.height();
const int64_t y = static_cast<int64_t>(captureSize.height()) * rect.width();
if (y < x) {
rect.ClampToCenteredSize(gfx::Size(
rect.width(), static_cast<int>(y / captureSize.width())));
} else {
rect.ClampToCenteredSize(gfx::Size(
static_cast<int>(x / captureSize.height()), rect.height()));
}
}
return NSRectFromCGRect(rect.ToCGRect());
}
- (BOOL)shouldResizeContentView {
return !isEmbeddingFullscreenWidget_ || !blockFullscreenResize_;
}
- (BOOL)isPopup {
return isPopup_;
}
- (WebTextfieldTouchBarController*)webTextfieldTouchBarController {
return touchBarController_.get();
}
@end
@implementation TabContentsController (SeparateFullscreenWindowDelegate)
- (void)windowDidEnterFullScreen:(NSNotification*)notification {
// Make the RenderWidgetHostViewCocoa the firstResponder for the
// SeparateFullscreenWindow.
contents_->Focus();
}
- (void)windowWillExitFullScreen:(NSNotification*)notification {
// Remove the screenshot view so that the WebContentsViewCocoa is
// retrieved and displayed again in the original window.
fullscreenPlaceholderView_ = nil;
[self ensureContentsVisibleInSuperview:[[self view] superview]];
// When exiting through the title bar Exit Fullscreen Window button, the
// WebContents must be notified of the change in fullscreen (like in
// FullscreenController::HandleUserPressedEscape).
contents_->ExitFullscreen(true);
}
- (void)windowDidExitFullScreen:(NSNotification*)notification {
// When exiting through the title bar Exit Fullscreen Window button, the
// SeparateFullscreenWindow doesn't close, so make sure it's closed.
[separateFullscreenWindow_ close];
separateFullscreenWindow_ = nil;
}
- (NSView*)createScreenshotView {
// Getting the current's window view and its boundaries.
NSWindow* window = [contents_->GetNativeView() window];
NSView* view = contents_->GetNativeView();
NSRect windowFrame = window.frame;
NSRect viewFrame = [view convertRect:view.bounds toView:nil];
// Moving the origin from the lower-left corner to the upper-left corner of
// the view and cropping out the scrollbar
viewFrame.origin.y = NSHeight(windowFrame) - NSMaxY(viewFrame);
viewFrame.size.width -= gfx::scrollbar_size();
// Taking a screenshot of the view and creating the custom view to display
CGImageRef windowScreenshot = (CGImageRef)[(id)CGWindowListCreateImage(
CGRectZero, kCGWindowListOptionIncludingWindow, [window windowNumber],
kCGWindowImageBoundsIgnoreFraming) autorelease];
CGImageRef viewScreenshot = (CGImageRef)[(id)CGImageCreateWithImageInRect(
windowScreenshot, [window convertRectToBacking:viewFrame]) autorelease];
FullscreenPlaceholderView* screenshotView =
[[[FullscreenPlaceholderView alloc] initWithFrame:[[self view] bounds]
image:viewScreenshot]
autorelease];
screenshotView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
return screenshotView;
}
// Creates a new window with the tab without detaching it from its source
// window.
- (NSWindow*)createSeparateWindowForTab:(WebContents*)separatedTab {
DCHECK(separatedTab->GetNativeView());
NSView* separatedTabView = separatedTab->GetNativeView();
NSWindow* sourceWindow = [separatedTabView window];
NSRect windowRect =
[separatedTabView convertRect:[separatedTabView bounds] toView:nil];
SeparateFullscreenWindow* separateWindow = [[SeparateFullscreenWindow alloc]
initWithContentRect:[sourceWindow convertRectToScreen:windowRect]
styleMask:NSResizableWindowMask
backing:NSBackingStoreBuffered
defer:NO];
[separateWindow setDelegate:self];
[[separateWindow contentView] addSubview:separatedTabView];
[separateWindow
setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary];
// Make TabContentsContainerView the first responder now as
// WebContentsViewCocoa is now in a separate window.
[sourceWindow makeFirstResponder:[self view]];
return separateWindow;
}
@end