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