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