// Copyright (c) 2009 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 "chrome/browser/autocomplete/autocomplete_popup_view_mac.h" #include "base/sys_string_conversions.h" #include "chrome/browser/autocomplete/autocomplete_edit.h" #include "chrome/browser/autocomplete/autocomplete_edit_view_mac.h" #include "chrome/browser/autocomplete/autocomplete_popup_model.h" namespace { // Background colors for different states of the popup elements. NSColor* BackgroundColor() { return [NSColor controlBackgroundColor]; } NSColor* SelectedBackgroundColor() { return [NSColor selectedControlColor]; } NSColor* HoveredBackgroundColor() { return [NSColor controlColor]; } // TODO(shess): These are totally unprincipled. I experimented with // +controlTextColor and the like, but found myself wondering whether // that was really appropriate. Circle back after consulting with // someone more knowledgeable about the ins and outs of this. static const NSColor* ContentTextColor() { return [NSColor blackColor]; } static const NSColor* URLTextColor() { return [NSColor colorWithCalibratedRed:0.0 green:0.55 blue:0.0 alpha:1.0]; } static const NSColor* DescriptionTextColor() { return [NSColor darkGrayColor]; } // TODO(shess): As with colors, there are a dozen ways to phrase this, // all of them probably wrong. Circle back with a ui-oriented // resource later. static const NSFont* NormalFont() { return [NSFont userFontOfSize:12]; } static const NSFont* BoldFont() { return [NSFont boldSystemFontOfSize:12]; } // Return the appropriate icon for the given match. Derived from the // gtk code. NSImage* MatchIcon(const AutocompleteMatch& match) { if (match.starred) { return [NSImage imageNamed:@"o2_star.png"]; } switch (match.type) { case AutocompleteMatch::URL_WHAT_YOU_TYPED: case AutocompleteMatch::NAVSUGGEST: { return [NSImage imageNamed:@"o2_globe.png"]; } case AutocompleteMatch::HISTORY_URL: case AutocompleteMatch::HISTORY_TITLE: case AutocompleteMatch::HISTORY_BODY: case AutocompleteMatch::HISTORY_KEYWORD: { return [NSImage imageNamed:@"o2_history.png"]; } case AutocompleteMatch::SEARCH_WHAT_YOU_TYPED: case AutocompleteMatch::SEARCH_HISTORY: case AutocompleteMatch::SEARCH_SUGGEST: case AutocompleteMatch::SEARCH_OTHER_ENGINE: { return [NSImage imageNamed:@"o2_search.png"]; } case AutocompleteMatch::OPEN_HISTORY_PAGE: { return [NSImage imageNamed:@"o2_more.png"]; } default: NOTREACHED(); break; } return nil; } } // 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 std::wstring &matchString, const AutocompleteMatch::ACMatchClassifications &classifications, NSColor* textColor) { // Start out with a string using the default style info. NSString* s = base::SysWideToNSString(matchString); NSDictionary* attributes = [NSDictionary dictionaryWithObjectsAndKeys: NormalFont(), 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)) { [as addAttribute:NSFontAttributeName value:BoldFont() range:range]; } } return as; } // Return the text to show for the match, based on the match's // contents and description. NSMutableAttributedString* AutocompletePopupViewMac::MatchText( const AutocompleteMatch& match) { NSMutableAttributedString *as = DecorateMatchedString(match.contents, match.contents_class, ContentTextColor()); // If there is a description, append it, separated from the contents // with an em dash, and decorated with a distinct color. if (!match.description.empty()) { NSDictionary* attributes = [NSDictionary dictionaryWithObjectsAndKeys: NormalFont(), NSFontAttributeName, ContentTextColor(), NSForegroundColorAttributeName, nil]; NSString* rawEmDash = [NSString stringWithFormat:@" %C ", 0x2014]; NSAttributedString* emDash = [[[NSAttributedString alloc] initWithString:rawEmDash attributes:attributes] autorelease]; NSAttributedString* description = DecorateMatchedString(match.description, match.description_class, DescriptionTextColor()); [as appendAttributedString:emDash]; [as appendAttributedString:description]; } NSMutableParagraphStyle* style = [[[NSMutableParagraphStyle alloc] init] autorelease]; [style setLineBreakMode:NSLineBreakByTruncatingTail]; [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 { } @end // Thin Obj-C bridge class between the target of the popup window's // AutocompleteMatrix and the AutocompletePopupView implementation. // TODO(shess): Now that I'm using AutocompleteMatrix, I could instead // subvert the target/action stuff and have it message popup_view_ // directly. @interface AutocompleteMatrixTarget : NSObject { @private AutocompletePopupViewMac* popup_view_; // weak, owns us. } - initWithPopupView:(AutocompletePopupViewMac*)view; // Tell popup model via popup_view_ about the selected row. - (void)select:sender; @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), matrix_target_([[AutocompleteMatrixTarget alloc] initWithPopupView:this]), popup_(nil) { DCHECK(edit_view); DCHECK(edit_model); DCHECK(profile); edit_model->set_popup_model(model_.get()); } AutocompletePopupViewMac::~AutocompletePopupViewMac() { // TODO(shess): Having to be aware of destructor ordering in this // way seems brittle. There must be a better way. // Destroy the popup model before this object is destroyed, because // it can call back to us in the destructor. model_.reset(); // Break references to matrix_target_ before it is released. NSMatrix* matrix = [popup_ contentView]; [matrix setTarget:nil]; } bool AutocompletePopupViewMac::IsOpen() const { return [popup_ isVisible] ? true : false; } void AutocompletePopupViewMac::CreatePopupIfNeeded() { if (!popup_) { popup_.reset([[NSWindow alloc] initWithContentRect:NSZeroRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:YES]); [popup_ setMovableByWindowBackground:NO]; [popup_ setOpaque:YES]; [popup_ setHasShadow:YES]; [popup_ setLevel:NSNormalWindowLevel]; AutocompleteMatrix* matrix = [[[AutocompleteMatrix alloc] initWithFrame:NSZeroRect] autorelease]; [matrix setTarget:matrix_target_]; [matrix setAction:@selector(select:)]; [popup_ setContentView:matrix]; } } void AutocompletePopupViewMac::UpdatePopupAppearance() { const AutocompleteResult& result = model_->result(); if (result.empty()) { [[popup_ parentWindow] removeChildWindow:popup_]; [popup_ orderOut:nil]; // Break references to matrix_target_ before releasing popup_. NSMatrix* matrix = [popup_ contentView]; [matrix setTarget:nil]; popup_.reset(nil); return; } CreatePopupIfNeeded(); // Layout the popup and size it to land underneath the field. // TODO(shess) Consider refactoring to remove this depenency, // because the popup doesn't need any of the field-like aspects of // field_. The edit view could expose helper methods for attaching // the window to the field. NSRect r = [field_ bounds]; r = [field_ convertRectToBase:r]; r.origin = [[field_ window] convertBaseToScreen:r.origin]; AutocompleteMatrix* matrix = [popup_ contentView]; [matrix setCellSize:NSMakeSize(r.size.width, [matrix cellSize].height)]; [matrix setFrameSize:NSMakeSize(r.size.width, [matrix frame].size.height)]; size_t rows = model_->result().size(); [matrix renewRows:rows columns:1]; [matrix sizeToCells]; r.size.height = [matrix frame].size.height; r.origin.y -= r.size.height + 2; 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:MatchIcon(match)]; [cell setAttributedTitle:MatchText(match)]; } // Update the selection. PaintUpdatesNow(); [popup_ setFrame:r display:YES]; if (!IsOpen()) { [[field_ window] addChildWindow:popup_ ordered:NSWindowAbove]; } } // This is only called by model in SetSelectedLine() after updating // everything. Popup should already be visible. void AutocompletePopupViewMac::PaintUpdatesNow() { AutocompleteMatrix* matrix = [popup_ contentView]; [matrix selectCellAtRow:model_->selected_line() column:0]; } AutocompletePopupModel* AutocompletePopupViewMac::GetModel() { return model_.get(); } void AutocompletePopupViewMac::AcceptInput() { AutocompleteMatrix* matrix = [popup_ contentView]; model_->SetSelectedLine([matrix selectedRow], false); edit_view_->AcceptInput(CURRENT_TAB, false); } @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(); } @end @implementation AutocompleteMatrix // 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; NSTrackingArea* trackingArea = [[[NSTrackingArea alloc] initWithRect:[self frame] options:options owner:self userInfo:nil] autorelease]; [self addTrackingArea:trackingArea]; } - (void)updateTrackingAreas { [self resetTrackingArea]; [super updateTrackingAreas]; } - initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { [self setCellClass:[AutocompleteButtonCell class]]; [self setIntercellSpacing:NSMakeSize(1.0, 1.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)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]; } @end @implementation AutocompleteMatrixTarget - initWithPopupView:(AutocompletePopupViewMac*)view { self = [super init]; if (self) { popup_view_ = view; } return self; } - (void)select:sender { DCHECK(popup_view_); popup_view_->AcceptInput(); } @end