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