// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include #include "chrome/browser/autocomplete/autocomplete_popup_view_mac.h" #include "base/stl_util-inl.h" #include "base/sys_string_conversions.h" #include "base/utf_string_conversions.h" #include "chrome/browser/autocomplete/autocomplete_edit.h" #include "chrome/browser/autocomplete/autocomplete_edit_view_mac.h" #include "chrome/browser/autocomplete/autocomplete_match.h" #include "chrome/browser/autocomplete/autocomplete_popup_model.h" #include "chrome/browser/instant/instant_confirm_dialog.h" #include "chrome/browser/instant/promo_counter.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/cocoa/event_utils.h" #include "chrome/browser/ui/cocoa/image_utils.h" #import "chrome/browser/ui/cocoa/location_bar/instant_opt_in_controller.h" #import "chrome/browser/ui/cocoa/location_bar/instant_opt_in_view.h" #import "chrome/browser/ui/cocoa/location_bar/omnibox_popup_view.h" #include "grit/theme_resources.h" #include "skia/ext/skia_utils_mac.h" #import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h" #import "third_party/GTM/AppKit/GTMNSBezierPath+RoundRect.h" #include "ui/base/resource/resource_bundle.h" #include "ui/base/text/text_elider.h" #include "ui/gfx/rect.h" namespace { // The size delta between the font used for the edit and the result // rows. const int kEditFontAdjust = -1; // How much to adjust the cell sizing up from the default determined // by the font. const int kCellHeightAdjust = 6.0; // How to round off the popup's corners. Goal is to match star and go // buttons. const CGFloat kPopupRoundingRadius = 3.5; // Gap between the field and the popup. const CGFloat kPopupFieldGap = 2.0; // How opaque the popup window should be. This matches Windows (see // autocomplete_popup_contents_view.cc, kGlassPopupTransparency). const CGFloat kPopupAlpha = 240.0 / 255.0; // How far to offset image column from the left. const CGFloat kImageXOffset = 4.0; // How far to offset the text column from the left. const CGFloat kTextXOffset = 27.0; // Animation duration when animating the popup window smaller. const NSTimeInterval kShrinkAnimationDuration = 0.1; // Maximum fraction of the popup width that can be used to display match // contents. const float kMaxContentsFraction = 0.7; // NSEvent -buttonNumber for middle mouse button. const static NSInteger kMiddleButtonNumber(2); // The autocomplete field's visual border is slightly inset from the // actual border so that it can spill a glow into the toolbar or // something like that. This is how much to inset vertically. const CGFloat kFieldVisualInset = 1.0; // The popup window has a single-pixel border in screen coordinates, // which has to be backed out to line the borders up with the field // borders. const CGFloat kWindowBorderWidth = 1.0; // Background colors for different states of the popup elements. NSColor* BackgroundColor() { return [[NSColor controlBackgroundColor] colorWithAlphaComponent:kPopupAlpha]; } NSColor* SelectedBackgroundColor() { return [[NSColor selectedControlColor] colorWithAlphaComponent:kPopupAlpha]; } NSColor* HoveredBackgroundColor() { return [[NSColor controlHighlightColor] colorWithAlphaComponent:kPopupAlpha]; } static NSColor* ContentTextColor() { return [NSColor blackColor]; } static NSColor* DimContentTextColor() { return [NSColor darkGrayColor]; } static NSColor* URLTextColor() { return [NSColor colorWithCalibratedRed:0.0 green:0.55 blue:0.0 alpha:1.0]; } } // namespace // Helper for MatchText() to allow sharing code between the contents // and description cases. Returns NSMutableAttributedString as a // convenience for MatchText(). NSMutableAttributedString* AutocompletePopupViewMac::DecorateMatchedString( const string16 &matchString, const AutocompleteMatch::ACMatchClassifications &classifications, NSColor* textColor, NSColor* dimTextColor, gfx::Font& font) { // Cache for on-demand computation of the bold version of |font|. NSFont* boldFont = nil; // Start out with a string using the default style info. NSString* s = base::SysUTF16ToNSString(matchString); NSDictionary* attributes = [NSDictionary dictionaryWithObjectsAndKeys: font.GetNativeFont(), NSFontAttributeName, textColor, NSForegroundColorAttributeName, nil]; NSMutableAttributedString* as = [[[NSMutableAttributedString alloc] initWithString:s attributes:attributes] autorelease]; // Mark up the runs which differ from the default. for (ACMatchClassifications::const_iterator i = classifications.begin(); i != classifications.end(); ++i) { const BOOL isLast = (i+1) == classifications.end(); const size_t nextOffset = (isLast ? matchString.length() : (i+1)->offset); const NSInteger location = static_cast(i->offset); const NSInteger length = static_cast(nextOffset - i->offset); const NSRange range = NSMakeRange(location, length); if (0 != (i->style & ACMatchClassification::URL)) { [as addAttribute:NSForegroundColorAttributeName value:URLTextColor() range:range]; } if (0 != (i->style & ACMatchClassification::MATCH)) { if (!boldFont) { NSFontManager* fontManager = [NSFontManager sharedFontManager]; boldFont = [fontManager convertFont:font.GetNativeFont() toHaveTrait:NSBoldFontMask]; } [as addAttribute:NSFontAttributeName value:boldFont range:range]; } if (0 != (i->style & ACMatchClassification::DIM)) { [as addAttribute:NSForegroundColorAttributeName value:dimTextColor range:range]; } } return as; } NSMutableAttributedString* AutocompletePopupViewMac::ElideString( NSMutableAttributedString* aString, const string16 originalString, const gfx::Font& font, const float width) { // If it already fits, nothing to be done. if ([aString size].width <= width) { return aString; } // If ElideText() decides to do nothing, nothing to be done. const string16 elided = ui::ElideText(originalString, font, width, false); if (0 == elided.compare(originalString)) { return aString; } // If everything was elided away, clear the string. if (elided.empty()) { [aString deleteCharactersInRange:NSMakeRange(0, [aString length])]; return aString; } // The ellipses should be the last character, and everything before // that should match the original string. const size_t i(elided.size() - 1); DCHECK_NE(0, elided.compare(0, i, originalString)); // Replace the end of |aString| with the ellipses from |elided|. NSString* s = base::SysUTF16ToNSString(elided.substr(i)); [aString replaceCharactersInRange:NSMakeRange(i, [aString length] - i) withString:s]; return aString; } // Return the text to show for the match, based on the match's // contents and description. Result will be in |font|, with the // boldfaced version used for matches. NSAttributedString* AutocompletePopupViewMac::MatchText( const AutocompleteMatch& match, gfx::Font& font, float cellWidth) { NSMutableAttributedString *as = DecorateMatchedString(match.contents, match.contents_class, ContentTextColor(), DimContentTextColor(), font); // If there is a description, append it, separated from the contents // with an en dash, and decorated with a distinct color. if (!match.description.empty()) { // Make sure the current string fits w/in kMaxContentsFraction of // the cell to make sure the description will be at least // partially visible. // TODO(shess): Consider revising our NSCell subclass to have two // bits and just draw them right, rather than truncating here. const float textWidth = cellWidth - kTextXOffset; as = ElideString(as, match.contents, font, textWidth * kMaxContentsFraction); NSDictionary* attributes = [NSDictionary dictionaryWithObjectsAndKeys: font.GetNativeFont(), NSFontAttributeName, ContentTextColor(), NSForegroundColorAttributeName, nil]; NSString* rawEnDash = [NSString stringWithFormat:@" %C ", 0x2013]; NSAttributedString* enDash = [[[NSAttributedString alloc] initWithString:rawEnDash attributes:attributes] autorelease]; // In Windows, a boolean force_dim is passed as true for the // description. Here, we pass the dim text color for both normal and dim, // to accomplish the same thing. NSAttributedString* description = DecorateMatchedString(match.description, match.description_class, DimContentTextColor(), DimContentTextColor(), font); [as appendAttributedString:enDash]; [as appendAttributedString:description]; } NSMutableParagraphStyle* style = [[[NSMutableParagraphStyle alloc] init] autorelease]; [style setLineBreakMode:NSLineBreakByTruncatingTail]; [style setTighteningFactorForTruncation:0.0]; [as addAttribute:NSParagraphStyleAttributeName value:style range:NSMakeRange(0, [as length])]; return as; } // AutocompleteButtonCell overrides how backgrounds are displayed to // handle hover versus selected. So long as we're in there, it also // provides some default initialization. @interface AutocompleteButtonCell : NSButtonCell { } @end // AutocompleteMatrix sets up a tracking area to implement hover by // highlighting the cell the mouse is over. @interface AutocompleteMatrix : NSMatrix { @private // If YES, the matrix draws itself with rounded corners at the bottom. // Otherwise, the bottom corners will be square. BOOL bottomCornersRounded_; // Target for click and middle-click. AutocompletePopupViewMac* popupView_; // weak, owns us. } @property(assign, nonatomic) BOOL bottomCornersRounded; // Create a zero-size matrix initializing |popupView_|. - initWithPopupView:(AutocompletePopupViewMac*)popupView; // Set |popupView_|. - (void)setPopupView:(AutocompletePopupViewMac*)popupView; // Return the currently highlighted row. Returns -1 if no row is // highlighted. - (NSInteger)highlightedRow; @end AutocompletePopupViewMac::AutocompletePopupViewMac( AutocompleteEditViewMac* edit_view, AutocompleteEditModel* edit_model, Profile* profile, NSTextField* field) : model_(new AutocompletePopupModel(this, edit_model, profile)), edit_view_(edit_view), field_(field), popup_(nil), opt_in_controller_(nil), targetPopupFrame_(NSZeroRect) { DCHECK(edit_view); DCHECK(edit_model); DCHECK(profile); } AutocompletePopupViewMac::~AutocompletePopupViewMac() { // Destroy the popup model before this object is destroyed, because // it can call back to us in the destructor. model_.reset(); // Break references to |this| because the popup may not be // deallocated immediately. AutocompleteMatrix* matrix = GetAutocompleteMatrix(); DCHECK(matrix == nil || [matrix isKindOfClass:[AutocompleteMatrix class]]); [matrix setPopupView:NULL]; } AutocompleteMatrix* AutocompletePopupViewMac::GetAutocompleteMatrix() { // The AutocompleteMatrix will always be the first subview of the popup's // content view. if (popup_ && [[[popup_ contentView] subviews] count]) { NSArray* subviews = [[popup_ contentView] subviews]; DCHECK_GE([subviews count], 0U); return (AutocompleteMatrix*)[subviews objectAtIndex:0]; } return nil; } bool AutocompletePopupViewMac::IsOpen() const { return popup_ != nil; } void AutocompletePopupViewMac::CreatePopupIfNeeded() { if (!popup_) { popup_.reset([[NSWindow alloc] initWithContentRect:NSZeroRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:YES]); [popup_ setMovableByWindowBackground:NO]; // The window shape is determined by the content view (OmniboxPopupView). [popup_ setAlphaValue:1.0]; [popup_ setOpaque:NO]; [popup_ setBackgroundColor:[NSColor clearColor]]; [popup_ setHasShadow:YES]; [popup_ setLevel:NSNormalWindowLevel]; scoped_nsobject matrix( [[AutocompleteMatrix alloc] initWithPopupView:this]); scoped_nsobject contentView( [[OmniboxPopupView alloc] initWithFrame:NSZeroRect]); [contentView addSubview:matrix]; [popup_ setContentView:contentView]; } } void AutocompletePopupViewMac::PositionPopup(const CGFloat matrixHeight) { // Calculate the popup's position on the screen. It should abut the // field's visual border vertically, and be below the bounds // horizontally. // Start with the field's rect on the screen. NSRect popupFrame = NSInsetRect([field_ bounds], 0.0, kFieldVisualInset); popupFrame = [field_ convertRect:popupFrame toView:nil]; popupFrame.origin = [[field_ window] convertBaseToScreen:popupFrame.origin]; // Size to fit the matrix, and shift down by the size plus the top // window border. Would prefer -convertSize:fromView: to // -userSpaceScaleFactor for the scale conversion, but until the // window is on-screen that doesn't work right (bug?). popupFrame.size.height = matrixHeight * [popup_ userSpaceScaleFactor]; popupFrame.origin.y -= NSHeight(popupFrame) + kWindowBorderWidth; // Inset to account for the horizontal border drawn by the window. popupFrame = NSInsetRect(popupFrame, kWindowBorderWidth, 0.0); // Leave a gap between the popup and the field. popupFrame.origin.y -= kPopupFieldGap * [popup_ userSpaceScaleFactor]; // Do nothing if the popup is already animating to the given |frame|. if (NSEqualRects(popupFrame, targetPopupFrame_)) return; NSRect currentPopupFrame = [popup_ frame]; targetPopupFrame_ = popupFrame; // Animate the frame change if the only change is that the height got smaller. // Otherwise, resize immediately. bool animate = (NSHeight(popupFrame) < NSHeight(currentPopupFrame) && NSWidth(popupFrame) == NSWidth(currentPopupFrame)); NSDictionary* savedAnimations = nil; if (!animate) { // In an ideal world, running a zero-length animation would cancel any // running animations and set the new frame value immediately. In practice, // zero-length animations are ignored entirely. Work around this AppKit bug // by explicitly setting an NSNull animation for the "frame" key and then // running the animation with a non-zero(!!) duration. This somehow // convinces AppKit to do the right thing. Save off the current animations // dictionary so it can be restored later. savedAnimations = [[popup_ animations] copy]; [popup_ setAnimations: [NSDictionary dictionaryWithObjectsAndKeys:[NSNull null], @"frame", nil]]; } [NSAnimationContext beginGrouping]; // Don't use the GTM additon for the "Steve" slowdown because this can happen // async from user actions and the effects could be a surprise. [[NSAnimationContext currentContext] setDuration:kShrinkAnimationDuration]; [[popup_ animator] setFrame:popupFrame display:YES]; [NSAnimationContext endGrouping]; if (!animate) { // Restore the original animations dictionary. This does not reinstate any // previously running animations. [popup_ setAnimations:savedAnimations]; } if (![popup_ isVisible]) [[field_ window] addChildWindow:popup_ ordered:NSWindowAbove]; } NSImage* AutocompletePopupViewMac::ImageForMatch( const AutocompleteMatch& match) { const SkBitmap* bitmap = model_->GetIconIfExtensionMatch(match); if (bitmap) return gfx::SkBitmapToNSImage(*bitmap); const int resource_id = match.starred ? IDR_OMNIBOX_STAR : AutocompleteMatch::TypeToIcon(match.type); return AutocompleteEditViewMac::ImageForResource(resource_id); } void AutocompletePopupViewMac::UpdatePopupAppearance() { DCHECK([NSThread isMainThread]); const AutocompleteResult& result = model_->result(); if (result.empty()) { [[popup_ parentWindow] removeChildWindow:popup_]; [popup_ orderOut:nil]; // Break references to |this| because the popup may not be // deallocated immediately. AutocompleteMatrix* matrix = GetAutocompleteMatrix(); DCHECK(matrix == nil || [matrix isKindOfClass:[AutocompleteMatrix class]]); [matrix setPopupView:NULL]; popup_.reset(nil); targetPopupFrame_ = NSZeroRect; return; } CreatePopupIfNeeded(); // The popup's font is a slightly smaller version of the field's. NSFont* fieldFont = AutocompleteEditViewMac::GetFieldFont(); const CGFloat resultFontSize = [fieldFont pointSize] + kEditFontAdjust; gfx::Font resultFont(base::SysNSStringToUTF16([fieldFont fontName]), static_cast(resultFontSize)); AutocompleteMatrix* matrix = GetAutocompleteMatrix(); // Calculate the width of the matrix based on backing out the // popup's border from the width of the field. Would prefer to use // [matrix convertSize:fromView:] for converting from screen size, // but that doesn't work until the popup is on-screen (bug?). const NSRect fieldRectBase = [field_ convertRect:[field_ bounds] toView:nil]; const CGFloat popupWidth = NSWidth(fieldRectBase) - 2 * kWindowBorderWidth; DCHECK_GT(popupWidth, 0.0); const CGFloat matrixWidth = popupWidth / [popup_ userSpaceScaleFactor]; // Load the results into the popup's matrix. const size_t rows = model_->result().size(); DCHECK_GT(rows, 0U); [matrix renewRows:rows columns:1]; for (size_t ii = 0; ii < rows; ++ii) { AutocompleteButtonCell* cell = [matrix cellAtRow:ii column:0]; const AutocompleteMatch& match = model_->result().match_at(ii); [cell setImage:ImageForMatch(match)]; [cell setAttributedTitle:MatchText(match, resultFont, matrixWidth)]; } // Set the cell size to fit a line of text in the cell's font. All // cells should use the same font and each should layout in one // line, so they should all be about the same height. const NSSize cellSize = [[matrix cellAtRow:0 column:0] cellSize]; DCHECK_GT(cellSize.height, 0.0); const CGFloat cellHeight = cellSize.height + kCellHeightAdjust; [matrix setCellSize:NSMakeSize(matrixWidth, cellHeight)]; // Add in the instant view if needed and not already present. CGFloat instantHeight = 0; if (ShouldShowInstantOptIn()) { if (!opt_in_controller_.get()) { opt_in_controller_.reset( [[InstantOptInController alloc] initWithDelegate:this]); } [[popup_ contentView] addSubview:[opt_in_controller_ view]]; [GetAutocompleteMatrix() setBottomCornersRounded:NO]; instantHeight = NSHeight([[opt_in_controller_ view] frame]); } else { [[opt_in_controller_ view] removeFromSuperview]; opt_in_controller_.reset(nil); [GetAutocompleteMatrix() setBottomCornersRounded:YES]; } // Update the selection before placing (and displaying) the window. PaintUpdatesNow(); // Calculate the matrix size manually rather than using -sizeToCells // because actually resizing the matrix messed up the popup size // animation. DCHECK_EQ([matrix intercellSpacing].height, 0.0); CGFloat matrixHeight = rows * cellHeight; PositionPopup(matrixHeight + instantHeight); } gfx::Rect AutocompletePopupViewMac::GetTargetBounds() { // Flip the coordinate system before returning. NSScreen* screen = [[NSScreen screens] objectAtIndex:0]; NSRect monitorFrame = [screen frame]; gfx::Rect bounds(NSRectToCGRect(targetPopupFrame_)); bounds.set_y(monitorFrame.size.height - bounds.y() - bounds.height()); return bounds; } void AutocompletePopupViewMac::SetSelectedLine(size_t line) { model_->SetSelectedLine(line, false, false); } // This is only called by model in SetSelectedLine() after updating // everything. Popup should already be visible. void AutocompletePopupViewMac::PaintUpdatesNow() { AutocompleteMatrix* matrix = GetAutocompleteMatrix(); [matrix selectCellAtRow:model_->selected_line() column:0]; } void AutocompletePopupViewMac::OpenURLForRow(int row, bool force_background) { DCHECK_GE(row, 0); WindowOpenDisposition disposition = NEW_BACKGROUND_TAB; if (!force_background) { disposition = event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]); } // OpenURL() may close the popup, which will clear the result set // and, by extension, |match| and its contents. So copy the // relevant strings out to make sure they stay alive until the call // completes. const AutocompleteMatch& match = model_->result().match_at(row); const GURL url(match.destination_url); string16 keyword; const bool is_keyword_hint = model_->GetKeywordForMatch(match, &keyword); edit_view_->OpenURL(url, disposition, match.transition, GURL(), row, is_keyword_hint ? string16() : keyword); } void AutocompletePopupViewMac::UserPressedOptIn(bool opt_in) { PromoCounter* counter = model_->profile()->GetInstantPromoCounter(); DCHECK(counter); counter->Hide(); if (opt_in) { browser::ShowInstantConfirmDialogIfNecessary([field_ window], model_->profile()); } // This call will remove and delete |opt_in_controller_|. UpdatePopupAppearance(); } bool AutocompletePopupViewMac::ShouldShowInstantOptIn() { PromoCounter* counter = model_->profile()->GetInstantPromoCounter(); return (counter && counter->ShouldShow(base::Time::Now())); } @implementation AutocompleteButtonCell - init { self = [super init]; if (self) { [self setImagePosition:NSImageLeft]; [self setBordered:NO]; [self setButtonType:NSRadioButton]; // Without this highlighting messes up white areas of images. [self setHighlightsBy:NSNoCellMask]; } return self; } - (NSColor*)backgroundColor { if ([self state] == NSOnState) { return SelectedBackgroundColor(); } else if ([self isHighlighted]) { return HoveredBackgroundColor(); } return BackgroundColor(); } // The default NSButtonCell drawing leaves the image flush left and // the title next to the image. This spaces things out to line up // with the star button and autocomplete field. - (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { [[self backgroundColor] set]; NSRectFill(cellFrame); // Put the image centered vertically but in a fixed column. NSImage* image = [self image]; if (image) { NSRect imageRect = cellFrame; imageRect.size = [image size]; imageRect.origin.y += std::floor((NSHeight(cellFrame) - NSHeight(imageRect)) / 2.0); imageRect.origin.x += kImageXOffset; [image drawInRect:imageRect fromRect:NSZeroRect // Entire image operation:NSCompositeSourceOver fraction:1.0 neverFlipped:YES]; } // Adjust the title position to be lined up under the field's text. NSAttributedString* title = [self attributedTitle]; if (title && [title length]) { NSRect titleRect = cellFrame; titleRect.size.width -= kTextXOffset; titleRect.origin.x += kTextXOffset; [self drawTitle:title withFrame:titleRect inView:controlView]; } } @end @implementation AutocompleteMatrix @synthesize bottomCornersRounded = bottomCornersRounded_; // Remove all tracking areas and initialize the one we want. Removing // all might be overkill, but it's unclear why there would be others // for the popup window. - (void)resetTrackingArea { for (NSTrackingArea* trackingArea in [self trackingAreas]) { [self removeTrackingArea:trackingArea]; } // TODO(shess): Consider overriding -acceptsFirstMouse: and changing // NSTrackingActiveInActiveApp to NSTrackingActiveAlways. NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited; options |= NSTrackingMouseMoved; options |= NSTrackingActiveInActiveApp; options |= NSTrackingInVisibleRect; scoped_nsobject trackingArea( [[NSTrackingArea alloc] initWithRect:[self frame] options:options owner:self userInfo:nil]); [self addTrackingArea:trackingArea]; } - (void)updateTrackingAreas { [self resetTrackingArea]; [super updateTrackingAreas]; } - initWithPopupView:(AutocompletePopupViewMac*)popupView { self = [super initWithFrame:NSZeroRect]; if (self) { popupView_ = popupView; [self setCellClass:[AutocompleteButtonCell class]]; // Cells pack with no spacing. [self setIntercellSpacing:NSMakeSize(0.0, 0.0)]; [self setDrawsBackground:YES]; [self setBackgroundColor:BackgroundColor()]; [self renewRows:0 columns:1]; [self setAllowsEmptySelection:YES]; [self setMode:NSRadioModeMatrix]; [self deselectAllCells]; [self resetTrackingArea]; } return self; } - (void)setPopupView:(AutocompletePopupViewMac*)popupView { popupView_ = popupView; } - (void)highlightRowAt:(NSInteger)rowIndex { // highlightCell will be nil if rowIndex is out of range, so no cell // will be highlighted. NSCell* highlightCell = [self cellAtRow:rowIndex column:0]; for (NSCell* cell in [self cells]) { [cell setHighlighted:(cell == highlightCell)]; } } - (void)highlightRowUnder:(NSEvent*)theEvent { NSPoint point = [self convertPoint:[theEvent locationInWindow] fromView:nil]; NSInteger row, column; if ([self getRow:&row column:&column forPoint:point]) { [self highlightRowAt:row]; } else { [self highlightRowAt:-1]; } } // Callbacks from NSTrackingArea. - (void)mouseEntered:(NSEvent*)theEvent { [self highlightRowUnder:theEvent]; } - (void)mouseMoved:(NSEvent*)theEvent { [self highlightRowUnder:theEvent]; } - (void)mouseExited:(NSEvent*)theEvent { [self highlightRowAt:-1]; } // The tracking area events aren't forwarded during a drag, so handle // highlighting manually for middle-click and middle-drag. - (void)otherMouseDown:(NSEvent*)theEvent { if ([theEvent buttonNumber] == kMiddleButtonNumber) { [self highlightRowUnder:theEvent]; } [super otherMouseDown:theEvent]; } - (void)otherMouseDragged:(NSEvent*)theEvent { if ([theEvent buttonNumber] == kMiddleButtonNumber) { [self highlightRowUnder:theEvent]; } [super otherMouseDragged:theEvent]; } - (void)otherMouseUp:(NSEvent*)theEvent { // Only intercept middle button. if ([theEvent buttonNumber] != kMiddleButtonNumber) { [super otherMouseUp:theEvent]; return; } // -otherMouseDragged: should always have been called at this // location, but make sure the user is getting the right feedback. [self highlightRowUnder:theEvent]; const NSInteger highlightedRow = [self highlightedRow]; if (highlightedRow != -1) { DCHECK(popupView_); popupView_->OpenURLForRow(highlightedRow, true); } } // Select cell under |theEvent|, returning YES if a selection is made. - (BOOL)selectCellForEvent:(NSEvent*)theEvent { NSPoint point = [self convertPoint:[theEvent locationInWindow] fromView:nil]; NSInteger row, column; if ([self getRow:&row column:&column forPoint:point]) { DCHECK_EQ(column, 0); DCHECK(popupView_); popupView_->SetSelectedLine(row); return YES; } return NO; } // Track the mouse until released, keeping the cell under the mouse // selected. If the mouse wanders off-view, revert to the // originally-selected cell. If the mouse is released over a cell, // call |popupView_| to open the row's URL. - (void)mouseDown:(NSEvent*)theEvent { NSCell* selectedCell = [self selectedCell]; // Clear any existing highlight. [self highlightRowAt:-1]; do { if (![self selectCellForEvent:theEvent]) { [self selectCell:selectedCell]; } const NSUInteger mask = NSLeftMouseUpMask | NSLeftMouseDraggedMask; theEvent = [[self window] nextEventMatchingMask:mask]; } while ([theEvent type] == NSLeftMouseDragged); // Do not message |popupView_| if released outside view. if (![self selectCellForEvent:theEvent]) { [self selectCell:selectedCell]; } else { const NSInteger selectedRow = [self selectedRow]; DCHECK_GE(selectedRow, 0); DCHECK(popupView_); popupView_->OpenURLForRow(selectedRow, false); } } - (NSInteger)highlightedRow { NSArray* cells = [self cells]; const NSUInteger count = [cells count]; for(NSUInteger i = 0; i < count; ++i) { if ([[cells objectAtIndex:i] isHighlighted]) { return i; } } return -1; } - (BOOL)isOpaque { return NO; } // This handles drawing the decorations of the rounded popup window, // calling on NSMatrix to draw the actual contents. - (void)drawRect:(NSRect)rect { CGFloat bottomCornerRadius = (bottomCornersRounded_ ? kPopupRoundingRadius : 0); // "Top" really means "bottom" here, since the view is flipped. NSBezierPath* path = [NSBezierPath gtm_bezierPathWithRoundRect:[self bounds] topLeftCornerRadius:bottomCornerRadius topRightCornerRadius:bottomCornerRadius bottomLeftCornerRadius:kPopupRoundingRadius bottomRightCornerRadius:kPopupRoundingRadius]; // Draw the matrix clipped to our border. [NSGraphicsContext saveGraphicsState]; [path addClip]; [super drawRect:rect]; [NSGraphicsContext restoreGraphicsState]; } @end