1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import "chrome/browser/ui/cocoa/browser_window_controller_private.h"
6
7#include <cmath>
8
9#include "base/command_line.h"
10#include "base/mac/mac_util.h"
11#import "base/mac/scoped_nsobject.h"
12#import "base/mac/sdk_forward_declarations.h"
13#include "base/prefs/pref_service.h"
14#include "base/prefs/scoped_user_pref_update.h"
15#include "chrome/browser/browser_process.h"
16#include "chrome/browser/fullscreen.h"
17#include "chrome/browser/profiles/profile.h"
18#include "chrome/browser/profiles/profile_avatar_icon_util.h"
19#include "chrome/browser/ui/bookmarks/bookmark_tab_helper.h"
20#include "chrome/browser/ui/browser.h"
21#include "chrome/browser/ui/browser_window_state.h"
22#import "chrome/browser/ui/cocoa/browser_window_layout.h"
23#import "chrome/browser/ui/cocoa/dev_tools_controller.h"
24#import "chrome/browser/ui/cocoa/fast_resize_view.h"
25#import "chrome/browser/ui/cocoa/find_bar/find_bar_cocoa_controller.h"
26#import "chrome/browser/ui/cocoa/floating_bar_backing_view.h"
27#import "chrome/browser/ui/cocoa/framed_browser_window.h"
28#import "chrome/browser/ui/cocoa/fullscreen_window.h"
29#import "chrome/browser/ui/cocoa/infobars/infobar_container_controller.h"
30#include "chrome/browser/ui/cocoa/last_active_browser_cocoa.h"
31#import "chrome/browser/ui/cocoa/presentation_mode_controller.h"
32#import "chrome/browser/ui/cocoa/profiles/avatar_button_controller.h"
33#import "chrome/browser/ui/cocoa/profiles/avatar_icon_controller.h"
34#import "chrome/browser/ui/cocoa/status_bubble_mac.h"
35#import "chrome/browser/ui/cocoa/tab_contents/overlayable_contents_controller.h"
36#import "chrome/browser/ui/cocoa/tabs/tab_strip_view.h"
37#import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
38#import "chrome/browser/ui/cocoa/version_independent_window.h"
39#import "chrome/browser/ui/cocoa/website_settings/permission_bubble_cocoa.h"
40#include "chrome/browser/ui/fullscreen/fullscreen_controller.h"
41#include "chrome/browser/ui/tabs/tab_strip_model.h"
42#include "chrome/common/chrome_switches.h"
43#include "chrome/common/pref_names.h"
44#include "content/public/browser/render_widget_host_view.h"
45#include "content/public/browser/web_contents.h"
46#import "ui/base/cocoa/focus_tracker.h"
47#import "ui/base/cocoa/nsview_additions.h"
48#include "ui/base/ui_base_types.h"
49
50using content::RenderWidgetHostView;
51using content::WebContents;
52
53namespace {
54
55// Space between the incognito badge and the right edge of the window.
56const CGFloat kAvatarRightOffset = 4;
57
58// Space between the location bar and the right edge of the window, when there
59// are no extension buttons present.
60// When there is a fullscreen button to the right of the new style profile
61// button, we align the profile button with the location bar (although it won't
62// be aligned when there are extension buttons).
63const CGFloat kLocationBarRightOffset = 35;
64
65}  // namespace
66
67@implementation BrowserWindowController(Private)
68
69// Create the tab strip controller.
70- (void)createTabStripController {
71  DCHECK([overlayableContentsController_ activeContainer]);
72  DCHECK([[overlayableContentsController_ activeContainer] window]);
73  tabStripController_.reset([[TabStripController alloc]
74      initWithView:[self tabStripView]
75        switchView:[overlayableContentsController_ activeContainer]
76           browser:browser_.get()
77          delegate:self]);
78}
79
80- (void)saveWindowPositionIfNeeded {
81  if (!chrome::ShouldSaveWindowPlacement(browser_.get()))
82    return;
83
84  // If we're in fullscreen mode, save the position of the regular window
85  // instead.
86  NSWindow* window =
87      [self isInAnyFullscreenMode] ? savedRegularWindow_ : [self window];
88
89  // Window positions are stored relative to the origin of the primary monitor.
90  NSRect monitorFrame = [[[NSScreen screens] objectAtIndex:0] frame];
91  NSScreen* windowScreen = [window screen];
92
93  // Start with the window's frame, which is in virtual coordinates.
94  // Do some y twiddling to flip the coordinate system.
95  gfx::Rect bounds(NSRectToCGRect([window frame]));
96  bounds.set_y(monitorFrame.size.height - bounds.y() - bounds.height());
97
98  // Browser::SaveWindowPlacement saves information for session restore.
99  ui::WindowShowState show_state = ui::SHOW_STATE_NORMAL;
100  if ([window isMiniaturized])
101    show_state = ui::SHOW_STATE_MINIMIZED;
102  else if ([self isInAnyFullscreenMode])
103    show_state = ui::SHOW_STATE_FULLSCREEN;
104  chrome::SaveWindowPlacement(browser_.get(), bounds, show_state);
105
106  // |windowScreen| can be nil (for example, if the monitor arrangement was
107  // changed while in fullscreen mode).  If we see a nil screen, return without
108  // saving.
109  // TODO(rohitrao): We should just not save anything for fullscreen windows.
110  // http://crbug.com/36479.
111  if (!windowScreen)
112    return;
113
114  // Only save main window information to preferences.
115  PrefService* prefs = browser_->profile()->GetPrefs();
116  if (!prefs || browser_ != chrome::GetLastActiveBrowser())
117    return;
118
119  // Save the current work area, in flipped coordinates.
120  gfx::Rect workArea(NSRectToCGRect([windowScreen visibleFrame]));
121  workArea.set_y(monitorFrame.size.height - workArea.y() - workArea.height());
122
123  scoped_ptr<DictionaryPrefUpdate> update =
124      chrome::GetWindowPlacementDictionaryReadWrite(
125          chrome::GetWindowName(browser_.get()),
126          browser_->profile()->GetPrefs());
127  base::DictionaryValue* windowPreferences = update->Get();
128  windowPreferences->SetInteger("left", bounds.x());
129  windowPreferences->SetInteger("top", bounds.y());
130  windowPreferences->SetInteger("right", bounds.right());
131  windowPreferences->SetInteger("bottom", bounds.bottom());
132  windowPreferences->SetBoolean("maximized", false);
133  windowPreferences->SetBoolean("always_on_top", false);
134  windowPreferences->SetInteger("work_area_left", workArea.x());
135  windowPreferences->SetInteger("work_area_top", workArea.y());
136  windowPreferences->SetInteger("work_area_right", workArea.right());
137  windowPreferences->SetInteger("work_area_bottom", workArea.bottom());
138}
139
140- (NSRect)window:(NSWindow*)window
141willPositionSheet:(NSWindow*)sheet
142       usingRect:(NSRect)defaultSheetRect {
143  // Position the sheet as follows:
144  //  - If the bookmark bar is hidden or shown as a bubble (on the NTP when the
145  //    bookmark bar is disabled), position the sheet immediately below the
146  //    normal toolbar.
147  //  - If the bookmark bar is shown (attached to the normal toolbar), position
148  //    the sheet below the bookmark bar.
149  //  - If the bookmark bar is currently animating, position the sheet according
150  //    to where the bar will be when the animation ends.
151  CGFloat defaultSheetY = defaultSheetRect.origin.y;
152  switch ([bookmarkBarController_ currentState]) {
153    case BookmarkBar::SHOW: {
154      NSRect bookmarkBarFrame = [[bookmarkBarController_ view] frame];
155      defaultSheetY = bookmarkBarFrame.origin.y;
156      break;
157    }
158    case BookmarkBar::HIDDEN:
159    case BookmarkBar::DETACHED: {
160      if ([self hasToolbar]) {
161        NSRect toolbarFrame = [[toolbarController_ view] frame];
162        defaultSheetY = toolbarFrame.origin.y;
163      } else {
164        // The toolbar is not shown in application mode. The sheet should be
165        // located at the top of the window, under the title of the window.
166        defaultSheetY = NSHeight([[window contentView] frame]) -
167                        defaultSheetRect.size.height;
168      }
169      break;
170    }
171  }
172
173  // AppKit may shift the window up to fit the sheet on screen, but it will
174  // never adjust the height of the sheet, or the origin of the sheet relative
175  // to the window. Adjust the origin to prevent sheets from extending past the
176  // bottom of the screen.
177
178  // Don't allow the sheet to extend past the bottom of the window. This logic
179  // intentionally ignores the size of the screens, since the window might span
180  // multiple screens, and AppKit may reposition the window.
181  CGFloat sheetHeight = NSHeight([sheet frame]);
182  defaultSheetY = std::max(defaultSheetY, sheetHeight);
183
184  // It doesn't make sense to provide a Y higher than the height of the window.
185  CGFloat windowHeight = NSHeight([window frame]);
186  defaultSheetY = std::min(defaultSheetY, windowHeight);
187
188  defaultSheetRect.origin.y = defaultSheetY;
189  return defaultSheetRect;
190}
191
192- (void)layoutSubviews {
193  // Suppress title drawing if necessary.
194  if ([self.window respondsToSelector:@selector(setShouldHideTitle:)])
195    [(id)self.window setShouldHideTitle:![self hasTitleBar]];
196
197  [bookmarkBarController_ updateHiddenState];
198  [self updateSubviewZOrder];
199
200  base::scoped_nsobject<BrowserWindowLayout> layout(
201      [[BrowserWindowLayout alloc] init]);
202  [self updateLayoutParameters:layout];
203  [self applyLayout:layout];
204
205  [toolbarController_ setDividerOpacity:[self toolbarDividerOpacity]];
206}
207
208- (CGFloat)layoutTabStripAtMaxY:(CGFloat)maxY
209                          width:(CGFloat)width
210                     fullscreen:(BOOL)fullscreen {
211  // Nothing to do if no tab strip.
212  if (![self hasTabStrip])
213    return maxY;
214
215  NSView* tabStripView = [self tabStripView];
216  CGFloat tabStripHeight = NSHeight([tabStripView frame]);
217  maxY -= tabStripHeight;
218  NSRect tabStripFrame = NSMakeRect(0, maxY, width, tabStripHeight);
219  BOOL requiresRelayout = !NSEqualRects(tabStripFrame, [tabStripView frame]);
220
221  // In Yosemite fullscreen, manually add the fullscreen controls to the tab
222  // strip.
223  BOOL addControlsInFullscreen =
224      [self isInAppKitFullscreen] && base::mac::IsOSYosemiteOrLater();
225
226  // Set left indentation based on fullscreen mode status.
227  CGFloat leftIndent = 0;
228  if (!fullscreen || addControlsInFullscreen)
229    leftIndent = [[tabStripController_ class] defaultLeftIndentForControls];
230  if (leftIndent != [tabStripController_ leftIndentForControls]) {
231    [tabStripController_ setLeftIndentForControls:leftIndent];
232    requiresRelayout = YES;
233  }
234
235  if (addControlsInFullscreen)
236    [tabStripController_ addWindowControls];
237  else
238    [tabStripController_ removeWindowControls];
239
240  // fullScreenButton is non-nil when isInAnyFullscreenMode is NO, and OS
241  // version is in the range 10.7 <= version <= 10.9. Starting with 10.10, the
242  // zoom/maximize button acts as the fullscreen button.
243  NSButton* fullScreenButton =
244      [[self window] standardWindowButton:NSWindowFullScreenButton];
245
246  // Lay out the icognito/avatar badge because calculating the indentation on
247  // the right depends on it.
248  NSView* avatarButton = [avatarButtonController_ view];
249  if ([self shouldShowAvatar]) {
250    CGFloat badgeXOffset = -kAvatarRightOffset;
251    CGFloat badgeYOffset = 0;
252    CGFloat buttonHeight = NSHeight([avatarButton frame]);
253
254    if ([self shouldUseNewAvatarButton]) {
255      // The fullscreen icon is displayed to the right of the avatar button.
256      if (fullScreenButton)
257        badgeXOffset = -kLocationBarRightOffset;
258      // Center the button vertically on the tabstrip.
259      badgeYOffset = (tabStripHeight - buttonHeight) / 2;
260    } else {
261      // Actually place the badge *above* |maxY|, by +2 to miss the divider.
262      badgeYOffset = 2 * [[avatarButton superview] cr_lineWidth];
263    }
264
265    [avatarButton setFrameSize:NSMakeSize(NSWidth([avatarButton frame]),
266        std::min(buttonHeight, tabStripHeight))];
267    NSPoint origin =
268        NSMakePoint(width - NSWidth([avatarButton frame]) + badgeXOffset,
269                    maxY + badgeYOffset);
270    [avatarButton setFrameOrigin:origin];
271    [avatarButton setHidden:NO];  // Make sure it's shown.
272  }
273
274  // Calculate the right indentation.
275  // On 10.7 Lion to 10.9 Mavericks, there will be a fullscreen button when not
276  // in fullscreen mode.
277  // There may also be a profile button, which can be on the right of the
278  // fullscreen button (old style), or to its left (new style).
279  // The right indentation is calculated to prevent the tab strip from
280  // overlapping these buttons.
281  CGFloat maxX = width;
282  if (fullScreenButton) {
283    maxX = NSMinX([fullScreenButton frame]);
284  }
285  if ([self shouldShowAvatar]) {
286    maxX = std::min(maxX, NSMinX([avatarButton frame]));
287  }
288  CGFloat rightIndent = width - maxX;
289  if (rightIndent != [tabStripController_ rightIndentForControls]) {
290    [tabStripController_ setRightIndentForControls:rightIndent];
291    requiresRelayout = YES;
292  }
293
294  // It is undesirable to force tabs relayout when the tap strip's frame did
295  // not change, because it will interrupt tab animations in progress.
296  // In addition, there appears to be an AppKit bug on <10.9 where interrupting
297  // a tab animation resulted in the tab frame being the animator's target
298  // frame instead of the interrupting setFrame. (See http://crbug.com/415093)
299  if (requiresRelayout) {
300    [tabStripView setFrame:tabStripFrame];
301    [tabStripController_ layoutTabsWithoutAnimation];
302  }
303
304  return maxY;
305}
306
307- (BOOL)placeBookmarkBarBelowInfoBar {
308  // If we are currently displaying the NTP detached bookmark bar or animating
309  // to/from it (from/to anything else), we display the bookmark bar below the
310  // info bar.
311  return [bookmarkBarController_ isInState:BookmarkBar::DETACHED] ||
312         [bookmarkBarController_ isAnimatingToState:BookmarkBar::DETACHED] ||
313         [bookmarkBarController_ isAnimatingFromState:BookmarkBar::DETACHED];
314}
315
316- (void)layoutTabContentArea:(NSRect)newFrame {
317  NSView* tabContentView = [self tabContentArea];
318  NSRect tabContentFrame = [tabContentView frame];
319
320  bool contentShifted =
321      NSMaxY(tabContentFrame) != NSMaxY(newFrame) ||
322      NSMinX(tabContentFrame) != NSMinX(newFrame);
323
324  tabContentFrame = newFrame;
325  [tabContentView setFrame:tabContentFrame];
326
327  // If the relayout shifts the content area up or down, let the renderer know.
328  if (contentShifted) {
329    if (WebContents* contents =
330            browser_->tab_strip_model()->GetActiveWebContents()) {
331      if (RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView())
332        rwhv->WindowFrameChanged();
333    }
334  }
335}
336
337- (void)adjustToolbarAndBookmarkBarForCompression:(CGFloat)compression {
338  CGFloat newHeight =
339      [toolbarController_ desiredHeightForCompression:compression];
340  NSRect toolbarFrame = [[toolbarController_ view] frame];
341  CGFloat deltaH = newHeight - toolbarFrame.size.height;
342
343  if (deltaH == 0)
344    return;
345
346  toolbarFrame.size.height = newHeight;
347  NSRect bookmarkFrame = [[bookmarkBarController_ view] frame];
348  bookmarkFrame.size.height = bookmarkFrame.size.height - deltaH;
349  [[toolbarController_ view] setFrame:toolbarFrame];
350  [[bookmarkBarController_ view] setFrame:bookmarkFrame];
351  [self layoutSubviews];
352}
353
354// Fullscreen and presentation mode methods
355
356- (void)moveViewsForImmersiveFullscreen:(BOOL)fullscreen
357                          regularWindow:(NSWindow*)regularWindow
358                       fullscreenWindow:(NSWindow*)fullscreenWindow {
359  NSWindow* sourceWindow = fullscreen ? regularWindow : fullscreenWindow;
360  NSWindow* destWindow = fullscreen ? fullscreenWindow : regularWindow;
361
362  // Close the bookmark bubble, if it's open.  Use |-ok:| instead of |-cancel:|
363  // or |-close| because that matches the behavior when the bubble loses key
364  // status.
365  [bookmarkBubbleController_ ok:self];
366
367  // Save the current first responder so we can restore after views are moved.
368  base::scoped_nsobject<FocusTracker> focusTracker(
369      [[FocusTracker alloc] initWithWindow:sourceWindow]);
370
371  // While we move views (and focus) around, disable any bar visibility changes.
372  [self disableBarVisibilityUpdates];
373
374  // Retain the tab strip view while we remove it from its superview.
375  base::scoped_nsobject<NSView> tabStripView;
376  if ([self hasTabStrip]) {
377    tabStripView.reset([[self tabStripView] retain]);
378    [tabStripView removeFromSuperview];
379  }
380
381  // Ditto for the content view.
382  base::scoped_nsobject<NSView> contentView(
383      [[sourceWindow contentView] retain]);
384  // Disable autoresizing of subviews while we move views around. This prevents
385  // spurious renderer resizes.
386  [contentView setAutoresizesSubviews:NO];
387  [contentView removeFromSuperview];
388
389  // Have to do this here, otherwise later calls can crash because the window
390  // has no delegate.
391  [sourceWindow setDelegate:nil];
392  [destWindow setDelegate:self];
393
394  // With this call, valgrind complains that a "Conditional jump or move depends
395  // on uninitialised value(s)".  The error happens in -[NSThemeFrame
396  // drawOverlayRect:].  I'm pretty convinced this is an Apple bug, but there is
397  // no visual impact.  I have been unable to tickle it away with other window
398  // or view manipulation Cocoa calls.  Stack added to suppressions_mac.txt.
399  [contentView setAutoresizesSubviews:YES];
400  [destWindow setContentView:contentView];
401  [self moveContentViewToBack:contentView];
402
403  // Move the incognito badge if present.
404  if ([self shouldShowAvatar]) {
405    NSView* avatarButtonView = [avatarButtonController_ view];
406
407    [avatarButtonView removeFromSuperview];
408    [avatarButtonView setHidden:YES];  // Will be shown in layout.
409    [[destWindow cr_windowView] addSubview:avatarButtonView];
410  }
411
412  // Add the tab strip after setting the content view and moving the incognito
413  // badge (if any), so that the tab strip will be on top (in the z-order).
414  if ([self hasTabStrip])
415    [self insertTabStripView:tabStripView intoWindow:[self window]];
416
417  [sourceWindow setWindowController:nil];
418  [self setWindow:destWindow];
419  [destWindow setWindowController:self];
420
421  // Move the status bubble over, if we have one.
422  if (statusBubble_)
423    statusBubble_->SwitchParentWindow(destWindow);
424
425  // Move the title over.
426  [destWindow setTitle:[sourceWindow title]];
427
428  // The window needs to be onscreen before we can set its first responder.
429  // Ordering the window to the front can change the active Space (either to
430  // the window's old Space or to the application's assigned Space). To prevent
431  // this by temporarily change the collectionBehavior.
432  NSWindowCollectionBehavior behavior = [sourceWindow collectionBehavior];
433  [destWindow setCollectionBehavior:
434      NSWindowCollectionBehaviorMoveToActiveSpace];
435  [destWindow makeKeyAndOrderFront:self];
436  [destWindow setCollectionBehavior:behavior];
437
438  if (![focusTracker restoreFocusInWindow:destWindow]) {
439    // During certain types of fullscreen transitions, the view that had focus
440    // may have gone away (e.g., the one for a Flash FS widget).  In this case,
441    // FocusTracker will fail to restore focus to anything, so we set the focus
442    // to the tab contents as a reasonable fall-back.
443    [self focusTabContents];
444  }
445  [sourceWindow orderOut:self];
446
447  // We're done moving focus, so re-enable bar visibility changes.
448  [self enableBarVisibilityUpdates];
449}
450
451- (void)permissionBubbleWindowWillClose:(NSNotification*)notification {
452  DCHECK(permissionBubbleCocoa_);
453
454  NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
455  [center removeObserver:self
456                    name:NSWindowWillCloseNotification
457                  object:[notification object]];
458  [self releaseBarVisibilityForOwner:[notification object]
459                       withAnimation:YES
460                               delay:YES];
461}
462
463- (void)configurePresentationModeController {
464  BOOL fullscreen_for_tab =
465      browser_->fullscreen_controller()->IsWindowFullscreenForTabOrPending();
466  BOOL kiosk_mode =
467      CommandLine::ForCurrentProcess()->HasSwitch(switches::kKioskMode);
468  BOOL showDropdown =
469      !fullscreen_for_tab && !kiosk_mode && ([self floatingBarHasFocus]);
470  if (permissionBubbleCocoa_ && permissionBubbleCocoa_->IsVisible()) {
471    DCHECK(permissionBubbleCocoa_->window());
472    // A visible permission bubble will force the dropdown to remain visible.
473    [self lockBarVisibilityForOwner:permissionBubbleCocoa_->window()
474                      withAnimation:NO
475                              delay:NO];
476    showDropdown = YES;
477    // Register to be notified when the permission bubble is closed, to
478    // allow fullscreen to hide the dropdown.
479    NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
480    [center addObserver:self
481               selector:@selector(permissionBubbleWindowWillClose:)
482                   name:NSWindowWillCloseNotification
483                 object:permissionBubbleCocoa_->window()];
484  }
485  if (showDropdown) {
486    // Turn on layered mode for the window's root view for the entry
487    // animation.  Without this, the OS fullscreen animation for entering
488    // fullscreen mode does not correctly draw the tab strip.
489    // It will be turned off (set back to NO) when the animation finishes,
490    // in -windowDidEnterFullScreen:.
491    // Leaving wantsLayer on for the duration of presentation mode causes
492    // performance issues when the dropdown is animated in/out.  It also does
493    // not seem to be required for the exit animation.
494    windowViewWantsLayer_ = [[[self window] cr_windowView] wantsLayer];
495    [[[self window] cr_windowView] setWantsLayer:YES];
496  }
497
498  NSView* contentView = [[self window] contentView];
499  [presentationModeController_
500      enterPresentationModeForContentView:contentView
501                             showDropdown:showDropdown];
502}
503
504- (void)adjustUIForExitingFullscreenAndStopOmniboxSliding {
505  [presentationModeController_ exitPresentationMode];
506  presentationModeController_.reset();
507
508  // Force the bookmark bar z-order to update.
509  [[bookmarkBarController_ view] removeFromSuperview];
510  [self layoutSubviews];
511}
512
513- (void)adjustUIForSlidingFullscreenStyle:(fullscreen_mac::SlidingStyle)style {
514  if (!presentationModeController_) {
515    presentationModeController_.reset(
516        [self newPresentationModeControllerWithStyle:style]);
517    [self configurePresentationModeController];
518  } else {
519    presentationModeController_.get().slidingStyle = style;
520  }
521
522  if (!floatingBarBackingView_.get() &&
523      ([self hasTabStrip] || [self hasToolbar] || [self hasLocationBar])) {
524    floatingBarBackingView_.reset(
525        [[FloatingBarBackingView alloc] initWithFrame:NSZeroRect]);
526    [floatingBarBackingView_
527        setAutoresizingMask:(NSViewWidthSizable | NSViewMinYMargin)];
528  }
529
530  // Force the bookmark bar z-order to update.
531  [[bookmarkBarController_ view] removeFromSuperview];
532  [self layoutSubviews];
533}
534
535- (PresentationModeController*)newPresentationModeControllerWithStyle:
536    (fullscreen_mac::SlidingStyle)style {
537  return [[PresentationModeController alloc] initWithBrowserController:self
538                                                                 style:style];
539}
540
541- (void)enterImmersiveFullscreen {
542  // Set to NO by |-windowDidEnterFullScreen:|.
543  enteringImmersiveFullscreen_ = YES;
544
545  // Fade to black.
546  const CGDisplayReservationInterval kFadeDurationSeconds = 0.6;
547  Boolean didFadeOut = NO;
548  CGDisplayFadeReservationToken token;
549  if (CGAcquireDisplayFadeReservation(kFadeDurationSeconds, &token)
550      == kCGErrorSuccess) {
551    didFadeOut = YES;
552    CGDisplayFade(token, kFadeDurationSeconds / 2, kCGDisplayBlendNormal,
553        kCGDisplayBlendSolidColor, 0.0, 0.0, 0.0, /*synchronous=*/true);
554  }
555
556  // Create the fullscreen window.
557  fullscreenWindow_.reset([[self createFullscreenWindow] retain]);
558  savedRegularWindow_ = [[self window] retain];
559  savedRegularWindowFrame_ = [savedRegularWindow_ frame];
560
561  [self moveViewsForImmersiveFullscreen:YES
562                          regularWindow:[self window]
563                       fullscreenWindow:fullscreenWindow_.get()];
564
565  fullscreen_mac::SlidingStyle style = fullscreen_mac::OMNIBOX_TABS_HIDDEN;
566  [self adjustUIForSlidingFullscreenStyle:style];
567
568  // AppKit is helpful and prevents NSWindows from having the same height as
569  // the screen while the menu bar is showing. This only applies to windows on
570  // a secondary screen, in a separate space. Calling [NSWindow
571  // setFrame:display:] with the screen's height will always reduce the
572  // height by the height of the MenuBar. Calling the method with any other
573  // height works fine. The relevant method in the 10.10 AppKit SDK is called:
574  // _canAdjustSizeForScreensHaveSeparateSpacesIfFillingSecondaryScreen
575  //
576  // TODO(erikchen): Refactor the logic to allow the window to be shown after
577  // the menubar has been hidden. This would remove the need for this hack.
578  // http://crbug.com/403203
579  NSRect frame = [[[self window] screen] frame];
580  if (!NSEqualRects(frame, [fullscreenWindow_ frame]))
581    [fullscreenWindow_ setFrame:[[[self window] screen] frame] display:YES];
582
583  [self layoutSubviews];
584
585  [self windowDidEnterFullScreen:nil];
586
587  // Fade back in.
588  if (didFadeOut) {
589    CGDisplayFade(token, kFadeDurationSeconds / 2, kCGDisplayBlendSolidColor,
590        kCGDisplayBlendNormal, 0.0, 0.0, 0.0, /*synchronous=*/false);
591    CGReleaseDisplayFadeReservation(token);
592  }
593}
594
595- (void)exitImmersiveFullscreen {
596  // Fade to black.
597  const CGDisplayReservationInterval kFadeDurationSeconds = 0.6;
598  Boolean didFadeOut = NO;
599  CGDisplayFadeReservationToken token;
600  if (CGAcquireDisplayFadeReservation(kFadeDurationSeconds, &token)
601      == kCGErrorSuccess) {
602    didFadeOut = YES;
603    CGDisplayFade(token, kFadeDurationSeconds / 2, kCGDisplayBlendNormal,
604        kCGDisplayBlendSolidColor, 0.0, 0.0, 0.0, /*synchronous=*/true);
605  }
606
607  [self windowWillExitFullScreen:nil];
608
609  [self moveViewsForImmersiveFullscreen:NO
610                          regularWindow:savedRegularWindow_
611                       fullscreenWindow:fullscreenWindow_.get()];
612
613  // When exiting fullscreen mode, we need to call layoutSubviews manually.
614  [savedRegularWindow_ autorelease];
615  savedRegularWindow_ = nil;
616  fullscreenWindow_.reset();
617  [self layoutSubviews];
618
619  [self windowDidExitFullScreen:nil];
620
621  // Fade back in.
622  if (didFadeOut) {
623    CGDisplayFade(token, kFadeDurationSeconds / 2, kCGDisplayBlendSolidColor,
624        kCGDisplayBlendNormal, 0.0, 0.0, 0.0, /*synchronous=*/false);
625    CGReleaseDisplayFadeReservation(token);
626  }
627}
628
629- (void)showFullscreenExitBubbleIfNecessary {
630  // This method is called in response to
631  // |-updateFullscreenExitBubbleURL:bubbleType:|. If we're in the middle of the
632  // transition into fullscreen (i.e., using the AppKit Fullscreen API), do not
633  // show the bubble because it will cause visual jank
634  // (http://crbug.com/130649). This will be called again as part of
635  // |-windowDidEnterFullScreen:|, so arrange to do that work then instead.
636  if (enteringAppKitFullscreen_)
637    return;
638
639  [self hideOverlayIfPossibleWithAnimation:NO delay:NO];
640
641  if (fullscreenBubbleType_ == FEB_TYPE_NONE ||
642      fullscreenBubbleType_ == FEB_TYPE_BROWSER_FULLSCREEN_EXIT_INSTRUCTION) {
643    // Show no exit instruction bubble on Mac when in Browser Fullscreen.
644    [self destroyFullscreenExitBubbleIfNecessary];
645  } else {
646    [fullscreenExitBubbleController_ closeImmediately];
647    fullscreenExitBubbleController_.reset(
648        [[FullscreenExitBubbleController alloc]
649            initWithOwner:self
650                  browser:browser_.get()
651                      url:fullscreenUrl_
652               bubbleType:fullscreenBubbleType_]);
653    [fullscreenExitBubbleController_ showWindow];
654  }
655}
656
657- (void)destroyFullscreenExitBubbleIfNecessary {
658  [fullscreenExitBubbleController_ closeImmediately];
659  fullscreenExitBubbleController_.reset();
660}
661
662- (void)contentViewDidResize:(NSNotification*)notification {
663  [self layoutSubviews];
664}
665
666- (void)registerForContentViewResizeNotifications {
667  [[NSNotificationCenter defaultCenter]
668      addObserver:self
669         selector:@selector(contentViewDidResize:)
670             name:NSViewFrameDidChangeNotification
671           object:[[self window] contentView]];
672}
673
674- (void)deregisterForContentViewResizeNotifications {
675  [[NSNotificationCenter defaultCenter]
676      removeObserver:self
677                name:NSViewFrameDidChangeNotification
678              object:[[self window] contentView]];
679}
680
681- (NSSize)window:(NSWindow*)window
682    willUseFullScreenContentSize:(NSSize)proposedSize {
683  return proposedSize;
684}
685
686- (NSApplicationPresentationOptions)window:(NSWindow*)window
687    willUseFullScreenPresentationOptions:(NSApplicationPresentationOptions)opt {
688  return (opt |
689          NSApplicationPresentationAutoHideDock |
690          NSApplicationPresentationAutoHideMenuBar);
691}
692
693- (void)windowWillEnterFullScreen:(NSNotification*)notification {
694  if (notification)  // For System Fullscreen when non-nil.
695    [self registerForContentViewResizeNotifications];
696
697  NSWindow* window = [self window];
698  savedRegularWindowFrame_ = [window frame];
699  BOOL mode = enteringPresentationMode_ ||
700       browser_->fullscreen_controller()->IsWindowFullscreenForTabOrPending();
701  enteringAppKitFullscreen_ = YES;
702
703  fullscreen_mac::SlidingStyle style =
704      mode ? fullscreen_mac::OMNIBOX_TABS_HIDDEN
705           : fullscreen_mac::OMNIBOX_TABS_PRESENT;
706
707  [self adjustUIForSlidingFullscreenStyle:style];
708}
709
710- (void)windowDidEnterFullScreen:(NSNotification*)notification {
711  // In Yosemite, some combination of the titlebar and toolbar always show in
712  // full-screen mode. We do not want either to show. Search for the window that
713  // contains the views, and hide it. There is no need to ever unhide the view.
714  // http://crbug.com/380235
715  if (base::mac::IsOSYosemiteOrLater()) {
716    for (NSWindow* window in [[NSApplication sharedApplication] windows]) {
717      if ([window
718              isKindOfClass:NSClassFromString(@"NSToolbarFullScreenWindow")]) {
719        [window.contentView setHidden:YES];
720      }
721    }
722  }
723
724  if (notification)  // For System Fullscreen when non-nil.
725    [self deregisterForContentViewResizeNotifications];
726  enteringAppKitFullscreen_ = NO;
727  enteringImmersiveFullscreen_ = NO;
728  enteringPresentationMode_ = NO;
729
730  [self showFullscreenExitBubbleIfNecessary];
731  browser_->WindowFullscreenStateChanged();
732  [[[self window] cr_windowView] setWantsLayer:windowViewWantsLayer_];
733}
734
735- (void)windowWillExitFullScreen:(NSNotification*)notification {
736  if (notification)  // For System Fullscreen when non-nil.
737    [self registerForContentViewResizeNotifications];
738  [self destroyFullscreenExitBubbleIfNecessary];
739  [self adjustUIForExitingFullscreenAndStopOmniboxSliding];
740}
741
742- (void)windowDidExitFullScreen:(NSNotification*)notification {
743  if (notification)  // For System Fullscreen when non-nil.
744    [self deregisterForContentViewResizeNotifications];
745  browser_->WindowFullscreenStateChanged();
746}
747
748- (void)windowDidFailToEnterFullScreen:(NSWindow*)window {
749  [self deregisterForContentViewResizeNotifications];
750  enteringAppKitFullscreen_ = NO;
751  [self adjustUIForExitingFullscreenAndStopOmniboxSliding];
752}
753
754- (void)windowDidFailToExitFullScreen:(NSWindow*)window {
755  [self deregisterForContentViewResizeNotifications];
756
757  // Force a relayout to try and get the window back into a reasonable state.
758  [self layoutSubviews];
759}
760
761- (void)enableBarVisibilityUpdates {
762  // Early escape if there's nothing to do.
763  if (barVisibilityUpdatesEnabled_)
764    return;
765
766  barVisibilityUpdatesEnabled_ = YES;
767
768  if ([barVisibilityLocks_ count])
769    [presentationModeController_ ensureOverlayShownWithAnimation:NO delay:NO];
770  else
771    [presentationModeController_ ensureOverlayHiddenWithAnimation:NO delay:NO];
772}
773
774- (void)disableBarVisibilityUpdates {
775  // Early escape if there's nothing to do.
776  if (!barVisibilityUpdatesEnabled_)
777    return;
778
779  barVisibilityUpdatesEnabled_ = NO;
780  [presentationModeController_ cancelAnimationAndTimers];
781}
782
783- (void)hideOverlayIfPossibleWithAnimation:(BOOL)animation delay:(BOOL)delay {
784  if (!barVisibilityUpdatesEnabled_ || [barVisibilityLocks_ count])
785    return;
786  [presentationModeController_ ensureOverlayHiddenWithAnimation:animation
787                                                          delay:delay];
788}
789
790- (CGFloat)toolbarDividerOpacity {
791  return [bookmarkBarController_ toolbarDividerOpacity];
792}
793
794- (void)updateLayerOrdering:(NSView*)view {
795  // Hold a reference to the view so that it doesn't accidentally get
796  // dealloc'ed.
797  base::scoped_nsobject<NSView> reference([view retain]);
798
799  // If the superview has a layer, then this hack isn't required.
800  NSView* superview = [view superview];
801  if ([superview layer])
802    return;
803
804  // Get the current position of the view.
805  NSArray* subviews = [superview subviews];
806  NSInteger index = [subviews indexOfObject:view];
807  NSView* siblingBelow = nil;
808  if (index > 0)
809    siblingBelow = [subviews objectAtIndex:index - 1];
810
811  // Remove the view.
812  [view removeFromSuperview];
813
814  // Add it to the same position.
815  if (siblingBelow) {
816    [superview addSubview:view
817               positioned:NSWindowAbove
818               relativeTo:siblingBelow];
819  } else {
820    [superview addSubview:view
821               positioned:NSWindowBelow
822               relativeTo:nil];
823  }
824}
825
826- (void)updateInfoBarTipVisibility {
827  // If there's no toolbar then hide the infobar tip.
828  [infoBarContainerController_
829      setShouldSuppressTopInfoBarTip:![self hasToolbar]];
830}
831
832- (NSInteger)pageInfoBubblePointY {
833  LocationBarViewMac* locationBarView = [self locationBarBridge];
834
835  // The point, in window coordinates.
836  NSPoint iconBottom = locationBarView->GetPageInfoBubblePoint();
837
838  // The toolbar, in window coordinates.
839  NSView* toolbar = [toolbarController_ view];
840  CGFloat toolbarY = NSMinY([toolbar convertRect:[toolbar bounds] toView:nil]);
841
842  return iconBottom.y - toolbarY;
843}
844
845- (void)enterAppKitFullscreen {
846  DCHECK(base::mac::IsOSLionOrLater());
847  if (FramedBrowserWindow* framedBrowserWindow =
848          base::mac::ObjCCast<FramedBrowserWindow>([self window])) {
849    [framedBrowserWindow toggleSystemFullScreen];
850  }
851}
852
853- (void)exitAppKitFullscreen {
854  DCHECK(base::mac::IsOSLionOrLater());
855  if (FramedBrowserWindow* framedBrowserWindow =
856          base::mac::ObjCCast<FramedBrowserWindow>([self window])) {
857    [framedBrowserWindow toggleSystemFullScreen];
858  }
859}
860
861- (void)updateLayoutParameters:(BrowserWindowLayout*)layout {
862  [layout setContentViewSize:[[[self window] contentView] bounds].size];
863  [layout setWindowSize:[[self window] frame].size];
864
865  [layout setInAnyFullscreen:[self isInAnyFullscreenMode]];
866  [layout setFullscreenSlidingStyle:
867      presentationModeController_.get().slidingStyle];
868  [layout setFullscreenMenubarOffset:
869      [presentationModeController_ menubarOffset]];
870  [layout setFullscreenToolbarFraction:
871      [presentationModeController_ toolbarFraction]];
872
873  [layout setHasTabStrip:[self hasTabStrip]];
874
875  [layout setHasToolbar:[self hasToolbar]];
876  [layout setToolbarHeight:NSHeight([[toolbarController_ view] bounds])];
877
878  [layout setHasLocationBar:[self hasLocationBar]];
879
880  [layout setPlaceBookmarkBarBelowInfoBar:[self placeBookmarkBarBelowInfoBar]];
881  [layout setBookmarkBarHidden:[bookmarkBarController_ view].isHidden];
882  [layout setBookmarkBarHeight:
883      NSHeight([[bookmarkBarController_ view] bounds])];
884
885  [layout setInfoBarHeight:[infoBarContainerController_ heightOfInfoBars]];
886  [layout setPageInfoBubblePointY:[self pageInfoBubblePointY]];
887
888  [layout setHasDownloadShelf:(downloadShelfController_.get() != nil)];
889  [layout setDownloadShelfHeight:
890      NSHeight([[downloadShelfController_ view] bounds])];
891}
892
893- (void)applyLayout:(BrowserWindowLayout*)layout {
894  chrome::LayoutOutput output = [layout computeLayout];
895
896  if (!NSIsEmptyRect(output.tabStripFrame)) {
897    // Note: The fullscreen parameter passed to the method is different from
898    // the field in |parameters| with the similar name.
899    [self layoutTabStripAtMaxY:NSMaxY(output.tabStripFrame)
900                         width:NSWidth(output.tabStripFrame)
901                    fullscreen:[self isInAnyFullscreenMode]];
902  }
903
904  if (!NSIsEmptyRect(output.toolbarFrame)) {
905    [[toolbarController_ view] setFrame:output.toolbarFrame];
906  }
907
908  if (!NSIsEmptyRect(output.bookmarkFrame)) {
909    NSView* bookmarkBarView = [bookmarkBarController_ view];
910    [bookmarkBarView setFrame:output.bookmarkFrame];
911
912    // Pin the bookmark bar to the top of the window and make the width
913    // flexible.
914    [bookmarkBarView setAutoresizingMask:NSViewWidthSizable | NSViewMinYMargin];
915
916    [bookmarkBarController_ layoutSubviews];
917  }
918
919  // The info bar is never hidden. Sometimes it has zero effective height.
920  [[infoBarContainerController_ view] setFrame:output.infoBarFrame];
921  [infoBarContainerController_
922      setMaxTopArrowHeight:output.infoBarMaxTopArrowHeight];
923
924  if (!NSIsEmptyRect(output.downloadShelfFrame))
925    [[downloadShelfController_ view] setFrame:output.downloadShelfFrame];
926
927  [self layoutTabContentArea:output.contentAreaFrame];
928
929  if (!NSIsEmptyRect(output.fullscreenBackingBarFrame)) {
930    [floatingBarBackingView_ setFrame:output.fullscreenBackingBarFrame];
931    [presentationModeController_
932        overlayFrameChanged:output.fullscreenBackingBarFrame];
933  }
934
935  [findBarCocoaController_
936      positionFindBarViewAtMaxY:output.findBarMaxY
937                       maxWidth:NSWidth(output.contentAreaFrame)];
938
939  [fullscreenExitBubbleController_
940      positionInWindowAtTop:output.fullscreenExitButtonMaxY
941                      width:NSWidth(output.contentAreaFrame)];
942}
943
944- (void)updateSubviewZOrder {
945  if ([self isInAnyFullscreenMode])
946    [self updateSubviewZOrderFullscreen];
947  else
948    [self updateSubviewZOrderNormal];
949
950  [self updateSubviewZOrderHack];
951}
952
953- (void)updateSubviewZOrderNormal {
954  base::scoped_nsobject<NSMutableArray> subviews([[NSMutableArray alloc] init]);
955  if ([downloadShelfController_ view])
956    [subviews addObject:[downloadShelfController_ view]];
957  if ([bookmarkBarController_ view])
958    [subviews addObject:[bookmarkBarController_ view]];
959  if ([toolbarController_ view])
960    [subviews addObject:[toolbarController_ view]];
961  if ([infoBarContainerController_ view])
962    [subviews addObject:[infoBarContainerController_ view]];
963  if ([self tabContentArea])
964    [subviews addObject:[self tabContentArea]];
965  if ([findBarCocoaController_ view])
966    [subviews addObject:[findBarCocoaController_ view]];
967
968  [self setContentViewSubviews:subviews];
969}
970
971- (void)updateSubviewZOrderFullscreen {
972  base::scoped_nsobject<NSMutableArray> subviews([[NSMutableArray alloc] init]);
973  if ([downloadShelfController_ view])
974    [subviews addObject:[downloadShelfController_ view]];
975  if ([self tabContentArea])
976    [subviews addObject:[self tabContentArea]];
977  if ([self placeBookmarkBarBelowInfoBar]) {
978    if ([bookmarkBarController_ view])
979      [subviews addObject:[bookmarkBarController_ view]];
980    if (floatingBarBackingView_)
981      [subviews addObject:floatingBarBackingView_];
982  } else {
983    if (floatingBarBackingView_)
984      [subviews addObject:floatingBarBackingView_];
985    if ([bookmarkBarController_ view])
986      [subviews addObject:[bookmarkBarController_ view]];
987  }
988  if ([toolbarController_ view])
989    [subviews addObject:[toolbarController_ view]];
990  if ([infoBarContainerController_ view])
991    [subviews addObject:[infoBarContainerController_ view]];
992  if ([findBarCocoaController_ view])
993    [subviews addObject:[findBarCocoaController_ view]];
994
995  [self setContentViewSubviews:subviews];
996}
997
998- (void)setContentViewSubviews:(NSArray*)subviews {
999  // Subviews already match.
1000  if ([[self.window.contentView subviews] isEqual:subviews])
1001    return;
1002
1003  // The tabContentArea isn't a subview, so just set all the subviews.
1004  NSView* tabContentArea = [self tabContentArea];
1005  if (![[self.window.contentView subviews] containsObject:tabContentArea]) {
1006    [self.window.contentView setSubviews:subviews];
1007    return;
1008  }
1009
1010  // Remove all subviews that aren't the tabContentArea.
1011  for (NSView* view in [[self.window.contentView subviews] copy]) {
1012    if (view != tabContentArea)
1013      [view removeFromSuperview];
1014  }
1015
1016  // Add in the subviews below the tabContentArea.
1017  NSInteger index = [subviews indexOfObject:tabContentArea];
1018  for (int i = index - 1; i >= 0; --i) {
1019    NSView* view = [subviews objectAtIndex:i];
1020    [self.window.contentView addSubview:view
1021                             positioned:NSWindowBelow
1022                             relativeTo:nil];
1023  }
1024
1025  // Add in the subviews above the tabContentArea.
1026  for (NSUInteger i = index + 1; i < [subviews count]; ++i) {
1027    NSView* view = [subviews objectAtIndex:i];
1028    [self.window.contentView addSubview:view
1029                             positioned:NSWindowAbove
1030                             relativeTo:nil];
1031  }
1032}
1033
1034- (void)updateSubviewZOrderHack {
1035  // TODO(erikchen): Remove and then add the tabStripView to the root NSView.
1036  // This fixes a layer ordering problem that occurs between the contentView
1037  // and the tabStripView. This is a hack required because NSThemeFrame is not
1038  // layer backed, and because Chrome adds subviews directly to the
1039  // NSThemeFrame.
1040  // http://crbug.com/407921
1041  if (enteringAppKitFullscreen_) {
1042    // The tabstrip frequently lies outside the bounds of its superview.
1043    // Repeatedly adding/removing the tabstrip from its superview during the
1044    // AppKit Fullscreen transition causes graphical glitches on 10.10. The
1045    // correct solution is to use the AppKit fullscreen transition APIs added
1046    // in 10.7+.
1047    // http://crbug.com/408791
1048    if (!hasAdjustedTabStripWhileEnteringAppKitFullscreen_) {
1049      // Disable implicit animations.
1050      [CATransaction begin];
1051      [CATransaction setDisableActions:YES];
1052
1053      [self updateLayerOrdering:[self tabStripView]];
1054      [self updateLayerOrdering:[avatarButtonController_ view]];
1055
1056      [CATransaction commit];
1057      hasAdjustedTabStripWhileEnteringAppKitFullscreen_ = YES;
1058    }
1059  } else {
1060    hasAdjustedTabStripWhileEnteringAppKitFullscreen_ = NO;
1061  }
1062}
1063
1064@end  // @implementation BrowserWindowController(Private)
1065