blob: da2b45113ef2964f0f46f5a496a6a1ca0a617b63 [file] [log] [blame]
// 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 <cmath>
#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<NSInteger>(i->offset);
const NSInteger length = static_cast<NSInteger>(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<AutocompleteMatrix> matrix(
[[AutocompleteMatrix alloc] initWithPopupView:this]);
scoped_nsobject<OmniboxPopupView> 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<int>(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<NSTrackingArea> 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