// 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/cocoa/browser_window_controller.h" #include #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" #import "chrome/browser/cocoa/background_gradient_view.h" #import "chrome/browser/cocoa/bookmarks/bookmark_bar_controller.h" #import "chrome/browser/cocoa/bookmarks/bookmark_editor_controller.h" #import "chrome/browser/cocoa/browser_window_cocoa.h" #import "chrome/browser/cocoa/browser_window_controller_private.h" #import "chrome/browser/cocoa/dev_tools_controller.h" #import "chrome/browser/cocoa/download/download_shelf_controller.h" #import "chrome/browser/cocoa/event_utils.h" #import "chrome/browser/cocoa/fast_resize_view.h" #import "chrome/browser/cocoa/find_bar_bridge.h" #import "chrome/browser/cocoa/find_bar_cocoa_controller.h" #import "chrome/browser/cocoa/focus_tracker.h" #import "chrome/browser/cocoa/fullscreen_controller.h" #import "chrome/browser/cocoa/fullscreen_window.h" #import "chrome/browser/cocoa/infobar_container_controller.h" #import "chrome/browser/cocoa/location_bar/autocomplete_text_field_editor.h" #import "chrome/browser/cocoa/previewable_contents_controller.h" #import "chrome/browser/cocoa/nswindow_additions.h" #import "chrome/browser/cocoa/sad_tab_controller.h" #import "chrome/browser/cocoa/sidebar_controller.h" #import "chrome/browser/cocoa/status_bubble_mac.h" #import "chrome/browser/cocoa/tab_contents_controller.h" #import "chrome/browser/cocoa/tab_strip_controller.h" #import "chrome/browser/cocoa/tab_strip_view.h" #import "chrome/browser/cocoa/tab_view.h" #import "chrome/browser/cocoa/tabpose_window.h" #import "chrome/browser/cocoa/toolbar_controller.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" #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_.mm // - ... in categories called "()" or "()" // 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(contents->view()); CGFloat intrinsicWidth = static_cast( 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)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(item) isKindOfClass:[NSMenuItem class]]) { NSString* menuTitle = l10n_util::GetNSString( [self isFullscreen] ? IDS_EXIT_FULLSCREEN_MAC : IDS_ENTER_FULLSCREEN_MAC); [static_cast(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(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( [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 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(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 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 tabStripView; if ([self hasTabStrip] && ![self useVerticalTabs]) { tabStripView.reset([[self tabStripView] retain]); [tabStripView removeFromSuperview]; } // Ditto for the content view. scoped_nsobject 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(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)