• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
32namespace {
33
34// The size delta between the font used for the edit and the result
35// rows.
36const int kEditFontAdjust = -1;
37
38// How much to adjust the cell sizing up from the default determined
39// by the font.
40const int kCellHeightAdjust = 6.0;
41
42// How to round off the popup's corners.  Goal is to match star and go
43// buttons.
44const CGFloat kPopupRoundingRadius = 3.5;
45
46// Gap between the field and the popup.
47const CGFloat kPopupFieldGap = 2.0;
48
49// How opaque the popup window should be.  This matches Windows (see
50// autocomplete_popup_contents_view.cc, kGlassPopupTransparency).
51const CGFloat kPopupAlpha = 240.0 / 255.0;
52
53// How far to offset image column from the left.
54const CGFloat kImageXOffset = 4.0;
55
56// How far to offset the text column from the left.
57const CGFloat kTextXOffset = 27.0;
58
59// Animation duration when animating the popup window smaller.
60const NSTimeInterval kShrinkAnimationDuration = 0.1;
61
62// Maximum fraction of the popup width that can be used to display match
63// contents.
64const float kMaxContentsFraction = 0.7;
65
66// NSEvent -buttonNumber for middle mouse button.
67const 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.
72const 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.
77const CGFloat kWindowBorderWidth = 1.0;
78
79// Background colors for different states of the popup elements.
80NSColor* BackgroundColor() {
81  return [[NSColor controlBackgroundColor] colorWithAlphaComponent:kPopupAlpha];
82}
83NSColor* SelectedBackgroundColor() {
84  return [[NSColor selectedControlColor] colorWithAlphaComponent:kPopupAlpha];
85}
86NSColor* HoveredBackgroundColor() {
87  return [[NSColor controlHighlightColor] colorWithAlphaComponent:kPopupAlpha];
88}
89
90static NSColor* ContentTextColor() {
91  return [NSColor blackColor];
92}
93static NSColor* DimContentTextColor() {
94  return [NSColor darkGrayColor];
95}
96static 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().
104NSMutableAttributedString* 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
155NSMutableAttributedString* 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.
193NSAttributedString* 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
282AutocompletePopupViewMac::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
298AutocompletePopupViewMac::~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
310AutocompleteMatrix* 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
321bool AutocompletePopupViewMac::IsOpen() const {
322  return popup_ != nil;
323}
324
325void 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
349void 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
416NSImage* 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
427void 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
512gfx::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
521void 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.
527void AutocompletePopupViewMac::PaintUpdatesNow() {
528  AutocompleteMatrix* matrix = GetAutocompleteMatrix();
529  [matrix selectCellAtRow:model_->selected_line() column:0];
530}
531
532void 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
553void 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
566bool 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