Home | History | Annotate | Download | only in autocomplete
      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 #include <cmath>
      6 
      7 #include "chrome/browser/autocomplete/autocomplete_popup_view_mac.h"
      8 
      9 #include "base/stl_util-inl.h"
     10 #include "base/sys_string_conversions.h"
     11 #include "base/utf_string_conversions.h"
     12 #include "chrome/browser/autocomplete/autocomplete_edit.h"
     13 #include "chrome/browser/autocomplete/autocomplete_edit_view_mac.h"
     14 #include "chrome/browser/autocomplete/autocomplete_match.h"
     15 #include "chrome/browser/autocomplete/autocomplete_popup_model.h"
     16 #include "chrome/browser/instant/instant_confirm_dialog.h"
     17 #include "chrome/browser/instant/promo_counter.h"
     18 #include "chrome/browser/profiles/profile.h"
     19 #include "chrome/browser/ui/cocoa/event_utils.h"
     20 #include "chrome/browser/ui/cocoa/image_utils.h"
     21 #import "chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.h"
     22 #import "chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.h"
     23 #import "chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.h"
     24 #include "grit/theme_resources.h"
     25 #include "skia/ext/skia_utils_mac.h"
     26 #import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
     27 #import "third_party/GTM/AppKit/GTMNSBezierPath+RoundRect.h"
     28 #include "ui/base/resource/resource_bundle.h"
     29 #include "ui/base/text/text_elider.h"
     30 #include "ui/gfx/rect.h"
     31 
     32 namespace {
     33 
     34 // The size delta between the font used for the edit and the result
     35 // rows.
     36 const int kEditFontAdjust = -1;
     37 
     38 // How much to adjust the cell sizing up from the default determined
     39 // by the font.
     40 const int kCellHeightAdjust = 6.0;
     41 
     42 // How to round off the popup's corners.  Goal is to match star and go
     43 // buttons.
     44 const CGFloat kPopupRoundingRadius = 3.5;
     45 
     46 // Gap between the field and the popup.
     47 const CGFloat kPopupFieldGap = 2.0;
     48 
     49 // How opaque the popup window should be.  This matches Windows (see
     50 // autocomplete_popup_contents_view.cc, kGlassPopupTransparency).
     51 const CGFloat kPopupAlpha = 240.0 / 255.0;
     52 
     53 // How far to offset image column from the left.
     54 const CGFloat kImageXOffset = 4.0;
     55 
     56 // How far to offset the text column from the left.
     57 const CGFloat kTextXOffset = 27.0;
     58 
     59 // Animation duration when animating the popup window smaller.
     60 const NSTimeInterval kShrinkAnimationDuration = 0.1;
     61 
     62 // Maximum fraction of the popup width that can be used to display match
     63 // contents.
     64 const float kMaxContentsFraction = 0.7;
     65 
     66 // NSEvent -buttonNumber for middle mouse button.
     67 const static NSInteger kMiddleButtonNumber(2);
     68 
     69 // The autocomplete field's visual border is slightly inset from the
     70 // actual border so that it can spill a glow into the toolbar or
     71 // something like that.  This is how much to inset vertically.
     72 const CGFloat kFieldVisualInset = 1.0;
     73 
     74 // The popup window has a single-pixel border in screen coordinates,
     75 // which has to be backed out to line the borders up with the field
     76 // borders.
     77 const CGFloat kWindowBorderWidth = 1.0;
     78 
     79 // Background colors for different states of the popup elements.
     80 NSColor* BackgroundColor() {
     81   return [[NSColor controlBackgroundColor] colorWithAlphaComponent:kPopupAlpha];
     82 }
     83 NSColor* SelectedBackgroundColor() {
     84   return [[NSColor selectedControlColor] colorWithAlphaComponent:kPopupAlpha];
     85 }
     86 NSColor* HoveredBackgroundColor() {
     87   return [[NSColor controlHighlightColor] colorWithAlphaComponent:kPopupAlpha];
     88 }
     89 
     90 static NSColor* ContentTextColor() {
     91   return [NSColor blackColor];
     92 }
     93 static NSColor* DimContentTextColor() {
     94   return [NSColor darkGrayColor];
     95 }
     96 static NSColor* URLTextColor() {
     97   return [NSColor colorWithCalibratedRed:0.0 green:0.55 blue:0.0 alpha:1.0];
     98 }
     99 }  // namespace
    100 
    101 // Helper for MatchText() to allow sharing code between the contents
    102 // and description cases.  Returns NSMutableAttributedString as a
    103 // convenience for MatchText().
    104 NSMutableAttributedString* AutocompletePopupViewMac::DecorateMatchedString(
    105     const string16 &matchString,
    106     const AutocompleteMatch::ACMatchClassifications &classifications,
    107     NSColor* textColor, NSColor* dimTextColor, gfx::Font& font) {
    108   // Cache for on-demand computation of the bold version of |font|.
    109   NSFont* boldFont = nil;
    110 
    111   // Start out with a string using the default style info.
    112   NSString* s = base::SysUTF16ToNSString(matchString);
    113   NSDictionary* attributes = [NSDictionary dictionaryWithObjectsAndKeys:
    114                                   font.GetNativeFont(), NSFontAttributeName,
    115                                   textColor, NSForegroundColorAttributeName,
    116                                   nil];
    117   NSMutableAttributedString* as =
    118       [[[NSMutableAttributedString alloc] initWithString:s
    119                                               attributes:attributes]
    120         autorelease];
    121 
    122   // Mark up the runs which differ from the default.
    123   for (ACMatchClassifications::const_iterator i = classifications.begin();
    124        i != classifications.end(); ++i) {
    125     const BOOL isLast = (i+1) == classifications.end();
    126     const size_t nextOffset = (isLast ? matchString.length() : (i+1)->offset);
    127     const NSInteger location = static_cast<NSInteger>(i->offset);
    128     const NSInteger length = static_cast<NSInteger>(nextOffset - i->offset);
    129     const NSRange range = NSMakeRange(location, length);
    130 
    131     if (0 != (i->style & ACMatchClassification::URL)) {
    132       [as addAttribute:NSForegroundColorAttributeName
    133                  value:URLTextColor() range:range];
    134     }
    135 
    136     if (0 != (i->style & ACMatchClassification::MATCH)) {
    137       if (!boldFont) {
    138         NSFontManager* fontManager = [NSFontManager sharedFontManager];
    139         boldFont = [fontManager convertFont:font.GetNativeFont()
    140                                 toHaveTrait:NSBoldFontMask];
    141       }
    142       [as addAttribute:NSFontAttributeName value:boldFont range:range];
    143     }
    144 
    145     if (0 != (i->style & ACMatchClassification::DIM)) {
    146       [as addAttribute:NSForegroundColorAttributeName
    147                  value:dimTextColor
    148                  range:range];
    149     }
    150   }
    151 
    152   return as;
    153 }
    154 
    155 NSMutableAttributedString* AutocompletePopupViewMac::ElideString(
    156     NSMutableAttributedString* aString,
    157     const string16 originalString,
    158     const gfx::Font& font,
    159     const float width) {
    160   // If it already fits, nothing to be done.
    161   if ([aString size].width <= width) {
    162     return aString;
    163   }
    164 
    165   // If ElideText() decides to do nothing, nothing to be done.
    166   const string16 elided = ui::ElideText(originalString, font, width, false);
    167   if (0 == elided.compare(originalString)) {
    168     return aString;
    169   }
    170 
    171   // If everything was elided away, clear the string.
    172   if (elided.empty()) {
    173     [aString deleteCharactersInRange:NSMakeRange(0, [aString length])];
    174     return aString;
    175   }
    176 
    177   // The ellipses should be the last character, and everything before
    178   // that should match the original string.
    179   const size_t i(elided.size() - 1);
    180   DCHECK_NE(0, elided.compare(0, i, originalString));
    181 
    182   // Replace the end of |aString| with the ellipses from |elided|.
    183   NSString* s = base::SysUTF16ToNSString(elided.substr(i));
    184   [aString replaceCharactersInRange:NSMakeRange(i, [aString length] - i)
    185                          withString:s];
    186 
    187   return aString;
    188 }
    189 
    190 // Return the text to show for the match, based on the match's
    191 // contents and description.  Result will be in |font|, with the
    192 // boldfaced version used for matches.
    193 NSAttributedString* AutocompletePopupViewMac::MatchText(
    194     const AutocompleteMatch& match, gfx::Font& font, float cellWidth) {
    195   NSMutableAttributedString *as =
    196       DecorateMatchedString(match.contents,
    197                             match.contents_class,
    198                             ContentTextColor(),
    199                             DimContentTextColor(),
    200                             font);
    201 
    202   // If there is a description, append it, separated from the contents
    203   // with an en dash, and decorated with a distinct color.
    204   if (!match.description.empty()) {
    205     // Make sure the current string fits w/in kMaxContentsFraction of
    206     // the cell to make sure the description will be at least
    207     // partially visible.
    208     // TODO(shess): Consider revising our NSCell subclass to have two
    209     // bits and just draw them right, rather than truncating here.
    210     const float textWidth = cellWidth - kTextXOffset;
    211     as = ElideString(as, match.contents, font,
    212                      textWidth * kMaxContentsFraction);
    213 
    214     NSDictionary* attributes =
    215         [NSDictionary dictionaryWithObjectsAndKeys:
    216              font.GetNativeFont(), NSFontAttributeName,
    217              ContentTextColor(), NSForegroundColorAttributeName,
    218              nil];
    219     NSString* rawEnDash = [NSString stringWithFormat:@" %C ", 0x2013];
    220     NSAttributedString* enDash =
    221         [[[NSAttributedString alloc] initWithString:rawEnDash
    222                                          attributes:attributes] autorelease];
    223 
    224     // In Windows, a boolean force_dim is passed as true for the
    225     // description.  Here, we pass the dim text color for both normal and dim,
    226     // to accomplish the same thing.
    227     NSAttributedString* description =
    228         DecorateMatchedString(match.description, match.description_class,
    229                               DimContentTextColor(),
    230                               DimContentTextColor(),
    231                               font);
    232 
    233     [as appendAttributedString:enDash];
    234     [as appendAttributedString:description];
    235   }
    236 
    237   NSMutableParagraphStyle* style =
    238       [[[NSMutableParagraphStyle alloc] init] autorelease];
    239   [style setLineBreakMode:NSLineBreakByTruncatingTail];
    240   [style setTighteningFactorForTruncation:0.0];
    241   [as addAttribute:NSParagraphStyleAttributeName value:style
    242              range:NSMakeRange(0, [as length])];
    243 
    244   return as;
    245 }
    246 
    247 // AutocompleteButtonCell overrides how backgrounds are displayed to
    248 // handle hover versus selected.  So long as we're in there, it also
    249 // provides some default initialization.
    250 
    251 @interface AutocompleteButtonCell : NSButtonCell {
    252 }
    253 @end
    254 
    255 // AutocompleteMatrix sets up a tracking area to implement hover by
    256 // highlighting the cell the mouse is over.
    257 
    258 @interface AutocompleteMatrix : NSMatrix {
    259  @private
    260   // If YES, the matrix draws itself with rounded corners at the bottom.
    261   // Otherwise, the bottom corners will be square.
    262   BOOL bottomCornersRounded_;
    263 
    264   // Target for click and middle-click.
    265   AutocompletePopupViewMac* popupView_;  // weak, owns us.
    266 }
    267 
    268 @property(assign, nonatomic) BOOL bottomCornersRounded;
    269 
    270 // Create a zero-size matrix initializing |popupView_|.
    271 - initWithPopupView:(AutocompletePopupViewMac*)popupView;
    272 
    273 // Set |popupView_|.
    274 - (void)setPopupView:(AutocompletePopupViewMac*)popupView;
    275 
    276 // Return the currently highlighted row.  Returns -1 if no row is
    277 // highlighted.
    278 - (NSInteger)highlightedRow;
    279 
    280 @end
    281 
    282 AutocompletePopupViewMac::AutocompletePopupViewMac(
    283     AutocompleteEditViewMac* edit_view,
    284     AutocompleteEditModel* edit_model,
    285     Profile* profile,
    286     NSTextField* field)
    287     : model_(new AutocompletePopupModel(this, edit_model, profile)),
    288       edit_view_(edit_view),
    289       field_(field),
    290       popup_(nil),
    291       opt_in_controller_(nil),
    292       targetPopupFrame_(NSZeroRect) {
    293   DCHECK(edit_view);
    294   DCHECK(edit_model);
    295   DCHECK(profile);
    296 }
    297 
    298 AutocompletePopupViewMac::~AutocompletePopupViewMac() {
    299   // Destroy the popup model before this object is destroyed, because
    300   // it can call back to us in the destructor.
    301   model_.reset();
    302 
    303   // Break references to |this| because the popup may not be
    304   // deallocated immediately.
    305   AutocompleteMatrix* matrix = GetAutocompleteMatrix();
    306   DCHECK(matrix == nil || [matrix isKindOfClass:[AutocompleteMatrix class]]);
    307   [matrix setPopupView:NULL];
    308 }
    309 
    310 AutocompleteMatrix* AutocompletePopupViewMac::GetAutocompleteMatrix() {
    311   // The AutocompleteMatrix will always be the first subview of the popup's
    312   // content view.
    313   if (popup_ && [[[popup_ contentView] subviews] count]) {
    314     NSArray* subviews = [[popup_ contentView] subviews];
    315     DCHECK_GE([subviews count], 0U);
    316     return (AutocompleteMatrix*)[subviews objectAtIndex:0];
    317   }
    318   return nil;
    319 }
    320 
    321 bool AutocompletePopupViewMac::IsOpen() const {
    322   return popup_ != nil;
    323 }
    324 
    325 void AutocompletePopupViewMac::CreatePopupIfNeeded() {
    326   if (!popup_) {
    327     popup_.reset([[NSWindow alloc] initWithContentRect:NSZeroRect
    328                                              styleMask:NSBorderlessWindowMask
    329                                                backing:NSBackingStoreBuffered
    330                                                  defer:YES]);
    331     [popup_ setMovableByWindowBackground:NO];
    332     // The window shape is determined by the content view (OmniboxPopupView).
    333     [popup_ setAlphaValue:1.0];
    334     [popup_ setOpaque:NO];
    335     [popup_ setBackgroundColor:[NSColor clearColor]];
    336     [popup_ setHasShadow:YES];
    337     [popup_ setLevel:NSNormalWindowLevel];
    338 
    339     scoped_nsobject<AutocompleteMatrix> matrix(
    340         [[AutocompleteMatrix alloc] initWithPopupView:this]);
    341     scoped_nsobject<OmniboxPopupView> contentView(
    342         [[OmniboxPopupView alloc] initWithFrame:NSZeroRect]);
    343 
    344     [contentView addSubview:matrix];
    345     [popup_ setContentView:contentView];
    346   }
    347 }
    348 
    349 void AutocompletePopupViewMac::PositionPopup(const CGFloat matrixHeight) {
    350   // Calculate the popup's position on the screen.  It should abut the
    351   // field's visual border vertically, and be below the bounds
    352   // horizontally.
    353 
    354   // Start with the field's rect on the screen.
    355   NSRect popupFrame = NSInsetRect([field_ bounds], 0.0, kFieldVisualInset);
    356   popupFrame = [field_ convertRect:popupFrame toView:nil];
    357   popupFrame.origin = [[field_ window] convertBaseToScreen:popupFrame.origin];
    358 
    359   // Size to fit the matrix, and shift down by the size plus the top
    360   // window border.  Would prefer -convertSize:fromView: to
    361   // -userSpaceScaleFactor for the scale conversion, but until the
    362   // window is on-screen that doesn't work right (bug?).
    363   popupFrame.size.height = matrixHeight * [popup_ userSpaceScaleFactor];
    364   popupFrame.origin.y -= NSHeight(popupFrame) + kWindowBorderWidth;
    365 
    366   // Inset to account for the horizontal border drawn by the window.
    367   popupFrame = NSInsetRect(popupFrame, kWindowBorderWidth, 0.0);
    368 
    369   // Leave a gap between the popup and the field.
    370   popupFrame.origin.y -= kPopupFieldGap * [popup_ userSpaceScaleFactor];
    371 
    372   // Do nothing if the popup is already animating to the given |frame|.
    373   if (NSEqualRects(popupFrame, targetPopupFrame_))
    374     return;
    375 
    376   NSRect currentPopupFrame = [popup_ frame];
    377   targetPopupFrame_ = popupFrame;
    378 
    379   // Animate the frame change if the only change is that the height got smaller.
    380   // Otherwise, resize immediately.
    381   bool animate = (NSHeight(popupFrame) < NSHeight(currentPopupFrame) &&
    382                   NSWidth(popupFrame) == NSWidth(currentPopupFrame));
    383 
    384   NSDictionary* savedAnimations = nil;
    385   if (!animate) {
    386     // In an ideal world, running a zero-length animation would cancel any
    387     // running animations and set the new frame value immediately.  In practice,
    388     // zero-length animations are ignored entirely.  Work around this AppKit bug
    389     // by explicitly setting an NSNull animation for the "frame" key and then
    390     // running the animation with a non-zero(!!) duration.  This somehow
    391     // convinces AppKit to do the right thing.  Save off the current animations
    392     // dictionary so it can be restored later.
    393     savedAnimations = [[popup_ animations] copy];
    394     [popup_ setAnimations:
    395               [NSDictionary dictionaryWithObjectsAndKeys:[NSNull null],
    396                                                          @"frame", nil]];
    397   }
    398 
    399   [NSAnimationContext beginGrouping];
    400   // Don't use the GTM additon for the "Steve" slowdown because this can happen
    401   // async from user actions and the effects could be a surprise.
    402   [[NSAnimationContext currentContext] setDuration:kShrinkAnimationDuration];
    403   [[popup_ animator] setFrame:popupFrame display:YES];
    404   [NSAnimationContext endGrouping];
    405 
    406   if (!animate) {
    407     // Restore the original animations dictionary.  This does not reinstate any
    408     // previously running animations.
    409     [popup_ setAnimations:savedAnimations];
    410   }
    411 
    412   if (![popup_ isVisible])
    413     [[field_ window] addChildWindow:popup_ ordered:NSWindowAbove];
    414 }
    415 
    416 NSImage* AutocompletePopupViewMac::ImageForMatch(
    417     const AutocompleteMatch& match) {
    418   const SkBitmap* bitmap = model_->GetIconIfExtensionMatch(match);
    419   if (bitmap)
    420     return gfx::SkBitmapToNSImage(*bitmap);
    421 
    422   const int resource_id = match.starred ?
    423       IDR_OMNIBOX_STAR : AutocompleteMatch::TypeToIcon(match.type);
    424   return AutocompleteEditViewMac::ImageForResource(resource_id);
    425 }
    426 
    427 void AutocompletePopupViewMac::UpdatePopupAppearance() {
    428   DCHECK([NSThread isMainThread]);
    429   const AutocompleteResult& result = model_->result();
    430   if (result.empty()) {
    431     [[popup_ parentWindow] removeChildWindow:popup_];
    432     [popup_ orderOut:nil];
    433 
    434     // Break references to |this| because the popup may not be
    435     // deallocated immediately.
    436     AutocompleteMatrix* matrix = GetAutocompleteMatrix();
    437     DCHECK(matrix == nil || [matrix isKindOfClass:[AutocompleteMatrix class]]);
    438     [matrix setPopupView:NULL];
    439 
    440     popup_.reset(nil);
    441 
    442     targetPopupFrame_ = NSZeroRect;
    443 
    444     return;
    445   }
    446 
    447   CreatePopupIfNeeded();
    448 
    449   // The popup's font is a slightly smaller version of the field's.
    450   NSFont* fieldFont = AutocompleteEditViewMac::GetFieldFont();
    451   const CGFloat resultFontSize = [fieldFont pointSize] + kEditFontAdjust;
    452   gfx::Font resultFont(base::SysNSStringToUTF16([fieldFont fontName]),
    453                        static_cast<int>(resultFontSize));
    454 
    455   AutocompleteMatrix* matrix = GetAutocompleteMatrix();
    456 
    457   // Calculate the width of the matrix based on backing out the
    458   // popup's border from the width of the field.  Would prefer to use
    459   // [matrix convertSize:fromView:] for converting from screen size,
    460   // but that doesn't work until the popup is on-screen (bug?).
    461   const NSRect fieldRectBase = [field_ convertRect:[field_ bounds] toView:nil];
    462   const CGFloat popupWidth = NSWidth(fieldRectBase) - 2 * kWindowBorderWidth;
    463   DCHECK_GT(popupWidth, 0.0);
    464   const CGFloat matrixWidth = popupWidth / [popup_ userSpaceScaleFactor];
    465 
    466   // Load the results into the popup's matrix.
    467   const size_t rows = model_->result().size();
    468   DCHECK_GT(rows, 0U);
    469   [matrix renewRows:rows columns:1];
    470   for (size_t ii = 0; ii < rows; ++ii) {
    471     AutocompleteButtonCell* cell = [matrix cellAtRow:ii column:0];
    472     const AutocompleteMatch& match = model_->result().match_at(ii);
    473     [cell setImage:ImageForMatch(match)];
    474     [cell setAttributedTitle:MatchText(match, resultFont, matrixWidth)];
    475   }
    476 
    477   // Set the cell size to fit a line of text in the cell's font.  All
    478   // cells should use the same font and each should layout in one
    479   // line, so they should all be about the same height.
    480   const NSSize cellSize = [[matrix cellAtRow:0 column:0] cellSize];
    481   DCHECK_GT(cellSize.height, 0.0);
    482   const CGFloat cellHeight = cellSize.height + kCellHeightAdjust;
    483   [matrix setCellSize:NSMakeSize(matrixWidth, cellHeight)];
    484 
    485   // Add in the instant view if needed and not already present.
    486   CGFloat instantHeight = 0;
    487   if (ShouldShowInstantOptIn()) {
    488     if (!opt_in_controller_.get()) {
    489       opt_in_controller_.reset(
    490           [[InstantOptInController alloc] initWithDelegate:this]);
    491     }
    492     [[popup_ contentView] addSubview:[opt_in_controller_ view]];
    493     [GetAutocompleteMatrix() setBottomCornersRounded:NO];
    494     instantHeight = NSHeight([[opt_in_controller_ view] frame]);
    495   } else {
    496     [[opt_in_controller_ view] removeFromSuperview];
    497     opt_in_controller_.reset(nil);
    498     [GetAutocompleteMatrix() setBottomCornersRounded:YES];
    499   }
    500 
    501   // Update the selection before placing (and displaying) the window.
    502   PaintUpdatesNow();
    503 
    504   // Calculate the matrix size manually rather than using -sizeToCells
    505   // because actually resizing the matrix messed up the popup size
    506   // animation.
    507   DCHECK_EQ([matrix intercellSpacing].height, 0.0);
    508   CGFloat matrixHeight = rows * cellHeight;
    509   PositionPopup(matrixHeight + instantHeight);
    510 }
    511 
    512 gfx::Rect AutocompletePopupViewMac::GetTargetBounds() {
    513   // Flip the coordinate system before returning.
    514   NSScreen* screen = [[NSScreen screens] objectAtIndex:0];
    515   NSRect monitorFrame = [screen frame];
    516   gfx::Rect bounds(NSRectToCGRect(targetPopupFrame_));
    517   bounds.set_y(monitorFrame.size.height - bounds.y() - bounds.height());
    518   return bounds;
    519 }
    520 
    521 void AutocompletePopupViewMac::SetSelectedLine(size_t line) {
    522   model_->SetSelectedLine(line, false, false);
    523 }
    524 
    525 // This is only called by model in SetSelectedLine() after updating
    526 // everything.  Popup should already be visible.
    527 void AutocompletePopupViewMac::PaintUpdatesNow() {
    528   AutocompleteMatrix* matrix = GetAutocompleteMatrix();
    529   [matrix selectCellAtRow:model_->selected_line() column:0];
    530 }
    531 
    532 void AutocompletePopupViewMac::OpenURLForRow(int row, bool force_background) {
    533   DCHECK_GE(row, 0);
    534 
    535   WindowOpenDisposition disposition = NEW_BACKGROUND_TAB;
    536   if (!force_background) {
    537     disposition =
    538         event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
    539   }
    540 
    541   // OpenURL() may close the popup, which will clear the result set
    542   // and, by extension, |match| and its contents.  So copy the
    543   // relevant strings out to make sure they stay alive until the call
    544   // completes.
    545   const AutocompleteMatch& match = model_->result().match_at(row);
    546   const GURL url(match.destination_url);
    547   string16 keyword;
    548   const bool is_keyword_hint = model_->GetKeywordForMatch(match, &keyword);
    549   edit_view_->OpenURL(url, disposition, match.transition, GURL(), row,
    550                       is_keyword_hint ? string16() : keyword);
    551 }
    552 
    553 void AutocompletePopupViewMac::UserPressedOptIn(bool opt_in) {
    554   PromoCounter* counter = model_->profile()->GetInstantPromoCounter();
    555   DCHECK(counter);
    556   counter->Hide();
    557   if (opt_in) {
    558     browser::ShowInstantConfirmDialogIfNecessary([field_ window],
    559                                                  model_->profile());
    560   }
    561 
    562   // This call will remove and delete |opt_in_controller_|.
    563   UpdatePopupAppearance();
    564 }
    565 
    566 bool AutocompletePopupViewMac::ShouldShowInstantOptIn() {
    567   PromoCounter* counter = model_->profile()->GetInstantPromoCounter();
    568   return (counter && counter->ShouldShow(base::Time::Now()));
    569 }
    570 
    571 @implementation AutocompleteButtonCell
    572 
    573 - init {
    574   self = [super init];
    575   if (self) {
    576     [self setImagePosition:NSImageLeft];
    577     [self setBordered:NO];
    578     [self setButtonType:NSRadioButton];
    579 
    580     // Without this highlighting messes up white areas of images.
    581     [self setHighlightsBy:NSNoCellMask];
    582   }
    583   return self;
    584 }
    585 
    586 - (NSColor*)backgroundColor {
    587   if ([self state] == NSOnState) {
    588     return SelectedBackgroundColor();
    589   } else if ([self isHighlighted]) {
    590     return HoveredBackgroundColor();
    591   }
    592   return BackgroundColor();
    593 }
    594 
    595 // The default NSButtonCell drawing leaves the image flush left and
    596 // the title next to the image.  This spaces things out to line up
    597 // with the star button and autocomplete field.
    598 - (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
    599   [[self backgroundColor] set];
    600   NSRectFill(cellFrame);
    601 
    602   // Put the image centered vertically but in a fixed column.
    603   NSImage* image = [self image];
    604   if (image) {
    605     NSRect imageRect = cellFrame;
    606     imageRect.size = [image size];
    607     imageRect.origin.y +=
    608         std::floor((NSHeight(cellFrame) - NSHeight(imageRect)) / 2.0);
    609     imageRect.origin.x += kImageXOffset;
    610     [image drawInRect:imageRect
    611              fromRect:NSZeroRect  // Entire image
    612             operation:NSCompositeSourceOver
    613              fraction:1.0
    614          neverFlipped:YES];
    615   }
    616 
    617   // Adjust the title position to be lined up under the field's text.
    618   NSAttributedString* title = [self attributedTitle];
    619   if (title && [title length]) {
    620     NSRect titleRect = cellFrame;
    621     titleRect.size.width -= kTextXOffset;
    622     titleRect.origin.x += kTextXOffset;
    623     [self drawTitle:title withFrame:titleRect inView:controlView];
    624   }
    625 }
    626 
    627 @end
    628 
    629 @implementation AutocompleteMatrix
    630 
    631 @synthesize bottomCornersRounded = bottomCornersRounded_;
    632 
    633 // Remove all tracking areas and initialize the one we want.  Removing
    634 // all might be overkill, but it's unclear why there would be others
    635 // for the popup window.
    636 - (void)resetTrackingArea {
    637   for (NSTrackingArea* trackingArea in [self trackingAreas]) {
    638     [self removeTrackingArea:trackingArea];
    639   }
    640 
    641   // TODO(shess): Consider overriding -acceptsFirstMouse: and changing
    642   // NSTrackingActiveInActiveApp to NSTrackingActiveAlways.
    643   NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited;
    644   options |= NSTrackingMouseMoved;
    645   options |= NSTrackingActiveInActiveApp;
    646   options |= NSTrackingInVisibleRect;
    647 
    648   scoped_nsobject<NSTrackingArea> trackingArea(
    649       [[NSTrackingArea alloc] initWithRect:[self frame]
    650                                    options:options
    651                                      owner:self
    652                                   userInfo:nil]);
    653   [self addTrackingArea:trackingArea];
    654 }
    655 
    656 - (void)updateTrackingAreas {
    657   [self resetTrackingArea];
    658   [super updateTrackingAreas];
    659 }
    660 
    661 - initWithPopupView:(AutocompletePopupViewMac*)popupView {
    662   self = [super initWithFrame:NSZeroRect];
    663   if (self) {
    664     popupView_ = popupView;
    665 
    666     [self setCellClass:[AutocompleteButtonCell class]];
    667 
    668     // Cells pack with no spacing.
    669     [self setIntercellSpacing:NSMakeSize(0.0, 0.0)];
    670 
    671     [self setDrawsBackground:YES];
    672     [self setBackgroundColor:BackgroundColor()];
    673     [self renewRows:0 columns:1];
    674     [self setAllowsEmptySelection:YES];
    675     [self setMode:NSRadioModeMatrix];
    676     [self deselectAllCells];
    677 
    678     [self resetTrackingArea];
    679   }
    680   return self;
    681 }
    682 
    683 - (void)setPopupView:(AutocompletePopupViewMac*)popupView {
    684   popupView_ = popupView;
    685 }
    686 
    687 - (void)highlightRowAt:(NSInteger)rowIndex {
    688   // highlightCell will be nil if rowIndex is out of range, so no cell
    689   // will be highlighted.
    690   NSCell* highlightCell = [self cellAtRow:rowIndex column:0];
    691 
    692   for (NSCell* cell in [self cells]) {
    693     [cell setHighlighted:(cell == highlightCell)];
    694   }
    695 }
    696 
    697 - (void)highlightRowUnder:(NSEvent*)theEvent {
    698   NSPoint point = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    699   NSInteger row, column;
    700   if ([self getRow:&row column:&column forPoint:point]) {
    701     [self highlightRowAt:row];
    702   } else {
    703     [self highlightRowAt:-1];
    704   }
    705 }
    706 
    707 // Callbacks from NSTrackingArea.
    708 - (void)mouseEntered:(NSEvent*)theEvent {
    709   [self highlightRowUnder:theEvent];
    710 }
    711 - (void)mouseMoved:(NSEvent*)theEvent {
    712   [self highlightRowUnder:theEvent];
    713 }
    714 - (void)mouseExited:(NSEvent*)theEvent {
    715   [self highlightRowAt:-1];
    716 }
    717 
    718 // The tracking area events aren't forwarded during a drag, so handle
    719 // highlighting manually for middle-click and middle-drag.
    720 - (void)otherMouseDown:(NSEvent*)theEvent {
    721   if ([theEvent buttonNumber] == kMiddleButtonNumber) {
    722     [self highlightRowUnder:theEvent];
    723   }
    724   [super otherMouseDown:theEvent];
    725 }
    726 - (void)otherMouseDragged:(NSEvent*)theEvent {
    727   if ([theEvent buttonNumber] == kMiddleButtonNumber) {
    728     [self highlightRowUnder:theEvent];
    729   }
    730   [super otherMouseDragged:theEvent];
    731 }
    732 
    733 - (void)otherMouseUp:(NSEvent*)theEvent {
    734   // Only intercept middle button.
    735   if ([theEvent buttonNumber] != kMiddleButtonNumber) {
    736     [super otherMouseUp:theEvent];
    737     return;
    738   }
    739 
    740   // -otherMouseDragged: should always have been called at this
    741   // location, but make sure the user is getting the right feedback.
    742   [self highlightRowUnder:theEvent];
    743 
    744   const NSInteger highlightedRow = [self highlightedRow];
    745   if (highlightedRow != -1) {
    746     DCHECK(popupView_);
    747     popupView_->OpenURLForRow(highlightedRow, true);
    748   }
    749 }
    750 
    751 // Select cell under |theEvent|, returning YES if a selection is made.
    752 - (BOOL)selectCellForEvent:(NSEvent*)theEvent {
    753   NSPoint point = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    754 
    755   NSInteger row, column;
    756   if ([self getRow:&row column:&column forPoint:point]) {
    757     DCHECK_EQ(column, 0);
    758     DCHECK(popupView_);
    759     popupView_->SetSelectedLine(row);
    760     return YES;
    761   }
    762   return NO;
    763 }
    764 
    765 // Track the mouse until released, keeping the cell under the mouse
    766 // selected.  If the mouse wanders off-view, revert to the
    767 // originally-selected cell.  If the mouse is released over a cell,
    768 // call |popupView_| to open the row's URL.
    769 - (void)mouseDown:(NSEvent*)theEvent {
    770   NSCell* selectedCell = [self selectedCell];
    771 
    772   // Clear any existing highlight.
    773   [self highlightRowAt:-1];
    774 
    775   do {
    776     if (![self selectCellForEvent:theEvent]) {
    777       [self selectCell:selectedCell];
    778     }
    779 
    780     const NSUInteger mask = NSLeftMouseUpMask | NSLeftMouseDraggedMask;
    781     theEvent = [[self window] nextEventMatchingMask:mask];
    782   } while ([theEvent type] == NSLeftMouseDragged);
    783 
    784   // Do not message |popupView_| if released outside view.
    785   if (![self selectCellForEvent:theEvent]) {
    786     [self selectCell:selectedCell];
    787   } else {
    788     const NSInteger selectedRow = [self selectedRow];
    789     DCHECK_GE(selectedRow, 0);
    790 
    791     DCHECK(popupView_);
    792     popupView_->OpenURLForRow(selectedRow, false);
    793   }
    794 }
    795 
    796 - (NSInteger)highlightedRow {
    797   NSArray* cells = [self cells];
    798   const NSUInteger count = [cells count];
    799   for(NSUInteger i = 0; i < count; ++i) {
    800     if ([[cells objectAtIndex:i] isHighlighted]) {
    801       return i;
    802     }
    803   }
    804   return -1;
    805 }
    806 
    807 - (BOOL)isOpaque {
    808   return NO;
    809 }
    810 
    811 // This handles drawing the decorations of the rounded popup window,
    812 // calling on NSMatrix to draw the actual contents.
    813 - (void)drawRect:(NSRect)rect {
    814   CGFloat bottomCornerRadius =
    815       (bottomCornersRounded_ ? kPopupRoundingRadius : 0);
    816 
    817   // "Top" really means "bottom" here, since the view is flipped.
    818   NSBezierPath* path =
    819      [NSBezierPath gtm_bezierPathWithRoundRect:[self bounds]
    820                            topLeftCornerRadius:bottomCornerRadius
    821                           topRightCornerRadius:bottomCornerRadius
    822                         bottomLeftCornerRadius:kPopupRoundingRadius
    823                        bottomRightCornerRadius:kPopupRoundingRadius];
    824 
    825   // Draw the matrix clipped to our border.
    826   [NSGraphicsContext saveGraphicsState];
    827   [path addClip];
    828   [super drawRect:rect];
    829   [NSGraphicsContext restoreGraphicsState];
    830 }
    831 
    832 @end
    833