toolbar_controller.mm revision ddb351dbec246cf1fab5ec20d2d5520909041de1
1// Copyright (c) 2011 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
6
7#include <algorithm>
8
9#include "app/mac/nsimage_cache.h"
10#include "base/mac/mac_util.h"
11#include "base/memory/singleton.h"
12#include "base/sys_string_conversions.h"
13#include "chrome/app/chrome_command_ids.h"
14#include "chrome/browser/autocomplete/autocomplete.h"
15#include "chrome/browser/autocomplete/autocomplete_classifier.h"
16#include "chrome/browser/autocomplete/autocomplete_edit_view.h"
17#include "chrome/browser/autocomplete/autocomplete_match.h"
18#include "chrome/browser/net/url_fixer_upper.h"
19#include "chrome/browser/prefs/pref_service.h"
20#include "chrome/browser/profiles/profile.h"
21#include "chrome/browser/search_engines/template_url_model.h"
22#include "chrome/browser/themes/theme_service.h"
23#include "chrome/browser/ui/browser.h"
24#include "chrome/browser/ui/browser_window.h"
25#import "chrome/browser/ui/cocoa/accelerators_cocoa.h"
26#import "chrome/browser/ui/cocoa/background_gradient_view.h"
27#import "chrome/browser/ui/cocoa/encoding_menu_controller_delegate_mac.h"
28#import "chrome/browser/ui/cocoa/extensions/browser_action_button.h"
29#import "chrome/browser/ui/cocoa/extensions/browser_actions_container_view.h"
30#import "chrome/browser/ui/cocoa/extensions/browser_actions_controller.h"
31#import "chrome/browser/ui/cocoa/gradient_button_cell.h"
32#import "chrome/browser/ui/cocoa/location_bar/autocomplete_text_field_editor.h"
33#import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
34#import "chrome/browser/ui/cocoa/menu_button.h"
35#import "chrome/browser/ui/cocoa/menu_controller.h"
36#import "chrome/browser/ui/cocoa/toolbar/back_forward_menu_controller.h"
37#import "chrome/browser/ui/cocoa/toolbar/reload_button.h"
38#import "chrome/browser/ui/cocoa/toolbar/toolbar_button.h"
39#import "chrome/browser/ui/cocoa/toolbar/toolbar_view.h"
40#import "chrome/browser/ui/cocoa/view_id_util.h"
41#import "chrome/browser/ui/cocoa/wrench_menu/wrench_menu_controller.h"
42#include "chrome/browser/ui/toolbar/toolbar_model.h"
43#include "chrome/browser/ui/toolbar/wrench_menu_model.h"
44#include "chrome/browser/upgrade_detector.h"
45#include "chrome/common/pref_names.h"
46#include "content/browser/tab_contents/tab_contents.h"
47#include "content/common/notification_details.h"
48#include "content/common/notification_observer.h"
49#include "content/common/notification_service.h"
50#include "content/common/notification_type.h"
51#include "grit/chromium_strings.h"
52#include "grit/generated_resources.h"
53#include "grit/theme_resources.h"
54#include "ui/base/l10n/l10n_util.h"
55#include "ui/base/l10n/l10n_util_mac.h"
56#include "ui/base/models/accelerator_cocoa.h"
57#include "ui/base/models/menu_model.h"
58#include "ui/base/resource/resource_bundle.h"
59#include "ui/gfx/image.h"
60#include "ui/gfx/rect.h"
61
62namespace {
63
64// Names of images in the bundle for buttons.
65NSString* const kBackButtonImageName = @"back_Template.pdf";
66NSString* const kForwardButtonImageName = @"forward_Template.pdf";
67NSString* const kReloadButtonReloadImageName = @"reload_Template.pdf";
68NSString* const kReloadButtonStopImageName = @"stop_Template.pdf";
69NSString* const kHomeButtonImageName = @"home_Template.pdf";
70NSString* const kWrenchButtonImageName = @"tools_Template.pdf";
71
72// Height of the toolbar in pixels when the bookmark bar is closed.
73const CGFloat kBaseToolbarHeight = 35.0;
74
75// The minimum width of the location bar in pixels.
76const CGFloat kMinimumLocationBarWidth = 100.0;
77
78// The duration of any animation that occurs within the toolbar in seconds.
79const CGFloat kAnimationDuration = 0.2;
80
81// The amount of left padding that the wrench menu should have.
82const CGFloat kWrenchMenuLeftPadding = 3.0;
83
84}  // namespace
85
86@interface ToolbarController(Private)
87- (void)addAccessibilityDescriptions;
88- (void)initCommandStatus:(CommandUpdater*)commands;
89- (void)prefChanged:(std::string*)prefName;
90- (BackgroundGradientView*)backgroundGradientView;
91- (void)toolbarFrameChanged;
92- (void)pinLocationBarToLeftOfBrowserActionsContainerAndAnimate:(BOOL)animate;
93- (void)maintainMinimumLocationBarWidth;
94- (void)adjustBrowserActionsContainerForNewWindow:(NSNotification*)notification;
95- (void)browserActionsContainerDragged:(NSNotification*)notification;
96- (void)browserActionsContainerDragFinished:(NSNotification*)notification;
97- (void)browserActionsVisibilityChanged:(NSNotification*)notification;
98- (void)adjustLocationSizeBy:(CGFloat)dX animate:(BOOL)animate;
99- (void)badgeWrenchMenuIfNeeded;
100@end
101
102namespace ToolbarControllerInternal {
103
104// A C++ delegate that handles the accelerators in the wrench menu.
105class WrenchAcceleratorDelegate : public ui::AcceleratorProvider {
106 public:
107  virtual bool GetAcceleratorForCommandId(int command_id,
108      ui::Accelerator* accelerator_generic) {
109    // Downcast so that when the copy constructor is invoked below, the key
110    // string gets copied, too.
111    ui::AcceleratorCocoa* out_accelerator =
112        static_cast<ui::AcceleratorCocoa*>(accelerator_generic);
113    AcceleratorsCocoa* keymap = AcceleratorsCocoa::GetInstance();
114    const ui::AcceleratorCocoa* accelerator =
115        keymap->GetAcceleratorForCommand(command_id);
116    if (accelerator) {
117      *out_accelerator = *accelerator;
118      return true;
119    }
120    return false;
121  }
122};
123
124// A class registered for C++ notifications. This is used to detect changes in
125// preferences and upgrade available notifications. Bridges the notification
126// back to the ToolbarController.
127class NotificationBridge : public NotificationObserver {
128 public:
129  explicit NotificationBridge(ToolbarController* controller)
130      : controller_(controller) {
131    registrar_.Add(this, NotificationType::UPGRADE_RECOMMENDED,
132                   NotificationService::AllSources());
133  }
134
135  // Overridden from NotificationObserver:
136  virtual void Observe(NotificationType type,
137                       const NotificationSource& source,
138                       const NotificationDetails& details) {
139    switch (type.value) {
140      case NotificationType::PREF_CHANGED:
141        [controller_ prefChanged:Details<std::string>(details).ptr()];
142        break;
143      case NotificationType::UPGRADE_RECOMMENDED:
144        [controller_ badgeWrenchMenuIfNeeded];
145        break;
146      default:
147        NOTREACHED();
148    }
149  }
150
151 private:
152  ToolbarController* controller_;  // weak, owns us
153
154  NotificationRegistrar registrar_;
155};
156
157}  // namespace ToolbarControllerInternal
158
159@implementation ToolbarController
160
161- (id)initWithModel:(ToolbarModel*)model
162           commands:(CommandUpdater*)commands
163            profile:(Profile*)profile
164            browser:(Browser*)browser
165     resizeDelegate:(id<ViewResizer>)resizeDelegate
166       nibFileNamed:(NSString*)nibName {
167  DCHECK(model && commands && profile && [nibName length]);
168  if ((self = [super initWithNibName:nibName
169                              bundle:base::mac::MainAppBundle()])) {
170    toolbarModel_ = model;
171    commands_ = commands;
172    profile_ = profile;
173    browser_ = browser;
174    resizeDelegate_ = resizeDelegate;
175    hasToolbar_ = YES;
176    hasLocationBar_ = YES;
177
178    // Register for notifications about state changes for the toolbar buttons
179    commandObserver_.reset(new CommandObserverBridge(self, commands));
180    commandObserver_->ObserveCommand(IDC_BACK);
181    commandObserver_->ObserveCommand(IDC_FORWARD);
182    commandObserver_->ObserveCommand(IDC_RELOAD);
183    commandObserver_->ObserveCommand(IDC_HOME);
184    commandObserver_->ObserveCommand(IDC_BOOKMARK_PAGE);
185  }
186  return self;
187}
188
189- (id)initWithModel:(ToolbarModel*)model
190           commands:(CommandUpdater*)commands
191            profile:(Profile*)profile
192            browser:(Browser*)browser
193     resizeDelegate:(id<ViewResizer>)resizeDelegate {
194  if ((self = [self initWithModel:model
195                         commands:commands
196                          profile:profile
197                          browser:browser
198                   resizeDelegate:resizeDelegate
199                     nibFileNamed:@"Toolbar"])) {
200  }
201  return self;
202}
203
204
205- (void)dealloc {
206  // Unset ViewIDs of toolbar elements.
207  // ViewIDs of |toolbarView|, |reloadButton_|, |locationBar_| and
208  // |browserActionsContainerView_| are handled by themselves.
209  view_id_util::UnsetID(backButton_);
210  view_id_util::UnsetID(forwardButton_);
211  view_id_util::UnsetID(homeButton_);
212  view_id_util::UnsetID(wrenchButton_);
213
214  // Make sure any code in the base class which assumes [self view] is
215  // the "parent" view continues to work.
216  hasToolbar_ = YES;
217  hasLocationBar_ = YES;
218
219  [[NSNotificationCenter defaultCenter] removeObserver:self];
220
221  if (trackingArea_.get())
222    [[self view] removeTrackingArea:trackingArea_.get()];
223  [super dealloc];
224}
225
226// Called after the view is done loading and the outlets have been hooked up.
227// Now we can hook up bridges that rely on UI objects such as the location
228// bar and button state.
229- (void)awakeFromNib {
230  // A bug in AppKit (<rdar://7298597>, <http://openradar.me/7298597>) causes
231  // images loaded directly from nibs in a framework to not get their "template"
232  // flags set properly. Thus, despite the images being set on the buttons in
233  // the xib, we must set them in code.
234  [backButton_ setImage:app::mac::GetCachedImageWithName(kBackButtonImageName)];
235  [forwardButton_ setImage:
236      app::mac::GetCachedImageWithName(kForwardButtonImageName)];
237  [reloadButton_ setImage:
238      app::mac::GetCachedImageWithName(kReloadButtonReloadImageName)];
239  [homeButton_ setImage:
240      app::mac::GetCachedImageWithName(kHomeButtonImageName)];
241  [wrenchButton_ setImage:
242      app::mac::GetCachedImageWithName(kWrenchButtonImageName)];
243  [self badgeWrenchMenuIfNeeded];
244
245  [wrenchButton_ setOpenMenuOnClick:YES];
246
247  [backButton_ setShowsBorderOnlyWhileMouseInside:YES];
248  [forwardButton_ setShowsBorderOnlyWhileMouseInside:YES];
249  [reloadButton_ setShowsBorderOnlyWhileMouseInside:YES];
250  [homeButton_ setShowsBorderOnlyWhileMouseInside:YES];
251  [wrenchButton_ setShowsBorderOnlyWhileMouseInside:YES];
252
253  [backButton_ setHandleMiddleClick:YES];
254  [forwardButton_ setHandleMiddleClick:YES];
255  [reloadButton_ setHandleMiddleClick:YES];
256  [homeButton_ setHandleMiddleClick:YES];
257
258  [self initCommandStatus:commands_];
259  locationBarView_.reset(new LocationBarViewMac(locationBar_,
260                                                commands_, toolbarModel_,
261                                                profile_, browser_));
262  [locationBar_ setFont:[NSFont systemFontOfSize:[NSFont systemFontSize]]];
263  // Register pref observers for the optional home and page/options buttons
264  // and then add them to the toolbar based on those prefs.
265  notificationBridge_.reset(
266      new ToolbarControllerInternal::NotificationBridge(self));
267  PrefService* prefs = profile_->GetPrefs();
268  showHomeButton_.Init(prefs::kShowHomeButton, prefs,
269                       notificationBridge_.get());
270  [self showOptionalHomeButton];
271  [self installWrenchMenu];
272
273  // Create the controllers for the back/forward menus.
274  backMenuController_.reset([[BackForwardMenuController alloc]
275          initWithBrowser:browser_
276                modelType:BACK_FORWARD_MENU_TYPE_BACK
277                   button:backButton_]);
278  forwardMenuController_.reset([[BackForwardMenuController alloc]
279          initWithBrowser:browser_
280                modelType:BACK_FORWARD_MENU_TYPE_FORWARD
281                   button:forwardButton_]);
282
283  // For a popup window, the toolbar is really just a location bar
284  // (see override for [ToolbarController view], below).  When going
285  // fullscreen, we remove the toolbar controller's view from the view
286  // hierarchy.  Calling [locationBar_ removeFromSuperview] when going
287  // fullscreen causes it to get released, making us unhappy
288  // (http://crbug.com/18551).  We avoid the problem by incrementing
289  // the retain count of the location bar; use of the scoped object
290  // helps us remember to release it.
291  locationBarRetainer_.reset([locationBar_ retain]);
292  trackingArea_.reset(
293      [[CrTrackingArea alloc] initWithRect:NSZeroRect // Ignored
294                                   options:NSTrackingMouseMoved |
295                                           NSTrackingInVisibleRect |
296                                           NSTrackingMouseEnteredAndExited |
297                                           NSTrackingActiveAlways
298                              proxiedOwner:self
299                                  userInfo:nil]);
300  NSView* toolbarView = [self view];
301  [toolbarView addTrackingArea:trackingArea_.get()];
302
303  // If the user has any Browser Actions installed, the container view for them
304  // may have to be resized depending on the width of the toolbar frame.
305  [toolbarView setPostsFrameChangedNotifications:YES];
306  [[NSNotificationCenter defaultCenter]
307      addObserver:self
308         selector:@selector(toolbarFrameChanged)
309             name:NSViewFrameDidChangeNotification
310           object:toolbarView];
311
312  // Set ViewIDs for toolbar elements which don't have their dedicated class.
313  // ViewIDs of |toolbarView|, |reloadButton_|, |locationBar_| and
314  // |browserActionsContainerView_| are handled by themselves.
315  view_id_util::SetID(backButton_, VIEW_ID_BACK_BUTTON);
316  view_id_util::SetID(forwardButton_, VIEW_ID_FORWARD_BUTTON);
317  view_id_util::SetID(homeButton_, VIEW_ID_HOME_BUTTON);
318  view_id_util::SetID(wrenchButton_, VIEW_ID_APP_MENU);
319
320  [self addAccessibilityDescriptions];
321}
322
323- (void)addAccessibilityDescriptions {
324  // Set accessibility descriptions. http://openradar.appspot.com/7496255
325  NSString* description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_BACK);
326  [[backButton_ cell]
327      accessibilitySetOverrideValue:description
328                       forAttribute:NSAccessibilityDescriptionAttribute];
329  description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_FORWARD);
330  [[forwardButton_ cell]
331      accessibilitySetOverrideValue:description
332                       forAttribute:NSAccessibilityDescriptionAttribute];
333  description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_RELOAD);
334  [[reloadButton_ cell]
335      accessibilitySetOverrideValue:description
336                       forAttribute:NSAccessibilityDescriptionAttribute];
337  description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_HOME);
338  [[homeButton_ cell]
339      accessibilitySetOverrideValue:description
340                       forAttribute:NSAccessibilityDescriptionAttribute];
341  description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_LOCATION);
342  [[locationBar_ cell]
343      accessibilitySetOverrideValue:description
344                       forAttribute:NSAccessibilityDescriptionAttribute];
345  description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_APP);
346  [[wrenchButton_ cell]
347      accessibilitySetOverrideValue:description
348                       forAttribute:NSAccessibilityDescriptionAttribute];
349}
350
351- (void)mouseExited:(NSEvent*)theEvent {
352  [[hoveredButton_ cell] setMouseInside:NO animate:YES];
353  [hoveredButton_ release];
354  hoveredButton_ = nil;
355}
356
357- (NSButton*)hoverButtonForEvent:(NSEvent*)theEvent {
358  NSButton* targetView = (NSButton*)[[self view]
359                                     hitTest:[theEvent locationInWindow]];
360
361  // Only interpret the view as a hoverButton_ if it's both button and has a
362  // button cell that cares.  GradientButtonCell derived cells care.
363  if (([targetView isKindOfClass:[NSButton class]]) &&
364      ([[targetView cell]
365         respondsToSelector:@selector(setMouseInside:animate:)]))
366    return targetView;
367  return nil;
368}
369
370- (void)mouseMoved:(NSEvent*)theEvent {
371  NSButton* targetView = [self hoverButtonForEvent:theEvent];
372  if (hoveredButton_ != targetView) {
373    [[hoveredButton_ cell] setMouseInside:NO animate:YES];
374    [[targetView cell] setMouseInside:YES animate:YES];
375    [hoveredButton_ release];
376    hoveredButton_ = [targetView retain];
377  }
378}
379
380- (void)mouseEntered:(NSEvent*)event {
381  [self mouseMoved:event];
382}
383
384- (LocationBarViewMac*)locationBarBridge {
385  return locationBarView_.get();
386}
387
388- (void)focusLocationBar:(BOOL)selectAll {
389  if (locationBarView_.get())
390    locationBarView_->FocusLocation(selectAll ? true : false);
391}
392
393// Called when the state for a command changes to |enabled|. Update the
394// corresponding UI element.
395- (void)enabledStateChangedForCommand:(NSInteger)command enabled:(BOOL)enabled {
396  NSButton* button = nil;
397  switch (command) {
398    case IDC_BACK:
399      button = backButton_;
400      break;
401    case IDC_FORWARD:
402      button = forwardButton_;
403      break;
404    case IDC_HOME:
405      button = homeButton_;
406      break;
407  }
408  [button setEnabled:enabled];
409}
410
411// Init the enabled state of the buttons on the toolbar to match the state in
412// the controller.
413- (void)initCommandStatus:(CommandUpdater*)commands {
414  [backButton_ setEnabled:commands->IsCommandEnabled(IDC_BACK) ? YES : NO];
415  [forwardButton_
416      setEnabled:commands->IsCommandEnabled(IDC_FORWARD) ? YES : NO];
417  [reloadButton_ setEnabled:YES];
418  [homeButton_ setEnabled:commands->IsCommandEnabled(IDC_HOME) ? YES : NO];
419}
420
421- (void)updateToolbarWithContents:(TabContents*)tab
422               shouldRestoreState:(BOOL)shouldRestore {
423  locationBarView_->Update(tab, shouldRestore ? true : false);
424
425  [locationBar_ updateCursorAndToolTipRects];
426
427  if (browserActionsController_.get()) {
428    [browserActionsController_ update];
429  }
430}
431
432- (void)setStarredState:(BOOL)isStarred {
433  locationBarView_->SetStarred(isStarred ? true : false);
434}
435
436- (void)setIsLoading:(BOOL)isLoading force:(BOOL)force {
437  [reloadButton_ setIsLoading:isLoading force:force];
438}
439
440- (void)setHasToolbar:(BOOL)toolbar hasLocationBar:(BOOL)locBar {
441  [self view];  // Force nib loading.
442
443  hasToolbar_ = toolbar;
444
445  // If there's a toolbar, there must be a location bar.
446  DCHECK((toolbar && locBar) || !toolbar);
447  hasLocationBar_ = toolbar ? YES : locBar;
448
449  // Decide whether to hide/show based on whether there's a location bar.
450  [[self view] setHidden:!hasLocationBar_];
451
452  // Make location bar not editable when in a pop-up.
453  locationBarView_->SetEditable(toolbar);
454}
455
456- (NSView*)view {
457  if (hasToolbar_)
458    return [super view];
459  return locationBar_;
460}
461
462// (Private) Returns the backdrop to the toolbar.
463- (BackgroundGradientView*)backgroundGradientView {
464  // We really do mean |[super view]|; see our override of |-view|.
465  DCHECK([[super view] isKindOfClass:[BackgroundGradientView class]]);
466  return (BackgroundGradientView*)[super view];
467}
468
469- (id)customFieldEditorForObject:(id)obj {
470  if (obj == locationBar_) {
471    // Lazilly construct Field editor, Cocoa UI code always runs on the
472    // same thread, so there shoudn't be a race condition here.
473    if (autocompleteTextFieldEditor_.get() == nil) {
474      autocompleteTextFieldEditor_.reset(
475          [[AutocompleteTextFieldEditor alloc] init]);
476    }
477
478    // This needs to be called every time, otherwise notifications
479    // aren't sent correctly.
480    DCHECK(autocompleteTextFieldEditor_.get());
481    [autocompleteTextFieldEditor_.get() setFieldEditor:YES];
482    return autocompleteTextFieldEditor_.get();
483  }
484  return nil;
485}
486
487// Returns an array of views in the order of the outlets above.
488- (NSArray*)toolbarViews {
489  return [NSArray arrayWithObjects:backButton_, forwardButton_, reloadButton_,
490             homeButton_, wrenchButton_, locationBar_,
491             browserActionsContainerView_, nil];
492}
493
494// Moves |rect| to the right by |delta|, keeping the right side fixed by
495// shrinking the width to compensate. Passing a negative value for |deltaX|
496// moves to the left and increases the width.
497- (NSRect)adjustRect:(NSRect)rect byAmount:(CGFloat)deltaX {
498  NSRect frame = NSOffsetRect(rect, deltaX, 0);
499  frame.size.width -= deltaX;
500  return frame;
501}
502
503// Show or hide the home button based on the pref.
504- (void)showOptionalHomeButton {
505  // Ignore this message if only showing the URL bar.
506  if (!hasToolbar_)
507    return;
508  BOOL hide = showHomeButton_.GetValue() ? NO : YES;
509  if (hide == [homeButton_ isHidden])
510    return;  // Nothing to do, view state matches pref state.
511
512  // Always shift the text field by the width of the home button minus one pixel
513  // since the frame edges of each button are right on top of each other. When
514  // hiding the button, reverse the direction of the movement (to the left).
515  CGFloat moveX = [homeButton_ frame].size.width - 1.0;
516  if (hide)
517    moveX *= -1;  // Reverse the direction of the move.
518
519  [locationBar_ setFrame:[self adjustRect:[locationBar_ frame]
520                                 byAmount:moveX]];
521  [homeButton_ setHidden:hide];
522}
523
524// Install the menu wrench buttons. Calling this repeatedly is inexpensive so it
525// can be done every time the buttons are shown.
526- (void)installWrenchMenu {
527  if (wrenchMenuModel_.get())
528    return;
529  acceleratorDelegate_.reset(
530      new ToolbarControllerInternal::WrenchAcceleratorDelegate());
531
532  wrenchMenuModel_.reset(new WrenchMenuModel(
533      acceleratorDelegate_.get(), browser_));
534  [wrenchMenuController_ setModel:wrenchMenuModel_.get()];
535  [wrenchMenuController_ setUseWithPopUpButtonCell:YES];
536  [wrenchButton_ setAttachedMenu:[wrenchMenuController_ menu]];
537}
538
539- (WrenchMenuController*)wrenchMenuController {
540  return wrenchMenuController_;
541}
542
543- (void)badgeWrenchMenuIfNeeded {
544
545  int badgeResource = 0;
546  if (UpgradeDetector::GetInstance()->notify_upgrade()) {
547    badgeResource = IDR_UPDATE_BADGE;
548  } else {
549    // No badge - clear the badge if one is already set.
550    if ([[wrenchButton_ cell] overlayImage])
551      [[wrenchButton_ cell] setOverlayImage:nil];
552    return;
553  }
554
555  NSImage* badge =
556      ResourceBundle::GetSharedInstance().GetNativeImageNamed(badgeResource);
557  NSImage* wrenchImage =
558      app::mac::GetCachedImageWithName(kWrenchButtonImageName);
559  NSSize wrenchImageSize = [wrenchImage size];
560  NSSize badgeSize = [badge size];
561
562  scoped_nsobject<NSImage> overlayImage(
563      [[NSImage alloc] initWithSize:wrenchImageSize]);
564
565  // Draw badge in the upper right corner of the button.
566  NSPoint overlayPosition =
567      NSMakePoint(wrenchImageSize.width - badgeSize.width,
568                  wrenchImageSize.height - badgeSize.height);
569
570  [overlayImage lockFocus];
571  [badge drawAtPoint:overlayPosition
572            fromRect:NSZeroRect
573           operation:NSCompositeSourceOver
574            fraction:1.0];
575  [overlayImage unlockFocus];
576
577  [[wrenchButton_ cell] setOverlayImage:overlayImage];
578}
579
580- (void)prefChanged:(std::string*)prefName {
581  if (!prefName) return;
582  if (*prefName == prefs::kShowHomeButton) {
583    [self showOptionalHomeButton];
584  }
585}
586
587- (void)createBrowserActionButtons {
588  if (!browserActionsController_.get()) {
589    browserActionsController_.reset([[BrowserActionsController alloc]
590            initWithBrowser:browser_
591              containerView:browserActionsContainerView_]);
592    [[NSNotificationCenter defaultCenter]
593        addObserver:self
594           selector:@selector(browserActionsContainerDragged:)
595               name:kBrowserActionGrippyDraggingNotification
596             object:browserActionsController_];
597    [[NSNotificationCenter defaultCenter]
598        addObserver:self
599           selector:@selector(browserActionsContainerDragFinished:)
600               name:kBrowserActionGrippyDragFinishedNotification
601             object:browserActionsController_];
602    [[NSNotificationCenter defaultCenter]
603        addObserver:self
604           selector:@selector(browserActionsVisibilityChanged:)
605               name:kBrowserActionVisibilityChangedNotification
606             object:browserActionsController_];
607    [[NSNotificationCenter defaultCenter]
608        addObserver:self
609           selector:@selector(adjustBrowserActionsContainerForNewWindow:)
610               name:NSWindowDidBecomeKeyNotification
611             object:[[self view] window]];
612  }
613  CGFloat containerWidth = [browserActionsContainerView_ isHidden] ? 0.0 :
614      NSWidth([browserActionsContainerView_ frame]);
615  if (containerWidth > 0.0)
616    [self adjustLocationSizeBy:(containerWidth * -1) animate:NO];
617}
618
619- (void)adjustBrowserActionsContainerForNewWindow:
620    (NSNotification*)notification {
621  [self toolbarFrameChanged];
622  [[NSNotificationCenter defaultCenter]
623      removeObserver:self
624                name:NSWindowDidBecomeKeyNotification
625              object:[[self view] window]];
626}
627
628- (void)browserActionsContainerDragged:(NSNotification*)notification {
629  CGFloat locationBarWidth = NSWidth([locationBar_ frame]);
630  locationBarAtMinSize_ = locationBarWidth <= kMinimumLocationBarWidth;
631  [browserActionsContainerView_ setCanDragLeft:!locationBarAtMinSize_];
632  [browserActionsContainerView_ setGrippyPinned:locationBarAtMinSize_];
633  [self adjustLocationSizeBy:
634      [browserActionsContainerView_ resizeDeltaX] animate:NO];
635}
636
637- (void)browserActionsContainerDragFinished:(NSNotification*)notification {
638  [browserActionsController_ resizeContainerAndAnimate:YES];
639  [self pinLocationBarToLeftOfBrowserActionsContainerAndAnimate:YES];
640}
641
642- (void)browserActionsVisibilityChanged:(NSNotification*)notification {
643  [self pinLocationBarToLeftOfBrowserActionsContainerAndAnimate:NO];
644}
645
646- (void)pinLocationBarToLeftOfBrowserActionsContainerAndAnimate:(BOOL)animate {
647  CGFloat locationBarXPos = NSMaxX([locationBar_ frame]);
648  CGFloat leftDistance;
649
650  if ([browserActionsContainerView_ isHidden]) {
651    CGFloat edgeXPos = [wrenchButton_ frame].origin.x;
652    leftDistance = edgeXPos - locationBarXPos - kWrenchMenuLeftPadding;
653  } else {
654    NSRect containerFrame = animate ?
655        [browserActionsContainerView_ animationEndFrame] :
656        [browserActionsContainerView_ frame];
657
658    leftDistance = containerFrame.origin.x - locationBarXPos;
659  }
660  if (leftDistance != 0.0)
661    [self adjustLocationSizeBy:leftDistance animate:animate];
662}
663
664- (void)maintainMinimumLocationBarWidth {
665  CGFloat locationBarWidth = NSWidth([locationBar_ frame]);
666  locationBarAtMinSize_ = locationBarWidth <= kMinimumLocationBarWidth;
667  if (locationBarAtMinSize_) {
668    CGFloat dX = kMinimumLocationBarWidth - locationBarWidth;
669    [self adjustLocationSizeBy:dX animate:NO];
670  }
671}
672
673- (void)toolbarFrameChanged {
674  // Do nothing if the frame changes but no Browser Action Controller is
675  // present.
676  if (!browserActionsController_.get())
677    return;
678
679  [self maintainMinimumLocationBarWidth];
680
681  if (locationBarAtMinSize_) {
682    // Once the grippy is pinned, leave it until it is explicity un-pinned.
683    [browserActionsContainerView_ setGrippyPinned:YES];
684    NSRect containerFrame = [browserActionsContainerView_ frame];
685    // Determine how much the container needs to move in case it's overlapping
686    // with the location bar.
687    CGFloat dX = NSMaxX([locationBar_ frame]) - containerFrame.origin.x;
688    containerFrame = NSOffsetRect(containerFrame, dX, 0);
689    containerFrame.size.width -= dX;
690    [browserActionsContainerView_ setFrame:containerFrame];
691  } else if (!locationBarAtMinSize_ &&
692      [browserActionsContainerView_ grippyPinned]) {
693    // Expand out the container until it hits the saved size, then unpin the
694    // grippy.
695    // Add 0.1 pixel so that it doesn't hit the minimum width codepath above.
696    CGFloat dX = NSWidth([locationBar_ frame]) -
697        (kMinimumLocationBarWidth + 0.1);
698    NSRect containerFrame = [browserActionsContainerView_ frame];
699    containerFrame = NSOffsetRect(containerFrame, -dX, 0);
700    containerFrame.size.width += dX;
701    CGFloat savedContainerWidth = [browserActionsController_ savedWidth];
702    if (NSWidth(containerFrame) >= savedContainerWidth) {
703      containerFrame = NSOffsetRect(containerFrame,
704          NSWidth(containerFrame) - savedContainerWidth, 0);
705      containerFrame.size.width = savedContainerWidth;
706      [browserActionsContainerView_ setGrippyPinned:NO];
707    }
708    [browserActionsContainerView_ setFrame:containerFrame];
709    [self pinLocationBarToLeftOfBrowserActionsContainerAndAnimate:NO];
710  }
711}
712
713- (void)adjustLocationSizeBy:(CGFloat)dX animate:(BOOL)animate {
714  // Ensure that the location bar is in its proper place.
715  NSRect locationFrame = [locationBar_ frame];
716  locationFrame.size.width += dX;
717
718  if (!animate) {
719    [locationBar_ setFrame:locationFrame];
720    return;
721  }
722
723  [NSAnimationContext beginGrouping];
724  [[NSAnimationContext currentContext] setDuration:kAnimationDuration];
725  [[locationBar_ animator] setFrame:locationFrame];
726  [NSAnimationContext endGrouping];
727}
728
729- (NSPoint)bookmarkBubblePoint {
730  return locationBarView_->GetBookmarkBubblePoint();
731}
732
733- (CGFloat)desiredHeightForCompression:(CGFloat)compressByHeight {
734  // With no toolbar, just ignore the compression.
735  return hasToolbar_ ? kBaseToolbarHeight - compressByHeight :
736                       NSHeight([locationBar_ frame]);
737}
738
739- (void)setDividerOpacity:(CGFloat)opacity {
740  BackgroundGradientView* view = [self backgroundGradientView];
741  [view setShowsDivider:(opacity > 0 ? YES : NO)];
742
743  // We may not have a toolbar view (e.g., popup windows only have a location
744  // bar).
745  if ([view isKindOfClass:[ToolbarView class]]) {
746    ToolbarView* toolbarView = (ToolbarView*)view;
747    [toolbarView setDividerOpacity:opacity];
748  }
749}
750
751- (BrowserActionsController*)browserActionsController {
752  return browserActionsController_.get();
753}
754
755// (URLDropTargetController protocol)
756- (void)dropURLs:(NSArray*)urls inView:(NSView*)view at:(NSPoint)point {
757  // TODO(viettrungluu): This code is more or less copied from the code in
758  // |TabStripController|. I'll refactor this soon to make it common and expand
759  // its capabilities (e.g., allow text DnD).
760  if ([urls count] < 1) {
761    NOTREACHED();
762    return;
763  }
764
765  // TODO(viettrungluu): dropping multiple URLs?
766  if ([urls count] > 1)
767    NOTIMPLEMENTED();
768
769  // Get the first URL and fix it up.
770  GURL url(URLFixerUpper::FixupURL(
771      base::SysNSStringToUTF8([urls objectAtIndex:0]), std::string()));
772
773  browser_->GetSelectedTabContents()->OpenURL(url, GURL(), CURRENT_TAB,
774                                              PageTransition::TYPED);
775}
776
777// (URLDropTargetController protocol)
778- (void)dropText:(NSString*)text inView:(NSView*)view at:(NSPoint)point {
779  // TODO(viettrungluu): This code is more or less copied from the code in
780  // |TabStripController|. I'll refactor this soon to make it common and expand
781  // its capabilities (e.g., allow text DnD).
782
783  // If the input is plain text, classify the input and make the URL.
784  AutocompleteMatch match;
785  browser_->profile()->GetAutocompleteClassifier()->Classify(
786      base::SysNSStringToUTF16(text), string16(), false, &match, NULL);
787  GURL url(match.destination_url);
788
789  browser_->GetSelectedTabContents()->OpenURL(url, GURL(), CURRENT_TAB,
790                                              PageTransition::TYPED);
791}
792
793// (URLDropTargetController protocol)
794- (void)indicateDropURLsInView:(NSView*)view at:(NSPoint)point {
795  // Do nothing.
796}
797
798// (URLDropTargetController protocol)
799- (void)hideDropURLsIndicatorInView:(NSView*)view {
800  // Do nothing.
801}
802
803@end
804