diff options
Diffstat (limited to 'chrome/browser/ui/cocoa/browser_window_controller.mm')
-rw-r--r-- | chrome/browser/ui/cocoa/browser_window_controller.mm | 2059 |
1 files changed, 2059 insertions, 0 deletions
diff --git a/chrome/browser/ui/cocoa/browser_window_controller.mm b/chrome/browser/ui/cocoa/browser_window_controller.mm new file mode 100644 index 0000000..82151f0 --- /dev/null +++ b/chrome/browser/ui/cocoa/browser_window_controller.mm @@ -0,0 +1,2059 @@ +// Copyright (c) 2010 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_controller.h" + +#include <Carbon/Carbon.h> + +#include "app/l10n_util.h" +#include "app/l10n_util_mac.h" +#include "base/mac_util.h" +#include "app/mac/scoped_nsdisable_screen_updates.h" +#include "base/nsimage_cache_mac.h" +#import "base/scoped_nsobject.h" +#include "base/sys_string_conversions.h" +#include "chrome/app/chrome_command_ids.h" // IDC_* +#include "chrome/browser/bookmarks/bookmark_editor.h" +#include "chrome/browser/dock_info.h" +#include "chrome/browser/encoding_menu_controller.h" +#include "chrome/browser/google/google_util.h" +#include "chrome/browser/location_bar.h" +#include "chrome/browser/profile.h" +#include "chrome/browser/renderer_host/render_widget_host_view.h" +#include "chrome/browser/sync/profile_sync_service.h" +#include "chrome/browser/sync/sync_ui_util_mac.h" +#include "chrome/browser/tab_contents/tab_contents.h" +#include "chrome/browser/tab_contents/tab_contents_view_mac.h" +#include "chrome/browser/tab_contents_wrapper.h" +#include "chrome/browser/tabs/tab_strip_model.h" +#include "chrome/browser/themes/browser_theme_provider.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_list.h" +#import "chrome/browser/ui/cocoa/background_gradient_view.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h" +#import "chrome/browser/ui/cocoa/browser_window_cocoa.h" +#import "chrome/browser/ui/cocoa/browser_window_controller_private.h" +#import "chrome/browser/ui/cocoa/dev_tools_controller.h" +#import "chrome/browser/ui/cocoa/download/download_shelf_controller.h" +#import "chrome/browser/ui/cocoa/event_utils.h" +#import "chrome/browser/ui/cocoa/fast_resize_view.h" +#import "chrome/browser/ui/cocoa/find_bar_bridge.h" +#import "chrome/browser/ui/cocoa/find_bar_cocoa_controller.h" +#import "chrome/browser/ui/cocoa/focus_tracker.h" +#import "chrome/browser/ui/cocoa/fullscreen_controller.h" +#import "chrome/browser/ui/cocoa/fullscreen_window.h" +#import "chrome/browser/ui/cocoa/infobar_container_controller.h" +#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h" +#import "chrome/browser/ui/cocoa/previewable_contents_controller.h" +#import "chrome/browser/ui/cocoa/nswindow_additions.h" +#import "chrome/browser/ui/cocoa/sad_tab_controller.h" +#import "chrome/browser/ui/cocoa/sidebar_controller.h" +#import "chrome/browser/ui/cocoa/status_bubble_mac.h" +#import "chrome/browser/ui/cocoa/tab_contents_controller.h" +#import "chrome/browser/ui/cocoa/tab_strip_controller.h" +#import "chrome/browser/ui/cocoa/tab_strip_view.h" +#import "chrome/browser/ui/cocoa/tab_view.h" +#import "chrome/browser/ui/cocoa/tabpose_window.h" +#import "chrome/browser/ui/cocoa/toolbar_controller.h" +#include "chrome/browser/window_sizer.h" +#include "chrome/common/url_constants.h" +#include "grit/generated_resources.h" +#include "grit/locale_settings.h" + +// ORGANIZATION: This is a big file. It is (in principle) organized as follows +// (in order): +// 1. Interfaces. Very short, one-time-use classes may include an implementation +// immediately after their interface. +// 2. The general implementation section, ordered as follows: +// i. Public methods and overrides. +// ii. Overrides/implementations of undocumented methods. +// iii. Delegate methods for various protocols, formal and informal, to which +// |BrowserWindowController| conforms. +// 3. (temporary) Implementation sections for various categories. +// +// Private methods are defined and implemented separately in +// browser_window_controller_private.{h,mm}. +// +// Not all of the above guidelines are followed and more (re-)organization is +// needed. BUT PLEASE TRY TO KEEP THIS FILE ORGANIZED. I'd rather re-organize as +// little as possible, since doing so messes up the file's history. +// +// TODO(viettrungluu): [crbug.com/35543] on-going re-organization, splitting +// things into multiple files -- the plan is as follows: +// - in general, everything stays in browser_window_controller.h, but is split +// off into categories (see below) +// - core stuff stays in browser_window_controller.mm +// - ... overrides also stay (without going into a category, in particular) +// - private stuff which everyone needs goes into +// browser_window_controller_private.{h,mm}; if no one else needs them, they +// can go in individual files (see below) +// - area/task-specific stuff go in browser_window_controller_<area>.mm +// - ... in categories called "(<Area>)" or "(<PrivateArea>)" +// Plan of action: +// - first re-organize into categories +// - then split into files + +// Notes on self-inflicted (not user-inflicted) window resizing and moving: +// +// When the bookmark bar goes from hidden to shown (on a non-NTP) page, or when +// the download shelf goes from hidden to shown, we grow the window downwards in +// order to maintain a constant content area size. When either goes from shown +// to hidden, we consequently shrink the window from the bottom, also to keep +// the content area size constant. To keep things simple, if the window is not +// entirely on-screen, we don't grow/shrink the window. +// +// The complications come in when there isn't enough room (on screen) below the +// window to accomodate the growth. In this case, we grow the window first +// downwards, and then upwards. So, when it comes to shrinking, we do the +// opposite: shrink from the top by the amount by which we grew at the top, and +// then from the bottom -- unless the user moved/resized/zoomed the window, in +// which case we "reset state" and just shrink from the bottom. +// +// A further complication arises due to the way in which "zoom" ("maximize") +// works on Mac OS X. Basically, for our purposes, a window is "zoomed" whenever +// it occupies the full available vertical space. (Note that the green zoom +// button does not track zoom/unzoomed state per se, but basically relies on +// this heuristic.) We don't, in general, want to shrink the window if the +// window is zoomed (scenario: window is zoomed, download shelf opens -- which +// doesn't cause window growth, download shelf closes -- shouldn't cause the +// window to become unzoomed!). However, if we grew the window +// (upwards/downwards) to become zoomed in the first place, we *should* shrink +// the window by the amounts by which we grew (scenario: window occupies *most* +// of vertical space, download shelf opens causing growth so that window +// occupies all of vertical space -- i.e., window is effectively zoomed, +// download shelf closes -- should return the window to its previous state). +// +// A major complication is caused by the way grows/shrinks are handled and +// animated. Basically, the BWC doesn't see the global picture, but it sees +// grows and shrinks in small increments (as dictated by the animation). Thus +// window growth/shrinkage (at the top/bottom) have to be tracked incrementally. +// Allowing shrinking from the zoomed state also requires tracking: We check on +// any shrink whether we're both zoomed and have previously grown -- if so, we +// set a flag, and constrain any resize by the allowed amounts. On further +// shrinks, we check the flag (since the size/position of the window will no +// longer indicate that the window is shrinking from an apparent zoomed state) +// and if it's set we continue to constrain the resize. + + +@interface NSWindow(NSPrivateApis) +// Note: These functions are private, use -[NSObject respondsToSelector:] +// before calling them. + +- (void)setBottomCornerRounded:(BOOL)rounded; + +- (NSRect)_growBoxRect; + +@end + + +// IncognitoImageView subclasses NSImageView to allow mouse events to pass +// through it so you can drag the window by dragging on the spy guy +@interface IncognitoImageView : NSImageView +@end + +@implementation IncognitoImageView +- (BOOL)mouseDownCanMoveWindow { + return YES; +} +@end + + +@implementation BrowserWindowController + ++ (BrowserWindowController*)browserWindowControllerForWindow:(NSWindow*)window { + while (window) { + id controller = [window windowController]; + if ([controller isKindOfClass:[BrowserWindowController class]]) + return (BrowserWindowController*)controller; + window = [window parentWindow]; + } + return nil; +} + ++ (BrowserWindowController*)browserWindowControllerForView:(NSView*)view { + NSWindow* window = [view window]; + return [BrowserWindowController browserWindowControllerForWindow:window]; +} + +// Load the browser window nib and do any Cocoa-specific initialization. +// Takes ownership of |browser|. Note that the nib also sets this controller +// up as the window's delegate. +- (id)initWithBrowser:(Browser*)browser { + return [self initWithBrowser:browser takeOwnership:YES]; +} + +// Private(TestingAPI) init routine with testing options. +- (id)initWithBrowser:(Browser*)browser takeOwnership:(BOOL)ownIt { + // Use initWithWindowNibPath:: instead of initWithWindowNibName: so we + // can override it in a unit test. + NSString* nibpath = [mac_util::MainAppBundle() + pathForResource:@"BrowserWindow" + ofType:@"nib"]; + if ((self = [super initWithWindowNibPath:nibpath owner:self])) { + DCHECK(browser); + initializing_ = YES; + browser_.reset(browser); + ownsBrowser_ = ownIt; + NSWindow* window = [self window]; + windowShim_.reset(new BrowserWindowCocoa(browser, self, window)); + + // Create the bar visibility lock set; 10 is arbitrary, but should hopefully + // be big enough to hold all locks that'll ever be needed. + barVisibilityLocks_.reset([[NSMutableSet setWithCapacity:10] retain]); + + // Sets the window to not have rounded corners, which prevents + // the resize control from being inset slightly and looking ugly. + if ([window respondsToSelector:@selector(setBottomCornerRounded:)]) + [window setBottomCornerRounded:NO]; + + // Get the most appropriate size for the window, then enforce the + // minimum width and height. The window shim will handle flipping + // the coordinates for us so we can use it to save some code. + // Note that this may leave a significant portion of the window + // offscreen, but there will always be enough window onscreen to + // drag the whole window back into view. + NSSize minSize = [[self window] minSize]; + gfx::Rect desiredContentRect = browser_->GetSavedWindowBounds(); + gfx::Rect windowRect = desiredContentRect; + if (windowRect.width() < minSize.width) + windowRect.set_width(minSize.width); + if (windowRect.height() < minSize.height) + windowRect.set_height(minSize.height); + + // When we are given x/y coordinates of 0 on a created popup window, assume + // none were given by the window.open() command. + if (browser_->type() & Browser::TYPE_POPUP && + windowRect.x() == 0 && windowRect.y() == 0) { + gfx::Size size = windowRect.size(); + windowRect.set_origin(WindowSizer::GetDefaultPopupOrigin(size)); + } + + // Size and position the window. Note that it is not yet onscreen. Popup + // windows may get resized later on in this function, once the actual size + // of the toolbar/tabstrip is known. + windowShim_->SetBounds(windowRect); + + // Puts the incognito badge on the window frame, if necessary. + [self installIncognitoBadge]; + + // Create a sub-controller for the docked devTools and add its view to the + // hierarchy. This must happen before the sidebar controller is + // instantiated. + devToolsController_.reset( + [[DevToolsController alloc] initWithDelegate:self]); + [[devToolsController_ view] setFrame:[[self tabContentArea] bounds]]; + [[self tabContentArea] addSubview:[devToolsController_ view]]; + + // Create a sub-controller for the docked sidebar and add its view to the + // hierarchy. This must happen before the previewable contents controller + // is instantiated. + sidebarController_.reset([[SidebarController alloc] initWithDelegate:self]); + [[sidebarController_ view] setFrame:[[devToolsController_ view] bounds]]; + [[devToolsController_ view] addSubview:[sidebarController_ view]]; + + // Create the previewable contents controller. This provides the switch + // view that TabStripController needs. + previewableContentsController_.reset( + [[PreviewableContentsController alloc] init]); + [[previewableContentsController_ view] + setFrame:[[sidebarController_ view] bounds]]; + [[sidebarController_ view] + addSubview:[previewableContentsController_ view]]; + + // Create a controller for the tab strip, giving it the model object for + // this window's Browser and the tab strip view. The controller will handle + // registering for the appropriate tab notifications from the back-end and + // managing the creation of new tabs. + [self createTabStripController]; + + // Create the infobar container view, so we can pass it to the + // ToolbarController. + infoBarContainerController_.reset( + [[InfoBarContainerController alloc] initWithResizeDelegate:self]); + [[[self window] contentView] addSubview:[infoBarContainerController_ view]]; + + // Create a controller for the toolbar, giving it the toolbar model object + // and the toolbar view from the nib. The controller will handle + // registering for the appropriate command state changes from the back-end. + // Adds the toolbar to the content area. + toolbarController_.reset([[ToolbarController alloc] + initWithModel:browser->toolbar_model() + commands:browser->command_updater() + profile:browser->profile() + browser:browser + resizeDelegate:self]); + [toolbarController_ setHasToolbar:[self hasToolbar] + hasLocationBar:[self hasLocationBar]]; + [[[self window] contentView] addSubview:[toolbarController_ view]]; + + // Create a sub-controller for the bookmark bar. + bookmarkBarController_.reset( + [[BookmarkBarController alloc] + initWithBrowser:browser_.get() + initialWidth:NSWidth([[[self window] contentView] frame]) + delegate:self + resizeDelegate:self]); + + // Add bookmark bar to the view hierarchy, which also triggers the nib load. + // The bookmark bar is defined (in the nib) to be bottom-aligned to its + // parent view (among other things), so position and resize properties don't + // need to be set. + [[[self window] contentView] addSubview:[bookmarkBarController_ view] + positioned:NSWindowBelow + relativeTo:[toolbarController_ view]]; + [bookmarkBarController_ setBookmarkBarEnabled:[self supportsBookmarkBar]]; + + // We don't want to try and show the bar before it gets placed in its parent + // view, so this step shoudn't be inside the bookmark bar controller's + // |-awakeFromNib|. + [self updateBookmarkBarVisibilityWithAnimation:NO]; + + // Allow bar visibility to be changed. + [self enableBarVisibilityUpdates]; + + // Force a relayout of all the various bars. + [self layoutSubviews]; + + // For a popup window, |desiredContentRect| contains the desired height of + // the content, not of the whole window. Now that all the views are laid + // out, measure the current content area size and grow if needed. The + // window has not been placed onscreen yet, so this extra resize will not + // cause visible jank. + if (browser_->type() & Browser::TYPE_POPUP) { + CGFloat deltaH = desiredContentRect.height() - + NSHeight([[self tabContentArea] frame]); + // Do not shrink the window, as that may break minimum size invariants. + if (deltaH > 0) { + // Convert from tabContentArea coordinates to window coordinates. + NSSize convertedSize = + [[self tabContentArea] convertSize:NSMakeSize(0, deltaH) + toView:nil]; + NSRect frame = [[self window] frame]; + frame.size.height += convertedSize.height; + frame.origin.y -= convertedSize.height; + [[self window] setFrame:frame display:NO]; + } + } + + // Create the bridge for the status bubble. + statusBubble_ = new StatusBubbleMac([self window], self); + + // Register for application hide/unhide notifications. + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(applicationDidHide:) + name:NSApplicationDidHideNotification + object:nil]; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(applicationDidUnhide:) + name:NSApplicationDidUnhideNotification + object:nil]; + + // This must be done after the view is added to the window since it relies + // on the window bounds to determine whether to show buttons or not. + if ([self hasToolbar]) // Do not create the buttons in popups. + [toolbarController_ createBrowserActionButtons]; + + // We are done initializing now. + initializing_ = NO; + } + return self; +} + +- (void)dealloc { + browser_->CloseAllTabs(); + [downloadShelfController_ exiting]; + + // Explicitly release |fullscreenController_| here, as it may call back to + // this BWC in |-dealloc|. We are required to call |-exitFullscreen| before + // releasing the controller. + [fullscreenController_ exitFullscreen]; + fullscreenController_.reset(); + + // Under certain testing configurations we may not actually own the browser. + if (ownsBrowser_ == NO) + ignore_result(browser_.release()); + + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + [super dealloc]; +} + +- (BrowserWindow*)browserWindow { + return windowShim_.get(); +} + +- (ToolbarController*)toolbarController { + return toolbarController_.get(); +} + +- (TabStripController*)tabStripController { + return tabStripController_.get(); +} + +- (StatusBubbleMac*)statusBubble { + return statusBubble_; +} + +- (LocationBarViewMac*)locationBarBridge { + return [toolbarController_ locationBarBridge]; +} + +- (void)destroyBrowser { + [NSApp removeWindowsItem:[self window]]; + + // We need the window to go away now. + // We can't actually use |-autorelease| here because there's an embedded + // run loop in the |-performClose:| which contains its own autorelease pool. + // Instead call it after a zero-length delay, which gets us back to the main + // event loop. + [self performSelector:@selector(autorelease) + withObject:nil + afterDelay:0]; +} + +// Called when the window meets the criteria to be closed (ie, +// |-windowShouldClose:| returns YES). We must be careful to preserve the +// semantics of BrowserWindow::Close() and not call the Browser's dtor directly +// from this method. +- (void)windowWillClose:(NSNotification*)notification { + DCHECK_EQ([notification object], [self window]); + DCHECK(browser_->tabstrip_model()->empty()); + [savedRegularWindow_ close]; + // We delete statusBubble here because we need to kill off the dependency + // that its window has on our window before our window goes away. + delete statusBubble_; + statusBubble_ = NULL; + // We can't actually use |-autorelease| here because there's an embedded + // run loop in the |-performClose:| which contains its own autorelease pool. + // Instead call it after a zero-length delay, which gets us back to the main + // event loop. + [self performSelector:@selector(autorelease) + withObject:nil + afterDelay:0]; +} + +- (void)attachConstrainedWindow:(ConstrainedWindowMac*)window { + [tabStripController_ attachConstrainedWindow:window]; +} + +- (void)removeConstrainedWindow:(ConstrainedWindowMac*)window { + [tabStripController_ removeConstrainedWindow:window]; +} + +- (BOOL)canAttachConstrainedWindow { + return ![previewableContentsController_ isShowingPreview]; +} + +- (void)updateDevToolsForContents:(TabContents*)contents { + [devToolsController_ updateDevToolsForTabContents:contents]; + [devToolsController_ ensureContentsVisible]; +} + +- (void)updateSidebarForContents:(TabContents*)contents { + [sidebarController_ updateSidebarForTabContents:contents]; + [sidebarController_ ensureContentsVisible]; +} + +// Called when the user wants to close a window or from the shutdown process. +// The Browser object is in control of whether or not we're allowed to close. It +// may defer closing due to several states, such as onUnload handlers needing to +// be fired. If closing is deferred, the Browser will handle the processing +// required to get us to the closing state and (by watching for all the tabs +// going away) will again call to close the window when it's finally ready. +- (BOOL)windowShouldClose:(id)sender { + // Disable updates while closing all tabs to avoid flickering. + app::mac::ScopedNSDisableScreenUpdates disabler; + // Give beforeunload handlers the chance to cancel the close before we hide + // the window below. + if (!browser_->ShouldCloseWindow()) + return NO; + + // saveWindowPositionIfNeeded: only works if we are the last active + // window, but orderOut: ends up activating another window, so we + // have to save the window position before we call orderOut:. + [self saveWindowPositionIfNeeded]; + + if (!browser_->tabstrip_model()->empty()) { + // Tab strip isn't empty. Hide the frame (so it appears to have closed + // immediately) and close all the tabs, allowing the renderers to shut + // down. When the tab strip is empty we'll be called back again. + [[self window] orderOut:self]; + browser_->OnWindowClosing(); + return NO; + } + + // the tab strip is empty, it's ok to close the window + return YES; +} + +// Called right after our window became the main window. +- (void)windowDidBecomeMain:(NSNotification*)notification { + BrowserList::SetLastActive(browser_.get()); + [self saveWindowPositionIfNeeded]; + + // TODO(dmaclach): Instead of redrawing the whole window, views that care + // about the active window state should be registering for notifications. + [[self window] setViewsNeedDisplay:YES]; + + // TODO(viettrungluu): For some reason, the above doesn't suffice. + if ([self isFullscreen]) + [floatingBarBackingView_ setNeedsDisplay:YES]; // Okay even if nil. +} + +- (void)windowDidResignMain:(NSNotification*)notification { + // TODO(dmaclach): Instead of redrawing the whole window, views that care + // about the active window state should be registering for notifications. + [[self window] setViewsNeedDisplay:YES]; + + // TODO(viettrungluu): For some reason, the above doesn't suffice. + if ([self isFullscreen]) + [floatingBarBackingView_ setNeedsDisplay:YES]; // Okay even if nil. +} + +// Called when we are activated (when we gain focus). +- (void)windowDidBecomeKey:(NSNotification*)notification { + // We need to activate the controls (in the "WebView"). To do this, get the + // selected TabContents's RenderWidgetHostViewMac and tell it to activate. + if (TabContents* contents = browser_->GetSelectedTabContents()) { + if (RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView()) + rwhv->SetActive(true); + } +} + +// Called when we are deactivated (when we lose focus). +- (void)windowDidResignKey:(NSNotification*)notification { + // If our app is still active and we're still the key window, ignore this + // message, since it just means that a menu extra (on the "system status bar") + // was activated; we'll get another |-windowDidResignKey| if we ever really + // lose key window status. + if ([NSApp isActive] && ([NSApp keyWindow] == [self window])) + return; + + // We need to deactivate the controls (in the "WebView"). To do this, get the + // selected TabContents's RenderWidgetHostView and tell it to deactivate. + if (TabContents* contents = browser_->GetSelectedTabContents()) { + if (RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView()) + rwhv->SetActive(false); + } +} + +// Called when we have been minimized. +- (void)windowDidMiniaturize:(NSNotification *)notification { + // Let the selected RenderWidgetHostView know, so that it can tell plugins. + if (TabContents* contents = browser_->GetSelectedTabContents()) { + if (RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView()) + rwhv->SetWindowVisibility(false); + } +} + +// Called when we have been unminimized. +- (void)windowDidDeminiaturize:(NSNotification *)notification { + // Let the selected RenderWidgetHostView know, so that it can tell plugins. + if (TabContents* contents = browser_->GetSelectedTabContents()) { + if (RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView()) + rwhv->SetWindowVisibility(true); + } +} + +// Called when the application has been hidden. +- (void)applicationDidHide:(NSNotification *)notification { + // Let the selected RenderWidgetHostView know, so that it can tell plugins + // (unless we are minimized, in which case nothing has really changed). + if (![[self window] isMiniaturized]) { + if (TabContents* contents = browser_->GetSelectedTabContents()) { + if (RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView()) + rwhv->SetWindowVisibility(false); + } + } +} + +// Called when the application has been unhidden. +- (void)applicationDidUnhide:(NSNotification *)notification { + // Let the selected RenderWidgetHostView know, so that it can tell plugins + // (unless we are minimized, in which case nothing has really changed). + if (![[self window] isMiniaturized]) { + if (TabContents* contents = browser_->GetSelectedTabContents()) { + if (RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView()) + rwhv->SetWindowVisibility(true); + } + } +} + +// Called when the user clicks the zoom button (or selects it from the Window +// menu) to determine the "standard size" of the window, based on the content +// and other factors. If the current size/location differs nontrivally from the +// standard size, Cocoa resizes the window to the standard size, and saves the +// current size as the "user size". If the current size/location is the same (up +// to a fudge factor) as the standard size, Cocoa resizes the window to the +// saved user size. (It is possible for the two to coincide.) In this way, the +// zoom button acts as a toggle. We determine the standard size based on the +// content, but enforce a minimum width (calculated using the dimensions of the +// screen) to ensure websites with small intrinsic width (such as google.com) +// don't end up with a wee window. Moreover, we always declare the standard +// width to be at least as big as the current width, i.e., we never want zooming +// to the standard width to shrink the window. This is consistent with other +// browsers' behaviour, and is desirable in multi-tab situations. Note, however, +// that the "toggle" behaviour means that the window can still be "unzoomed" to +// the user size. +- (NSRect)windowWillUseStandardFrame:(NSWindow*)window + defaultFrame:(NSRect)frame { + // Forget that we grew the window up (if we in fact did). + [self resetWindowGrowthState]; + + // |frame| already fills the current screen. Never touch y and height since we + // always want to fill vertically. + + // If the shift key is down, maximize. Hopefully this should make the + // "switchers" happy. + if ([[NSApp currentEvent] modifierFlags] & NSShiftKeyMask) { + return frame; + } + + // To prevent strange results on portrait displays, the basic minimum zoomed + // width is the larger of: 60% of available width, 60% of available height + // (bounded by available width). + const CGFloat kProportion = 0.6; + CGFloat zoomedWidth = + std::max(kProportion * frame.size.width, + std::min(kProportion * frame.size.height, frame.size.width)); + + TabContents* contents = browser_->GetSelectedTabContents(); + if (contents) { + // If the intrinsic width is bigger, then make it the zoomed width. + const int kScrollbarWidth = 16; // TODO(viettrungluu): ugh. + TabContentsViewMac* tab_contents_view = + static_cast<TabContentsViewMac*>(contents->view()); + CGFloat intrinsicWidth = static_cast<CGFloat>( + tab_contents_view->preferred_width() + kScrollbarWidth); + zoomedWidth = std::max(zoomedWidth, + std::min(intrinsicWidth, frame.size.width)); + } + + // Never shrink from the current size on zoom (see above). + NSRect currentFrame = [[self window] frame]; + zoomedWidth = std::max(zoomedWidth, currentFrame.size.width); + + // |frame| determines our maximum extents. We need to set the origin of the + // frame -- and only move it left if necessary. + if (currentFrame.origin.x + zoomedWidth > frame.origin.x + frame.size.width) + frame.origin.x = frame.origin.x + frame.size.width - zoomedWidth; + else + frame.origin.x = currentFrame.origin.x; + + // Set the width. Don't touch y or height. + frame.size.width = zoomedWidth; + + return frame; +} + +- (void)activate { + [[self window] makeKeyAndOrderFront:self]; +} + +// Determine whether we should let a window zoom/unzoom to the given |newFrame|. +// We avoid letting unzoom move windows between screens, because it's really +// strange and unintuitive. +- (BOOL)windowShouldZoom:(NSWindow*)window toFrame:(NSRect)newFrame { + // Figure out which screen |newFrame| is on. + NSScreen* newScreen = nil; + CGFloat newScreenOverlapArea = 0.0; + for (NSScreen* screen in [NSScreen screens]) { + NSRect overlap = NSIntersectionRect(newFrame, [screen frame]); + CGFloat overlapArea = overlap.size.width * overlap.size.height; + if (overlapArea > newScreenOverlapArea) { + newScreen = screen; + newScreenOverlapArea = overlapArea; + } + } + // If we're somehow not on any screen, allow the zoom. + if (!newScreen) + return YES; + + // If the new screen is the current screen, we can return a definitive YES. + // Note: This check is not strictly necessary, but just short-circuits in the + // "no-brainer" case. To test the complicated logic below, comment this out! + NSScreen* curScreen = [window screen]; + if (newScreen == curScreen) + return YES; + + // Worry a little: What happens when a window is on two (or more) screens? + // E.g., what happens in a 50-50 scenario? Cocoa may reasonably elect to zoom + // to the other screen rather than staying on the officially current one. So + // we compare overlaps with the current window frame, and see if Cocoa's + // choice was reasonable (allowing a small rounding error). This should + // hopefully avoid us ever erroneously denying a zoom when a window is on + // multiple screens. + NSRect curFrame = [window frame]; + NSRect newScrIntersectCurFr = NSIntersectionRect([newScreen frame], curFrame); + NSRect curScrIntersectCurFr = NSIntersectionRect([curScreen frame], curFrame); + if (newScrIntersectCurFr.size.width*newScrIntersectCurFr.size.height >= + (curScrIntersectCurFr.size.width*curScrIntersectCurFr.size.height - 1.0)) + return YES; + + // If it wasn't reasonable, return NO. + return NO; +} + +// Adjusts the window height by the given amount. +- (void)adjustWindowHeightBy:(CGFloat)deltaH { + // By not adjusting the window height when initializing, we can ensure that + // the window opens with the same size that was saved on close. + if (initializing_ || [self isFullscreen] || deltaH == 0) + return; + + NSWindow* window = [self window]; + NSRect windowFrame = [window frame]; + NSRect workarea = [[window screen] visibleFrame]; + + // If the window is not already fully in the workarea, do not adjust its frame + // at all. + if (!NSContainsRect(workarea, windowFrame)) + return; + + // Record the position of the top/bottom of the window, so we can easily check + // whether we grew the window upwards/downwards. + CGFloat oldWindowMaxY = NSMaxY(windowFrame); + CGFloat oldWindowMinY = NSMinY(windowFrame); + + // We are "zoomed" if we occupy the full vertical space. + bool isZoomed = (windowFrame.origin.y == workarea.origin.y && + windowFrame.size.height == workarea.size.height); + + // If we're shrinking the window.... + if (deltaH < 0) { + bool didChange = false; + + // Don't reset if not currently zoomed since shrinking can take several + // steps! + if (isZoomed) + isShrinkingFromZoomed_ = YES; + + // If we previously grew at the top, shrink as much as allowed at the top + // first. + if (windowTopGrowth_ > 0) { + CGFloat shrinkAtTopBy = MIN(-deltaH, windowTopGrowth_); + windowFrame.size.height -= shrinkAtTopBy; // Shrink the window. + deltaH += shrinkAtTopBy; // Update the amount left to shrink. + windowTopGrowth_ -= shrinkAtTopBy; // Update the growth state. + didChange = true; + } + + // Similarly for the bottom (not an "else if" since we may have to + // simultaneously shrink at both the top and at the bottom). Note that + // |deltaH| may no longer be nonzero due to the above. + if (deltaH < 0 && windowBottomGrowth_ > 0) { + CGFloat shrinkAtBottomBy = MIN(-deltaH, windowBottomGrowth_); + windowFrame.origin.y += shrinkAtBottomBy; // Move the window up. + windowFrame.size.height -= shrinkAtBottomBy; // Shrink the window. + deltaH += shrinkAtBottomBy; // Update the amount left.... + windowBottomGrowth_ -= shrinkAtBottomBy; // Update the growth state. + didChange = true; + } + + // If we're shrinking from zoomed but we didn't change the top or bottom + // (since we've reached the limits imposed by |window...Growth_|), then stop + // here. Don't reset |isShrinkingFromZoomed_| since we might get called + // again for the same shrink. + if (isShrinkingFromZoomed_ && !didChange) + return; + } else { + isShrinkingFromZoomed_ = NO; + + // Don't bother with anything else. + if (isZoomed) + return; + } + + // Shrinking from zoomed is handled above (and is constrained by + // |window...Growth_|). + if (!isShrinkingFromZoomed_) { + // Resize the window down until it hits the bottom of the workarea, then if + // needed continue resizing upwards. Do not resize the window to be taller + // than the current workarea. + // Resize the window as requested, keeping the top left corner fixed. + windowFrame.origin.y -= deltaH; + windowFrame.size.height += deltaH; + + // If the bottom left corner is now outside the visible frame, move the + // window up to make it fit, but make sure not to move the top left corner + // out of the visible frame. + if (windowFrame.origin.y < workarea.origin.y) { + windowFrame.origin.y = workarea.origin.y; + windowFrame.size.height = + std::min(windowFrame.size.height, workarea.size.height); + } + + // Record (if applicable) how much we grew the window in either direction. + // (N.B.: These only record growth, not shrinkage.) + if (NSMaxY(windowFrame) > oldWindowMaxY) + windowTopGrowth_ += NSMaxY(windowFrame) - oldWindowMaxY; + if (NSMinY(windowFrame) < oldWindowMinY) + windowBottomGrowth_ += oldWindowMinY - NSMinY(windowFrame); + } + + // Disable subview resizing while resizing the window, or else we will get + // unwanted renderer resizes. The calling code must call layoutSubviews to + // make things right again. + NSView* contentView = [window contentView]; + [contentView setAutoresizesSubviews:NO]; + [window setFrame:windowFrame display:NO]; + [contentView setAutoresizesSubviews:YES]; +} + +// Main method to resize browser window subviews. This method should be called +// when resizing any child of the content view, rather than resizing the views +// directly. If the view is already the correct height, does not force a +// relayout. +- (void)resizeView:(NSView*)view newHeight:(CGFloat)height { + // We should only ever be called for one of the following four views. + // |downloadShelfController_| may be nil. If we are asked to size the bookmark + // bar directly, its superview must be this controller's content view. + DCHECK(view); + DCHECK(view == [toolbarController_ view] || + view == [infoBarContainerController_ view] || + view == [downloadShelfController_ view] || + view == [bookmarkBarController_ view]); + + // Change the height of the view and call |-layoutSubViews|. We set the height + // here without regard to where the view is on the screen or whether it needs + // to "grow up" or "grow down." The below call to |-layoutSubviews| will + // position each view correctly. + NSRect frame = [view frame]; + if (NSHeight(frame) == height) + return; + + // Grow or shrink the window by the amount of the height change. We adjust + // the window height only in two cases: + // 1) We are adjusting the height of the bookmark bar and it is currently + // animating either open or closed. + // 2) We are adjusting the height of the download shelf. + // + // We do not adjust the window height for bookmark bar changes on the NTP. + BOOL shouldAdjustBookmarkHeight = + [bookmarkBarController_ isAnimatingBetweenState:bookmarks::kHiddenState + andState:bookmarks::kShowingState]; + if ((shouldAdjustBookmarkHeight && view == [bookmarkBarController_ view]) || + view == [downloadShelfController_ view]) { + [[self window] disableScreenUpdatesUntilFlush]; + CGFloat deltaH = height - frame.size.height; + [self adjustWindowHeightBy:deltaH]; + } + + frame.size.height = height; + // TODO(rohitrao): Determine if calling setFrame: twice is bad. + [view setFrame:frame]; + [self layoutSubviews]; +} + +- (void)setAnimationInProgress:(BOOL)inProgress { + [[self tabContentArea] setFastResizeMode:inProgress]; +} + +// Update a toggle state for an NSMenuItem if modified. +// Take care to ensure |item| looks like a NSMenuItem. +// Called by validateUserInterfaceItem:. +- (void)updateToggleStateWithTag:(NSInteger)tag forItem:(id)item { + if (![item respondsToSelector:@selector(state)] || + ![item respondsToSelector:@selector(setState:)]) + return; + + // On Windows this logic happens in bookmark_bar_view.cc. On the + // Mac we're a lot more MVC happy so we've moved it into a + // controller. To be clear, this simply updates the menu item; it + // does not display the bookmark bar itself. + if (tag == IDC_SHOW_BOOKMARK_BAR) { + bool toggled = windowShim_->IsBookmarkBarVisible(); + NSInteger oldState = [item state]; + NSInteger newState = toggled ? NSOnState : NSOffState; + if (oldState != newState) + [item setState:newState]; + } + + // Update the checked/Unchecked state of items in the encoding menu. + // On Windows, this logic is part of |EncodingMenuModel| in + // browser/views/toolbar_view.h. + EncodingMenuController encoding_controller; + if (encoding_controller.DoesCommandBelongToEncodingMenu(tag)) { + DCHECK(browser_.get()); + Profile* profile = browser_->profile(); + DCHECK(profile); + TabContents* current_tab = browser_->GetSelectedTabContents(); + if (!current_tab) { + return; + } + const std::string encoding = current_tab->encoding(); + + bool toggled = encoding_controller.IsItemChecked(profile, encoding, tag); + NSInteger oldState = [item state]; + NSInteger newState = toggled ? NSOnState : NSOffState; + if (oldState != newState) + [item setState:newState]; + } +} + +- (BOOL)supportsFullscreen { + // TODO(avi, thakis): GTMWindowSheetController has no api to move + // tabsheets between windows. Until then, we have to prevent having to + // move a tabsheet between windows, e.g. no fullscreen toggling + NSArray* a = [[tabStripController_ sheetController] viewsWithAttachedSheets]; + return [a count] == 0; +} + +// Called to validate menu and toolbar items when this window is key. All the +// items we care about have been set with the |-commandDispatch:| or +// |-commandDispatchUsingKeyModifiers:| actions and a target of FirstResponder +// in IB. If it's not one of those, let it continue up the responder chain to be +// handled elsewhere. We pull out the tag as the cross-platform constant to +// differentiate and dispatch the various commands. +// NOTE: we might have to handle state for app-wide menu items, +// although we could cheat and directly ask the app controller if our +// command_updater doesn't support the command. This may or may not be an issue, +// too early to tell. +- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item { + SEL action = [item action]; + BOOL enable = NO; + if (action == @selector(commandDispatch:) || + action == @selector(commandDispatchUsingKeyModifiers:)) { + NSInteger tag = [item tag]; + if (browser_->command_updater()->SupportsCommand(tag)) { + // Generate return value (enabled state) + enable = browser_->command_updater()->IsCommandEnabled(tag); + switch (tag) { + case IDC_CLOSE_TAB: + // Disable "close tab" if we're not the key window or if there's only + // one tab. + enable &= [self numberOfTabs] > 1 && [[self window] isKeyWindow]; + break; + case IDC_FULLSCREEN: { + enable &= [self supportsFullscreen]; + if ([static_cast<NSObject*>(item) isKindOfClass:[NSMenuItem class]]) { + NSString* menuTitle = l10n_util::GetNSString( + [self isFullscreen] ? IDS_EXIT_FULLSCREEN_MAC : + IDS_ENTER_FULLSCREEN_MAC); + [static_cast<NSMenuItem*>(item) setTitle:menuTitle]; + } + break; + } + case IDC_SYNC_BOOKMARKS: + enable &= browser_->profile()->IsSyncAccessible(); + sync_ui_util::UpdateSyncItem(item, enable, browser_->profile()); + break; + default: + // Special handling for the contents of the Text Encoding submenu. On + // Mac OS, instead of enabling/disabling the top-level menu item, we + // enable/disable the submenu's contents (per Apple's HIG). + EncodingMenuController encoding_controller; + if (encoding_controller.DoesCommandBelongToEncodingMenu(tag)) { + enable &= browser_->command_updater()->IsCommandEnabled( + IDC_ENCODING_MENU) ? YES : NO; + } + } + + // If the item is toggleable, find its toggle state and + // try to update it. This is a little awkward, but the alternative is + // to check after a commandDispatch, which seems worse. + [self updateToggleStateWithTag:tag forItem:item]; + } + } + return enable; +} + +// Called when the user picks a menu or toolbar item when this window is key. +// Calls through to the browser object to execute the command. This assumes that +// the command is supported and doesn't check, otherwise it would have been +// disabled in the UI in validateUserInterfaceItem:. +- (void)commandDispatch:(id)sender { + DCHECK(sender); + // Identify the actual BWC to which the command should be dispatched. It might + // belong to a background window, yet this controller gets it because it is + // the foreground window's controller and thus in the responder chain. Some + // senders don't have this problem (for example, menus only operate on the + // foreground window), so this is only an issue for senders that are part of + // windows. + BrowserWindowController* targetController = self; + if ([sender respondsToSelector:@selector(window)]) + targetController = [[sender window] windowController]; + DCHECK([targetController isKindOfClass:[BrowserWindowController class]]); + DCHECK(targetController->browser_.get()); + targetController->browser_->ExecuteCommand([sender tag]); +} + +// Same as |-commandDispatch:|, but executes commands using a disposition +// determined by the key flags. If the window is in the background and the +// command key is down, ignore the command key, but process any other modifiers. +- (void)commandDispatchUsingKeyModifiers:(id)sender { + DCHECK(sender); + // See comment above for why we do this. + BrowserWindowController* targetController = self; + if ([sender respondsToSelector:@selector(window)]) + targetController = [[sender window] windowController]; + DCHECK([targetController isKindOfClass:[BrowserWindowController class]]); + NSInteger command = [sender tag]; + NSUInteger modifierFlags = [[NSApp currentEvent] modifierFlags]; + if ((command == IDC_RELOAD) && + (modifierFlags & (NSShiftKeyMask | NSControlKeyMask))) { + command = IDC_RELOAD_IGNORING_CACHE; + // Mask off Shift and Control so they don't affect the disposition below. + modifierFlags &= ~(NSShiftKeyMask | NSControlKeyMask); + } + if (![[sender window] isMainWindow]) { + // Remove the command key from the flags, it means "keep the window in + // the background" in this case. + modifierFlags &= ~NSCommandKeyMask; + } + WindowOpenDisposition disposition = + event_utils::WindowOpenDispositionFromNSEventWithFlags( + [NSApp currentEvent], modifierFlags); + switch (command) { + case IDC_BACK: + case IDC_FORWARD: + case IDC_RELOAD: + case IDC_RELOAD_IGNORING_CACHE: + if (disposition == CURRENT_TAB) { + // Forcibly reset the location bar, since otherwise it won't discard any + // ongoing user edits, since it doesn't realize this is a user-initiated + // action. + [targetController locationBarBridge]->Revert(); + } + } + DCHECK(targetController->browser_.get()); + targetController->browser_->ExecuteCommandWithDisposition(command, + disposition); +} + +// Called when another part of the internal codebase needs to execute a +// command. +- (void)executeCommand:(int)command { + if (browser_->command_updater()->IsCommandEnabled(command)) + browser_->ExecuteCommand(command); +} + +// StatusBubble delegate method: tell the status bubble the frame it should +// position itself in. +- (NSRect)statusBubbleBaseFrame { + NSView* view = [previewableContentsController_ view]; + return [view convertRect:[view bounds] toView:nil]; +} + +- (GTMWindowSheetController*)sheetController { + return [tabStripController_ sheetController]; +} + +- (void)updateToolbarWithContents:(TabContents*)tab + shouldRestoreState:(BOOL)shouldRestore { + [toolbarController_ updateToolbarWithContents:tab + shouldRestoreState:shouldRestore]; +} + +- (void)setStarredState:(BOOL)isStarred { + [toolbarController_ setStarredState:isStarred]; +} + +// Accept tabs from a BrowserWindowController with the same Profile. +- (BOOL)canReceiveFrom:(TabWindowController*)source { + if (![source isKindOfClass:[BrowserWindowController class]]) { + return NO; + } + + BrowserWindowController* realSource = + static_cast<BrowserWindowController*>(source); + if (browser_->profile() != realSource->browser_->profile()) { + return NO; + } + + // Can't drag a tab from a normal browser to a pop-up + if (browser_->type() != realSource->browser_->type()) { + return NO; + } + + return YES; +} + +// Move a given tab view to the location of the current placeholder. If there is +// no placeholder, it will go at the end. |controller| is the window controller +// of a tab being dropped from a different window. It will be nil if the drag is +// within the window, otherwise the tab is removed from that window before being +// placed into this one. The implementation will call |-removePlaceholder| since +// the drag is now complete. This also calls |-layoutTabs| internally so +// clients do not need to call it again. +- (void)moveTabView:(NSView*)view + fromController:(TabWindowController*)dragController { + if (dragController) { + // Moving between windows. Figure out the TabContents to drop into our tab + // model from the source window's model. + BOOL isBrowser = + [dragController isKindOfClass:[BrowserWindowController class]]; + DCHECK(isBrowser); + if (!isBrowser) return; + BrowserWindowController* dragBWC = (BrowserWindowController*)dragController; + int index = [dragBWC->tabStripController_ modelIndexForTabView:view]; + TabContentsWrapper* contents = + dragBWC->browser_->GetTabContentsWrapperAt(index); + // The tab contents may have gone away if given a window.close() while it + // is being dragged. If so, bail, we've got nothing to drop. + if (!contents) + return; + + // Convert |view|'s frame (which starts in the source tab strip's coordinate + // system) to the coordinate system of the destination tab strip. This needs + // to be done before being detached so the window transforms can be + // performed. + NSRect destinationFrame = [view frame]; + NSPoint tabOrigin = destinationFrame.origin; + tabOrigin = [[dragController tabStripView] convertPoint:tabOrigin + toView:nil]; + tabOrigin = [[view window] convertBaseToScreen:tabOrigin]; + tabOrigin = [[self window] convertScreenToBase:tabOrigin]; + tabOrigin = [[self tabStripView] convertPoint:tabOrigin fromView:nil]; + destinationFrame.origin = tabOrigin; + + // Before the tab is detached from its originating tab strip, store the + // pinned state so that it can be maintained between the windows. + bool isPinned = dragBWC->browser_->tabstrip_model()->IsTabPinned(index); + + // Now that we have enough information about the tab, we can remove it from + // the dragging window. We need to do this *before* we add it to the new + // window as this will remove the TabContents' delegate. + [dragController detachTabView:view]; + + // Deposit it into our model at the appropriate location (it already knows + // where it should go from tracking the drag). Doing this sets the tab's + // delegate to be the Browser. + [tabStripController_ dropTabContents:contents + withFrame:destinationFrame + asPinnedTab:isPinned]; + } else { + // Moving within a window. + int index = [tabStripController_ modelIndexForTabView:view]; + [tabStripController_ moveTabFromIndex:index]; + } + + // Remove the placeholder since the drag is now complete. + [self removePlaceholder]; +} + +// Tells the tab strip to forget about this tab in preparation for it being +// put into a different tab strip, such as during a drop on another window. +- (void)detachTabView:(NSView*)view { + int index = [tabStripController_ modelIndexForTabView:view]; + browser_->tabstrip_model()->DetachTabContentsAt(index); +} + +- (NSView*)selectedTabView { + return [tabStripController_ selectedTabView]; +} + +- (void)setIsLoading:(BOOL)isLoading force:(BOOL)force { + [toolbarController_ setIsLoading:isLoading force:force]; +} + +// Make the location bar the first responder, if possible. +- (void)focusLocationBar:(BOOL)selectAll { + [toolbarController_ focusLocationBar:selectAll]; +} + +- (void)focusTabContents { + [[self window] makeFirstResponder:[tabStripController_ selectedTabView]]; +} + +- (void)layoutTabs { + [tabStripController_ layoutTabs]; +} + +- (TabWindowController*)detachTabToNewWindow:(TabView*)tabView { + // Disable screen updates so that this appears as a single visual change. + app::mac::ScopedNSDisableScreenUpdates disabler; + + // Fetch the tab contents for the tab being dragged. + int index = [tabStripController_ modelIndexForTabView:tabView]; + TabContentsWrapper* contents = browser_->GetTabContentsWrapperAt(index); + + // Set the window size. Need to do this before we detach the tab so it's + // still in the window. We have to flip the coordinates as that's what + // is expected by the Browser code. + NSWindow* sourceWindow = [tabView window]; + NSRect windowRect = [sourceWindow frame]; + NSScreen* screen = [sourceWindow screen]; + windowRect.origin.y = + [screen frame].size.height - windowRect.size.height - + windowRect.origin.y; + gfx::Rect browserRect(windowRect.origin.x, windowRect.origin.y, + windowRect.size.width, windowRect.size.height); + + NSRect sourceTabRect = [tabView frame]; + NSView* tabStrip = [self tabStripView]; + + // Pushes tabView's frame back inside the tabstrip. + NSSize tabOverflow = + [self overflowFrom:[tabStrip convertRectToBase:sourceTabRect] + to:[tabStrip frame]]; + NSRect tabRect = NSOffsetRect(sourceTabRect, + -tabOverflow.width, -tabOverflow.height); + + // Before detaching the tab, store the pinned state. + bool isPinned = browser_->tabstrip_model()->IsTabPinned(index); + + // Detach it from the source window, which just updates the model without + // deleting the tab contents. This needs to come before creating the new + // Browser because it clears the TabContents' delegate, which gets hooked + // up during creation of the new window. + browser_->tabstrip_model()->DetachTabContentsAt(index); + + // Create the new window with a single tab in its model, the one being + // dragged. + DockInfo dockInfo; + Browser* newBrowser = browser_->tabstrip_model()->delegate()-> + CreateNewStripWithContents(contents, browserRect, dockInfo, false); + + // Propagate the tab pinned state of the new tab (which is the only tab in + // this new window). + newBrowser->tabstrip_model()->SetTabPinned(0, isPinned); + + // Get the new controller by asking the new window for its delegate. + BrowserWindowController* controller = + reinterpret_cast<BrowserWindowController*>( + [newBrowser->window()->GetNativeHandle() delegate]); + DCHECK(controller && [controller isKindOfClass:[TabWindowController class]]); + + // Force the added tab to the right size (remove stretching.) + tabRect.size.height = [TabStripController defaultTabHeight]; + + // And make sure we use the correct frame in the new view. + [[controller tabStripController] setFrameOfSelectedTab:tabRect]; + return controller; +} + +- (void)insertPlaceholderForTab:(TabView*)tab + frame:(NSRect)frame + yStretchiness:(CGFloat)yStretchiness { + [super insertPlaceholderForTab:tab frame:frame yStretchiness:yStretchiness]; + [tabStripController_ insertPlaceholderForTab:tab + frame:frame + yStretchiness:yStretchiness]; +} + +- (void)removePlaceholder { + [super removePlaceholder]; + [tabStripController_ insertPlaceholderForTab:nil + frame:NSZeroRect + yStretchiness:0]; +} + +- (BOOL)tabDraggingAllowed { + return [tabStripController_ tabDraggingAllowed]; +} + +- (BOOL)tabTearingAllowed { + return ![self isFullscreen]; +} + +- (BOOL)windowMovementAllowed { + return ![self isFullscreen]; +} + +- (BOOL)isTabFullyVisible:(TabView*)tab { + return [tabStripController_ isTabFullyVisible:tab]; +} + +- (void)showNewTabButton:(BOOL)show { + [tabStripController_ showNewTabButton:show]; +} + +- (BOOL)isBookmarkBarVisible { + return [bookmarkBarController_ isVisible]; +} + +- (BOOL)isBookmarkBarAnimating { + return [bookmarkBarController_ isAnimationRunning]; +} + +- (void)updateBookmarkBarVisibilityWithAnimation:(BOOL)animate { + [bookmarkBarController_ + updateAndShowNormalBar:[self shouldShowBookmarkBar] + showDetachedBar:[self shouldShowDetachedBookmarkBar] + withAnimation:animate]; +} + +- (BOOL)isDownloadShelfVisible { + return downloadShelfController_ != nil && + [downloadShelfController_ isVisible]; +} + +- (DownloadShelfController*)downloadShelf { + if (!downloadShelfController_.get()) { + downloadShelfController_.reset([[DownloadShelfController alloc] + initWithBrowser:browser_.get() resizeDelegate:self]); + [[[self window] contentView] addSubview:[downloadShelfController_ view]]; + [downloadShelfController_ show:nil]; + } + return downloadShelfController_; +} + +- (void)addFindBar:(FindBarCocoaController*)findBarCocoaController { + // Shouldn't call addFindBar twice. + DCHECK(!findBarCocoaController_.get()); + + // Create a controller for the findbar. + findBarCocoaController_.reset([findBarCocoaController retain]); + NSView *contentView = [[self window] contentView]; + [contentView addSubview:[findBarCocoaController_ view] + positioned:NSWindowAbove + relativeTo:[toolbarController_ view]]; + + // Place the find bar immediately below the toolbar/attached bookmark bar. In + // fullscreen mode, it hangs off the top of the screen when the bar is hidden. + CGFloat maxY = [self placeBookmarkBarBelowInfoBar] ? + NSMinY([[toolbarController_ view] frame]) : + NSMinY([[bookmarkBarController_ view] frame]); + CGFloat maxWidth = NSWidth([contentView frame]); + [findBarCocoaController_ positionFindBarViewAtMaxY:maxY maxWidth:maxWidth]; +} + +- (NSWindow*)createFullscreenWindow { + return [[[FullscreenWindow alloc] initForScreen:[[self window] screen]] + autorelease]; +} + +- (NSInteger)numberOfTabs { + // count() includes pinned tabs. + return browser_->tabstrip_model()->count(); +} + +- (BOOL)hasLiveTabs { + return !browser_->tabstrip_model()->empty(); +} + +- (NSString*)selectedTabTitle { + TabContents* contents = browser_->GetSelectedTabContents(); + return base::SysUTF16ToNSString(contents->GetTitle()); +} + +- (NSRect)regularWindowFrame { + return [self isFullscreen] ? [savedRegularWindow_ frame] : + [[self window] frame]; +} + +// (Override of |TabWindowController| method.) +- (BOOL)hasTabStrip { + return [self supportsWindowFeature:Browser::FEATURE_TABSTRIP]; +} + +// TabContentsControllerDelegate protocol. +- (void)tabContentsViewFrameWillChange:(TabContentsController*)source + frameRect:(NSRect)frameRect { + TabContents* contents = [source tabContents]; + RenderWidgetHostView* render_widget_host_view = contents ? + contents->GetRenderWidgetHostView() : NULL; + if (!render_widget_host_view) + return; + + gfx::Rect reserved_rect; + + NSWindow* window = [self window]; + if ([window respondsToSelector:@selector(_growBoxRect)]) { + NSView* view = [source view]; + if (view && [view superview]) { + NSRect windowGrowBoxRect = [window _growBoxRect]; + NSRect viewRect = [[view superview] convertRect:frameRect toView:nil]; + NSRect growBoxRect = NSIntersectionRect(windowGrowBoxRect, viewRect); + if (!NSIsEmptyRect(growBoxRect)) { + // Before we return a rect, we need to convert it from window + // coordinates to content area coordinates and flip the coordinate + // system. + // Superview is used here because, first, it's a frame rect, so it is + // specified in the parent's coordinates and, second, view is not + // positioned yet. + growBoxRect = [[view superview] convertRect:growBoxRect fromView:nil]; + growBoxRect.origin.y = + NSHeight(frameRect) - NSHeight(growBoxRect); + growBoxRect = + NSOffsetRect(growBoxRect, -frameRect.origin.x, -frameRect.origin.y); + + reserved_rect = + gfx::Rect(growBoxRect.origin.x, growBoxRect.origin.y, + growBoxRect.size.width, growBoxRect.size.height); + } + } + } + + render_widget_host_view->set_reserved_contents_rect(reserved_rect); +} + +// TabStripControllerDelegate protocol. +- (void)onSelectTabWithContents:(TabContents*)contents { + // Update various elements that are interested in knowing the current + // TabContents. + + // Update all the UI bits. + windowShim_->UpdateTitleBar(); + + [sidebarController_ updateSidebarForTabContents:contents]; + [devToolsController_ updateDevToolsForTabContents:contents]; + + // Update the bookmark bar. + // Must do it after sidebar and devtools update, otherwise bookmark bar might + // call resizeView -> layoutSubviews and cause unnecessary relayout. + // TODO(viettrungluu): perhaps update to not terminate running animations (if + // applicable)? + [self updateBookmarkBarVisibilityWithAnimation:NO]; + + [infoBarContainerController_ changeTabContents:contents]; + + // Update devTools and sidebar contents after size for all views is set. + [sidebarController_ ensureContentsVisible]; + [devToolsController_ ensureContentsVisible]; +} + +- (void)onReplaceTabWithContents:(TabContents*)contents { + // This is only called when instant results are committed. Simply remove the + // preview view; the tab strip controller will reinstall the view as the + // active view. + [previewableContentsController_ hidePreview]; + [self updateBookmarkBarVisibilityWithAnimation:NO]; +} + +- (void)onSelectedTabChange:(TabStripModelObserver::TabChangeType)change { + // Update titles if this is the currently selected tab and if it isn't just + // the loading state which changed. + if (change != TabStripModelObserver::LOADING_ONLY) + windowShim_->UpdateTitleBar(); + + // Update the bookmark bar if this is the currently selected tab and if it + // isn't just the title which changed. This for transitions between the NTP + // (showing its floating bookmark bar) and normal web pages (showing no + // bookmark bar). + // TODO(viettrungluu): perhaps update to not terminate running animations? + if (change != TabStripModelObserver::TITLE_NOT_LOADING) + [self updateBookmarkBarVisibilityWithAnimation:NO]; +} + +- (void)onTabDetachedWithContents:(TabContents*)contents { + [infoBarContainerController_ tabDetachedWithContents:contents]; +} + +- (void)userChangedTheme { + // TODO(dmaclach): Instead of redrawing the whole window, views that care + // about the active window state should be registering for notifications. + [[self window] setViewsNeedDisplay:YES]; +} + +- (ThemeProvider*)themeProvider { + return browser_->profile()->GetThemeProvider(); +} + +- (ThemedWindowStyle)themedWindowStyle { + ThemedWindowStyle style = 0; + if (browser_->profile()->IsOffTheRecord()) + style |= THEMED_INCOGNITO; + + Browser::Type type = browser_->type(); + if (type == Browser::TYPE_POPUP) + style |= THEMED_POPUP; + else if (type == Browser::TYPE_DEVTOOLS) + style |= THEMED_DEVTOOLS; + + return style; +} + +- (NSPoint)themePatternPhase { + // Our patterns want to be drawn from the upper left hand corner of the view. + // Cocoa wants to do it from the lower left of the window. + // + // Rephase our pattern to fit this view. Some other views (Tabs, Toolbar etc.) + // will phase their patterns relative to this so all the views look right. + // + // To line up the background pattern with the pattern in the browser window + // the background pattern for the tabs needs to be moved left by 5 pixels. + const CGFloat kPatternHorizontalOffset = -5; + NSView* tabStripView = [self tabStripView]; + NSRect tabStripViewWindowBounds = [tabStripView bounds]; + NSView* windowChromeView = [[[self window] contentView] superview]; + tabStripViewWindowBounds = + [tabStripView convertRect:tabStripViewWindowBounds + toView:windowChromeView]; + NSPoint phase = NSMakePoint(NSMinX(tabStripViewWindowBounds) + + kPatternHorizontalOffset, + NSMinY(tabStripViewWindowBounds) + + [TabStripController defaultTabHeight]); + return phase; +} + +- (NSPoint)bookmarkBubblePoint { + return [toolbarController_ bookmarkBubblePoint]; +} + +// Show the bookmark bubble (e.g. user just clicked on the STAR). +- (void)showBookmarkBubbleForURL:(const GURL&)url + alreadyBookmarked:(BOOL)alreadyMarked { + if (!bookmarkBubbleController_) { + BookmarkModel* model = browser_->profile()->GetBookmarkModel(); + const BookmarkNode* node = model->GetMostRecentlyAddedNodeForURL(url); + bookmarkBubbleController_ = + [[BookmarkBubbleController alloc] initWithParentWindow:[self window] + model:model + node:node + alreadyBookmarked:alreadyMarked]; + [bookmarkBubbleController_ showWindow:self]; + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center addObserver:self + selector:@selector(bubbleWindowWillClose:) + name:NSWindowWillCloseNotification + object:[bookmarkBubbleController_ window]]; + } +} + +// Nil out the weak bookmark bubble controller reference. +- (void)bubbleWindowWillClose:(NSNotification*)notification { + DCHECK([notification object] == [bookmarkBubbleController_ window]); + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center removeObserver:self + name:NSWindowWillCloseNotification + object:[bookmarkBubbleController_ window]]; + bookmarkBubbleController_ = nil; +} + +// Handle the editBookmarkNode: action sent from bookmark bubble controllers. +- (void)editBookmarkNode:(id)sender { + BOOL responds = [sender respondsToSelector:@selector(node)]; + DCHECK(responds); + if (responds) { + const BookmarkNode* node = [sender node]; + if (node) { + // A BookmarkEditorController is a sheet that owns itself, and + // deallocates itself when closed. + [[[BookmarkEditorController alloc] + initWithParentWindow:[self window] + profile:browser_->profile() + parent:node->GetParent() + node:node + configuration:BookmarkEditor::SHOW_TREE] + runAsModalSheet]; + } + } +} + +// If the browser is in incognito mode, install the image view to decorate +// the window at the upper right. Use the same base y coordinate as the +// tab strip. +- (void)installIncognitoBadge { + // Only install if this browser window is OTR and has a tab strip. + if (!browser_->profile()->IsOffTheRecord() || ![self hasTabStrip]) + return; + + // Install the image into the badge view and size the view appropriately. + // Hide it for now; positioning and showing will be done by the layout code. + NSImage* image = nsimage_cache::ImageNamed(@"otr_icon.pdf"); + incognitoBadge_.reset([[IncognitoImageView alloc] init]); + [incognitoBadge_ setImage:image]; + [incognitoBadge_ setFrameSize:[image size]]; + [incognitoBadge_ setAutoresizingMask:NSViewMinXMargin | NSViewMinYMargin]; + [incognitoBadge_ setHidden:YES]; + + // Give it a shadow. + scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]); + [shadow.get() setShadowColor:[NSColor colorWithCalibratedWhite:0.0 + alpha:0.5]]; + [shadow.get() setShadowOffset:NSMakeSize(0, -1)]; + [shadow setShadowBlurRadius:2.0]; + [incognitoBadge_ setShadow:shadow]; + + // Install the view. + [[[[self window] contentView] superview] addSubview:incognitoBadge_]; +} + +// Documented in 10.6+, but present starting in 10.5. Called when we get a +// three-finger swipe. +- (void)swipeWithEvent:(NSEvent*)event { + // Map forwards and backwards to history; left is positive, right is negative. + unsigned int command = 0; + if ([event deltaX] > 0.5) { + command = IDC_BACK; + } else if ([event deltaX] < -0.5) { + command = IDC_FORWARD; + } else if ([event deltaY] > 0.5) { + // TODO(pinkerton): figure out page-up, http://crbug.com/16305 + } else if ([event deltaY] < -0.5) { + // TODO(pinkerton): figure out page-down, http://crbug.com/16305 + browser_->ExecuteCommand(IDC_TABPOSE); + } + + // Ensure the command is valid first (ExecuteCommand() won't do that) and + // then make it so. + if (browser_->command_updater()->IsCommandEnabled(command)) + browser_->ExecuteCommandWithDisposition(command, + event_utils::WindowOpenDispositionFromNSEvent(event)); +} + +// Documented in 10.6+, but present starting in 10.5. Called repeatedly during +// a pinch gesture, with incremental change values. +- (void)magnifyWithEvent:(NSEvent*)event { + // The deltaZ difference necessary to trigger a zoom action. Derived from + // experimentation to find a value that feels reasonable. + const float kZoomStepValue = 150; + + // Find the (absolute) thresholds on either side of the current zoom factor, + // then convert those to actual numbers to trigger a zoom in or out. + // This logic deliberately makes the range around the starting zoom value for + // the gesture twice as large as the other ranges (i.e., the notches are at + // ..., -3*step, -2*step, -step, step, 2*step, 3*step, ... but not at 0) + // so that it's easier to get back to your starting point than it is to + // overshoot. + float nextStep = (abs(currentZoomStepDelta_) + 1) * kZoomStepValue; + float backStep = abs(currentZoomStepDelta_) * kZoomStepValue; + float zoomInThreshold = (currentZoomStepDelta_ >= 0) ? nextStep : -backStep; + float zoomOutThreshold = (currentZoomStepDelta_ <= 0) ? -nextStep : backStep; + + unsigned int command = 0; + totalMagnifyGestureAmount_ += [event deltaZ]; + if (totalMagnifyGestureAmount_ > zoomInThreshold) { + command = IDC_ZOOM_PLUS; + } else if (totalMagnifyGestureAmount_ < zoomOutThreshold) { + command = IDC_ZOOM_MINUS; + } + + if (command && browser_->command_updater()->IsCommandEnabled(command)) { + currentZoomStepDelta_ += (command == IDC_ZOOM_PLUS) ? 1 : -1; + browser_->ExecuteCommandWithDisposition(command, + event_utils::WindowOpenDispositionFromNSEvent(event)); + } +} + +// Documented in 10.6+, but present starting in 10.5. Called at the beginning +// of a gesture. +- (void)beginGestureWithEvent:(NSEvent*)event { + totalMagnifyGestureAmount_ = 0; + currentZoomStepDelta_ = 0; +} + +// Delegate method called when window is resized. +- (void)windowDidResize:(NSNotification*)notification { + // Resize (and possibly move) the status bubble. Note that we may get called + // when the status bubble does not exist. + if (statusBubble_) { + statusBubble_->UpdateSizeAndPosition(); + } + + // Let the selected RenderWidgetHostView know, so that it can tell plugins. + if (TabContents* contents = browser_->GetSelectedTabContents()) { + if (RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView()) + rwhv->WindowFrameChanged(); + } +} + +// Handle the openLearnMoreAboutCrashLink: action from SadTabController when +// "Learn more" link in "Aw snap" page (i.e. crash page or sad tab) is +// clicked. Decoupling the action from its target makes unitestting possible. +- (void)openLearnMoreAboutCrashLink:(id)sender { + if ([sender isKindOfClass:[SadTabController class]]) { + SadTabController* sad_tab = static_cast<SadTabController*>(sender); + TabContents* tab_contents = [sad_tab tabContents]; + if (tab_contents) { + GURL helpUrl = + google_util::AppendGoogleLocaleParam(GURL(chrome::kCrashReasonURL)); + tab_contents->OpenURL(helpUrl, GURL(), CURRENT_TAB, PageTransition::LINK); + } + } +} + +// Delegate method called when window did move. (See below for why we don't use +// |-windowWillMove:|, which is called less frequently than |-windowDidMove| +// instead.) +- (void)windowDidMove:(NSNotification*)notification { + NSWindow* window = [self window]; + NSRect windowFrame = [window frame]; + NSRect workarea = [[window screen] visibleFrame]; + + // We reset the window growth state whenever the window is moved out of the + // work area or away (up or down) from the bottom or top of the work area. + // Unfortunately, Cocoa sends |-windowWillMove:| too frequently (including + // when clicking on the title bar to activate), and of course + // |-windowWillMove| is called too early for us to apply our heuristic. (The + // heuristic we use for detecting window movement is that if |windowTopGrowth_ + // > 0|, then we should be at the bottom of the work area -- if we're not, + // we've moved. Similarly for the other side.) + if (!NSContainsRect(workarea, windowFrame) || + (windowTopGrowth_ > 0 && NSMinY(windowFrame) != NSMinY(workarea)) || + (windowBottomGrowth_ > 0 && NSMaxY(windowFrame) != NSMaxY(workarea))) + [self resetWindowGrowthState]; + + // Let the selected RenderWidgetHostView know, so that it can tell plugins. + if (TabContents* contents = browser_->GetSelectedTabContents()) { + if (RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView()) + rwhv->WindowFrameChanged(); + } +} + +// Delegate method called when window will be resized; not called for +// |-setFrame:display:|. +- (NSSize)windowWillResize:(NSWindow*)sender toSize:(NSSize)frameSize { + [self resetWindowGrowthState]; + return frameSize; +} + +// Delegate method: see |NSWindowDelegate| protocol. +- (id)windowWillReturnFieldEditor:(NSWindow*)sender toObject:(id)obj { + // Ask the toolbar controller if it wants to return a custom field editor + // for the specific object. + return [toolbarController_ customFieldEditorForObject:obj]; +} + +// (Needed for |BookmarkBarControllerDelegate| protocol.) +- (void)bookmarkBar:(BookmarkBarController*)controller + didChangeFromState:(bookmarks::VisualState)oldState + toState:(bookmarks::VisualState)newState { + [toolbarController_ + setDividerOpacity:[bookmarkBarController_ toolbarDividerOpacity]]; + [self adjustToolbarAndBookmarkBarForCompression: + [controller getDesiredToolbarHeightCompression]]; +} + +// (Needed for |BookmarkBarControllerDelegate| protocol.) +- (void)bookmarkBar:(BookmarkBarController*)controller +willAnimateFromState:(bookmarks::VisualState)oldState + toState:(bookmarks::VisualState)newState { + [toolbarController_ + setDividerOpacity:[bookmarkBarController_ toolbarDividerOpacity]]; + [self adjustToolbarAndBookmarkBarForCompression: + [controller getDesiredToolbarHeightCompression]]; +} + +// (Private/TestingAPI) +- (void)resetWindowGrowthState { + windowTopGrowth_ = 0; + windowBottomGrowth_ = 0; + isShrinkingFromZoomed_ = NO; +} + +- (NSSize)overflowFrom:(NSRect)source + to:(NSRect)target { + // If |source|'s boundary is outside of |target|'s, set its distance + // to |x|. Note that |source| can overflow to both side, but we + // have nothing to do for such case. + CGFloat x = 0; + if (NSMaxX(target) < NSMaxX(source)) // |source| overflows to right + x = NSMaxX(source) - NSMaxX(target); + else if (NSMinX(source) < NSMinX(target)) // |source| overflows to left + x = NSMinX(source) - NSMinX(target); + + // Same as |x| above. + CGFloat y = 0; + if (NSMaxY(target) < NSMaxY(source)) + y = NSMaxY(source) - NSMaxY(target); + else if (NSMinY(source) < NSMinY(target)) + y = NSMinY(source) - NSMinY(target); + + return NSMakeSize(x, y); +} + +// Override to swap in the correct tab strip controller based on the new +// tab strip mode. +- (void)toggleTabStripDisplayMode { + [super toggleTabStripDisplayMode]; + [self createTabStripController]; +} + +- (BOOL)useVerticalTabs { + return browser_->tabstrip_model()->delegate()->UseVerticalTabs(); +} + +- (void)showInstant:(TabContents*)previewContents { + [previewableContentsController_ showPreview:previewContents]; + [self updateBookmarkBarVisibilityWithAnimation:NO]; +} + +- (void)hideInstant { + // TODO(rohitrao): Revisit whether or not this method should be called when + // instant isn't showing. + if (![previewableContentsController_ isShowingPreview]) + return; + + [previewableContentsController_ hidePreview]; + [self updateBookmarkBarVisibilityWithAnimation:NO]; +} + +- (NSRect)instantFrame { + // The view's bounds are in its own coordinate system. Convert that to the + // window base coordinate system, then translate it into the screen's + // coordinate system. + NSView* view = [previewableContentsController_ view]; + if (!view) + return NSZeroRect; + + NSRect frame = [view convertRect:[view bounds] toView:nil]; + NSPoint originInScreenCoords = + [[view window] convertBaseToScreen:frame.origin]; + frame.origin = originInScreenCoords; + return frame; +} + +- (void)sheetDidEnd:(NSWindow*)sheet + returnCode:(NSInteger)code + context:(void*)context { + [sheet orderOut:self]; +} + +@end // @implementation BrowserWindowController + + +@implementation BrowserWindowController(Fullscreen) + +- (void)setFullscreen:(BOOL)fullscreen { + // The logic in this function is a bit complicated and very carefully + // arranged. See the below comments for more details. + + if (fullscreen == [self isFullscreen]) + return; + + if (![self supportsFullscreen]) + return; + + // Fade to black. + const CGDisplayReservationInterval kFadeDurationSeconds = 0.6; + Boolean didFadeOut = NO; + CGDisplayFadeReservationToken token; + if (CGAcquireDisplayFadeReservation(kFadeDurationSeconds, &token) + == kCGErrorSuccess) { + didFadeOut = YES; + CGDisplayFade(token, kFadeDurationSeconds / 2, kCGDisplayBlendNormal, + kCGDisplayBlendSolidColor, 0.0, 0.0, 0.0, /*synchronous=*/true); + } + + // Close the bookmark bubble, if it's open. We use |-ok:| instead of + // |-cancel:| or |-close| because that matches the behavior when the bubble + // loses key status. + [bookmarkBubbleController_ ok:self]; + + // Save the current first responder so we can restore after views are moved. + NSWindow* window = [self window]; + scoped_nsobject<FocusTracker> focusTracker( + [[FocusTracker alloc] initWithWindow:window]); + BOOL showDropdown = [self floatingBarHasFocus]; + + // While we move views (and focus) around, disable any bar visibility changes. + [self disableBarVisibilityUpdates]; + + // If we're entering fullscreen, create the fullscreen controller. If we're + // exiting fullscreen, kill the controller. + if (fullscreen) { + fullscreenController_.reset([[FullscreenController alloc] + initWithBrowserController:self]); + } else { + [fullscreenController_ exitFullscreen]; + fullscreenController_.reset(); + } + + // Destroy the tab strip's sheet controller. We will recreate it in the new + // window when needed. + [tabStripController_ destroySheetController]; + + // Retain the tab strip view while we remove it from its superview. + scoped_nsobject<NSView> tabStripView; + if ([self hasTabStrip] && ![self useVerticalTabs]) { + tabStripView.reset([[self tabStripView] retain]); + [tabStripView removeFromSuperview]; + } + + // Ditto for the content view. + scoped_nsobject<NSView> contentView([[window contentView] retain]); + // Disable autoresizing of subviews while we move views around. This prevents + // spurious renderer resizes. + [contentView setAutoresizesSubviews:NO]; + [contentView removeFromSuperview]; + + NSWindow* destWindow = nil; + if (fullscreen) { + DCHECK(!savedRegularWindow_); + savedRegularWindow_ = [window retain]; + destWindow = [self createFullscreenWindow]; + } else { + DCHECK(savedRegularWindow_); + destWindow = [savedRegularWindow_ autorelease]; + savedRegularWindow_ = nil; + + CGSWorkspaceID workspace; + if ([window cr_workspace:&workspace]) { + [destWindow cr_moveToWorkspace:workspace]; + } + } + DCHECK(destWindow); + + // Have to do this here, otherwise later calls can crash because the window + // has no delegate. + [window setDelegate:nil]; + [destWindow setDelegate:self]; + + // With this call, valgrind complains that a "Conditional jump or move depends + // on uninitialised value(s)". The error happens in -[NSThemeFrame + // drawOverlayRect:]. I'm pretty convinced this is an Apple bug, but there is + // no visual impact. I have been unable to tickle it away with other window + // or view manipulation Cocoa calls. Stack added to suppressions_mac.txt. + [contentView setAutoresizesSubviews:YES]; + [destWindow setContentView:contentView]; + + // Move the incognito badge if present. + if (incognitoBadge_.get()) { + [incognitoBadge_ removeFromSuperview]; + [incognitoBadge_ setHidden:YES]; // Will be shown in layout. + [[[destWindow contentView] superview] addSubview:incognitoBadge_]; + } + + // Add the tab strip after setting the content view and moving the incognito + // badge (if any), so that the tab strip will be on top (in the z-order). + if ([self hasTabStrip] && ![self useVerticalTabs]) + [[[destWindow contentView] superview] addSubview:tabStripView]; + + [window setWindowController:nil]; + [self setWindow:destWindow]; + [destWindow setWindowController:self]; + [self adjustUIForFullscreen:fullscreen]; + + // When entering fullscreen mode, the controller forces a layout for us. When + // exiting, we need to call layoutSubviews manually. + if (fullscreen) { + [fullscreenController_ enterFullscreenForContentView:contentView + showDropdown:showDropdown]; + } else { + [self layoutSubviews]; + } + + // Move the status bubble over, if we have one. + if (statusBubble_) + statusBubble_->SwitchParentWindow(destWindow); + + // Move the title over. + [destWindow setTitle:[window title]]; + + // The window needs to be onscreen before we can set its first responder. + [destWindow makeKeyAndOrderFront:self]; + [focusTracker restoreFocusInWindow:destWindow]; + [window orderOut:self]; + + // We're done moving focus, so re-enable bar visibility changes. + [self enableBarVisibilityUpdates]; + + // Fade back in. + if (didFadeOut) { + CGDisplayFade(token, kFadeDurationSeconds / 2, kCGDisplayBlendSolidColor, + kCGDisplayBlendNormal, 0.0, 0.0, 0.0, /*synchronous=*/false); + CGReleaseDisplayFadeReservation(token); + } +} + +- (BOOL)isFullscreen { + return fullscreenController_.get() && [fullscreenController_ isFullscreen]; +} + +- (void)resizeFullscreenWindow { + DCHECK([self isFullscreen]); + if (![self isFullscreen]) + return; + + NSWindow* window = [self window]; + [window setFrame:[[window screen] frame] display:YES]; + [self layoutSubviews]; +} + +- (CGFloat)floatingBarShownFraction { + return floatingBarShownFraction_; +} + +- (void)setFloatingBarShownFraction:(CGFloat)fraction { + floatingBarShownFraction_ = fraction; + [self layoutSubviews]; +} + +- (BOOL)isBarVisibilityLockedForOwner:(id)owner { + DCHECK(owner); + DCHECK(barVisibilityLocks_); + return [barVisibilityLocks_ containsObject:owner]; +} + +- (void)lockBarVisibilityForOwner:(id)owner + withAnimation:(BOOL)animate + delay:(BOOL)delay { + if (![self isBarVisibilityLockedForOwner:owner]) { + [barVisibilityLocks_ addObject:owner]; + + // If enabled, show the overlay if necessary (and if in fullscreen mode). + if (barVisibilityUpdatesEnabled_) { + [fullscreenController_ ensureOverlayShownWithAnimation:animate + delay:delay]; + } + } +} + +- (void)releaseBarVisibilityForOwner:(id)owner + withAnimation:(BOOL)animate + delay:(BOOL)delay { + if ([self isBarVisibilityLockedForOwner:owner]) { + [barVisibilityLocks_ removeObject:owner]; + + // If enabled, hide the overlay if necessary (and if in fullscreen mode). + if (barVisibilityUpdatesEnabled_ && + ![barVisibilityLocks_ count]) { + [fullscreenController_ ensureOverlayHiddenWithAnimation:animate + delay:delay]; + } + } +} + +- (BOOL)floatingBarHasFocus { + NSResponder* focused = [[self window] firstResponder]; + return [focused isKindOfClass:[AutocompleteTextFieldEditor class]]; +} + +- (void)openTabpose { + NSUInteger modifierFlags = [[NSApp currentEvent] modifierFlags]; + BOOL slomo = (modifierFlags & NSShiftKeyMask) != 0; + + // Cover info bars, inspector window, and detached bookmark bar on NTP. + // Do not cover download shelf. + NSRect activeArea = [[self tabContentArea] frame]; + activeArea.size.height += + NSHeight([[infoBarContainerController_ view] frame]); + if ([self isBookmarkBarVisible] && [self placeBookmarkBarBelowInfoBar]) { + NSView* bookmarkBarView = [bookmarkBarController_ view]; + activeArea.size.height += NSHeight([bookmarkBarView frame]); + } + + [TabposeWindow openTabposeFor:[self window] + rect:activeArea + slomo:slomo + tabStripModel:browser_->tabstrip_model()]; +} + +@end // @implementation BrowserWindowController(Fullscreen) + + +@implementation BrowserWindowController(WindowType) + +- (BOOL)supportsWindowFeature:(int)feature { + return browser_->SupportsWindowFeature( + static_cast<Browser::WindowFeature>(feature)); +} + +- (BOOL)hasTitleBar { + return [self supportsWindowFeature:Browser::FEATURE_TITLEBAR]; +} + +- (BOOL)hasToolbar { + return [self supportsWindowFeature:Browser::FEATURE_TOOLBAR]; +} + +- (BOOL)hasLocationBar { + return [self supportsWindowFeature:Browser::FEATURE_LOCATIONBAR]; +} + +- (BOOL)supportsBookmarkBar { + return [self supportsWindowFeature:Browser::FEATURE_BOOKMARKBAR]; +} + +- (BOOL)isNormalWindow { + return browser_->type() == Browser::TYPE_NORMAL; +} + +@end // @implementation BrowserWindowController(WindowType) |