1 // Copyright (c) 2012 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.
5 #include "chrome/browser/ui/cocoa/omnibox/omnibox_popup_view_mac.h"
9 #include "base/stl_util.h"
10 #include "base/strings/sys_string_conversions.h"
11 #include "chrome/browser/autocomplete/autocomplete_match.h"
12 #include "chrome/browser/search/search.h"
13 #include "chrome/browser/ui/cocoa/browser_window_controller.h"
14 #import "chrome/browser/ui/cocoa/omnibox/omnibox_popup_cell.h"
15 #import "chrome/browser/ui/cocoa/omnibox/omnibox_popup_separator_view.h"
16 #include "chrome/browser/ui/cocoa/omnibox/omnibox_view_mac.h"
17 #include "chrome/browser/ui/omnibox/omnibox_edit_model.h"
18 #include "chrome/browser/ui/omnibox/omnibox_popup_model.h"
19 #include "chrome/common/autocomplete_match_type.h"
20 #include "grit/theme_resources.h"
21 #include "skia/ext/skia_utils_mac.h"
22 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
23 #import "ui/base/cocoa/cocoa_event_utils.h"
24 #import "ui/base/cocoa/flipped_view.h"
25 #include "ui/base/cocoa/window_size_constants.h"
26 #include "ui/base/resource/resource_bundle.h"
27 #include "ui/gfx/rect.h"
28 #include "ui/gfx/text_elider.h"
29 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
33 // How much to adjust the cell sizing up from the default determined
35 const CGFloat kCellHeightAdjust = 6.0;
37 // Padding between matrix and the top and bottom of the popup window.
38 const CGFloat kPopupPaddingVertical = 5.0;
40 // How far to offset the text column from the left.
41 const CGFloat kTextXOffset = 28.0;
43 // Animation duration when animating the popup window smaller.
44 const NSTimeInterval kShrinkAnimationDuration = 0.1;
46 // Maximum fraction of the popup width that can be used to display match
48 const CGFloat kMaxContentsFraction = 0.7;
50 // Background colors for different states of the popup elements.
51 NSColor* BackgroundColor() {
52 return [NSColor controlBackgroundColor];
55 NSColor* ContentTextColor() {
56 return [NSColor blackColor];
58 NSColor* DimContentTextColor() {
59 return [NSColor darkGrayColor];
61 NSColor* URLTextColor() {
62 return [NSColor colorWithCalibratedRed:0.0 green:0.55 blue:0.0 alpha:1.0];
67 OmniboxPopupViewMac::OmniboxPopupViewMac(OmniboxView* omnibox_view,
68 OmniboxEditModel* edit_model,
70 : omnibox_view_(omnibox_view),
71 model_(new OmniboxPopupModel(this, edit_model)),
74 target_popup_frame_(NSZeroRect) {
79 OmniboxPopupViewMac::~OmniboxPopupViewMac() {
80 // Destroy the popup model before this object is destroyed, because
81 // it can call back to us in the destructor.
84 // Break references to |this| because the popup may not be
85 // deallocated immediately.
86 [matrix_ setDelegate:NULL];
89 bool OmniboxPopupViewMac::IsOpen() const {
93 void OmniboxPopupViewMac::UpdatePopupAppearance() {
94 DCHECK([NSThread isMainThread]);
95 const AutocompleteResult& result = GetResult();
96 const size_t start_match = result.ShouldHideTopMatch() ? 1 : 0;
97 const size_t rows = result.size() - start_match;
99 [[popup_ parentWindow] removeChildWindow:popup_];
100 [popup_ orderOut:nil];
102 // Break references to |this| because the popup may not be
103 // deallocated immediately.
104 [matrix_ setDelegate:nil];
109 target_popup_frame_ = NSZeroRect;
114 CreatePopupIfNeeded();
116 gfx::Font result_font(OmniboxViewMac::GetFieldFont());
118 // Calculate the width of the matrix based on backing out the popup's border
119 // from the width of the field.
120 const CGFloat matrix_width = NSWidth([field_ bounds]);
121 DCHECK_GT(matrix_width, 0.0);
123 // Load the results into the popup's matrix.
125 [matrix_ renewRows:rows columns:1];
126 for (size_t ii = 0; ii < rows; ++ii) {
127 OmniboxPopupCell* cell = [matrix_ cellAtRow:ii column:0];
128 const AutocompleteMatch& match = GetResult().match_at(ii + start_match);
129 [cell setImage:ImageForMatch(match)];
130 [cell setAttributedTitle:MatchText(match, result_font, matrix_width)];
133 // Set the cell size to fit a line of text in the cell's font. All
134 // cells should use the same font and each should layout in one
135 // line, so they should all be about the same height.
136 const NSSize cell_size = [[matrix_ cellAtRow:0 column:0] cellSize];
137 DCHECK_GT(cell_size.height, 0.0);
138 const CGFloat cell_height = cell_size.height + kCellHeightAdjust;
139 [matrix_ setCellSize:NSMakeSize(matrix_width, cell_height)];
141 // Update the selection before placing (and displaying) the window.
144 // Calculate the matrix size manually rather than using -sizeToCells
145 // because actually resizing the matrix messed up the popup size
147 DCHECK_EQ([matrix_ intercellSpacing].height, 0.0);
148 PositionPopup(rows * cell_height);
151 gfx::Rect OmniboxPopupViewMac::GetTargetBounds() {
152 // Flip the coordinate system before returning.
153 NSScreen* screen = [[NSScreen screens] objectAtIndex:0];
154 NSRect monitor_frame = [screen frame];
155 gfx::Rect bounds(NSRectToCGRect(target_popup_frame_));
156 bounds.set_y(monitor_frame.size.height - bounds.y() - bounds.height());
160 // This is only called by model in SetSelectedLine() after updating
161 // everything. Popup should already be visible.
162 void OmniboxPopupViewMac::PaintUpdatesNow() {
163 size_t start_match = model_->result().ShouldHideTopMatch() ? 1 : 0;
164 if (start_match > model_->selected_line()) {
165 [matrix_ deselectAllCells];
167 [matrix_ selectCellAtRow:model_->selected_line() - start_match column:0];
172 void OmniboxPopupViewMac::OnMatrixRowSelected(OmniboxPopupMatrix* matrix,
174 size_t start_match = model_->result().ShouldHideTopMatch() ? 1 : 0;
175 model_->SetSelectedLine(row + start_match, false, false);
178 void OmniboxPopupViewMac::OnMatrixRowClicked(OmniboxPopupMatrix* matrix,
181 ui::WindowOpenDispositionFromNSEvent([NSApp currentEvent]));
184 void OmniboxPopupViewMac::OnMatrixRowMiddleClicked(OmniboxPopupMatrix* matrix,
186 OpenURLForRow(row, NEW_BACKGROUND_TAB);
189 // Return the text to show for the match, based on the match's
190 // contents and description. Result will be in |font|, with the
191 // boldfaced version used for matches.
192 NSAttributedString* OmniboxPopupViewMac::MatchText(
193 const AutocompleteMatch& match,
196 NSMutableAttributedString *as =
197 DecorateMatchedString(match.contents,
198 match.contents_class,
200 DimContentTextColor(),
203 // If there is a description, append it, separated from the contents
204 // with an en dash, and decorated with a distinct color.
205 if (!match.description.empty()) {
206 // Make sure the current string fits w/in kMaxContentsFraction of
207 // the cell to make sure the description will be at least
208 // partially visible.
209 // TODO(shess): Consider revising our NSCell subclass to have two
210 // bits and just draw them right, rather than truncating here.
211 const float text_width = cell_width - kTextXOffset;
212 as = ElideString(as, match.contents, font,
213 text_width * kMaxContentsFraction);
215 NSDictionary* attributes = @{
216 NSFontAttributeName : font.GetNativeFont(),
217 NSForegroundColorAttributeName : ContentTextColor()
219 NSString* raw_en_dash = @" \u2013 ";
220 NSAttributedString* en_dash =
221 [[[NSAttributedString alloc] initWithString:raw_en_dash
222 attributes:attributes] autorelease];
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(),
233 [as appendAttributedString:en_dash];
234 [as appendAttributedString:description];
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])];
248 NSMutableAttributedString* OmniboxPopupViewMac::DecorateMatchedString(
249 const base::string16& match_string,
250 const AutocompleteMatch::ACMatchClassifications& classifications,
252 NSColor* dim_text_color,
254 // Cache for on-demand computation of the bold version of |font|.
255 NSFont* bold_font = nil;
257 // Start out with a string using the default style info.
258 NSString* s = base::SysUTF16ToNSString(match_string);
259 NSDictionary* attributes = @{
260 NSFontAttributeName : font.GetNativeFont(),
261 NSForegroundColorAttributeName : text_color
263 NSMutableAttributedString* as =
264 [[[NSMutableAttributedString alloc] initWithString:s
265 attributes:attributes]
268 // As a protective measure, bail if the length of the match string is not
269 // the same as the length of the converted NSString. http://crbug.com/121703
270 if ([s length] != match_string.size())
273 // Mark up the runs which differ from the default.
274 for (ACMatchClassifications::const_iterator i = classifications.begin();
275 i != classifications.end(); ++i) {
276 const BOOL is_last = (i+1) == classifications.end();
277 const NSInteger next_offset =
278 (is_last ? [s length] : static_cast<NSInteger>((i + 1)->offset));
279 const NSInteger location = static_cast<NSInteger>(i->offset);
280 const NSInteger length = next_offset - static_cast<NSInteger>(i->offset);
281 // Guard against bad, off-the-end classification ranges.
282 if (i->offset >= [s length] || length <= 0)
284 const NSRange range = NSMakeRange(location,
285 MIN(length, static_cast<NSInteger>([s length]) - location));
287 if (0 != (i->style & ACMatchClassification::URL)) {
288 [as addAttribute:NSForegroundColorAttributeName
293 if (0 != (i->style & ACMatchClassification::MATCH)) {
295 NSFontManager* font_manager = [NSFontManager sharedFontManager];
296 bold_font = [font_manager convertFont:font.GetNativeFont()
297 toHaveTrait:NSBoldFontMask];
299 [as addAttribute:NSFontAttributeName value:bold_font range:range];
302 if (0 != (i->style & ACMatchClassification::DIM)) {
303 [as addAttribute:NSForegroundColorAttributeName
312 NSMutableAttributedString* OmniboxPopupViewMac::ElideString(
313 NSMutableAttributedString* a_string,
314 const base::string16& original_string,
315 const gfx::Font& font,
317 // If it already fits, nothing to be done.
318 if ([a_string size].width <= width) {
322 // If ElideText() decides to do nothing, nothing to be done.
323 const base::string16 elided =
324 gfx::ElideText(original_string, gfx::FontList(font), width,
326 if (0 == elided.compare(original_string)) {
330 // If everything was elided away, clear the string.
331 if (elided.empty()) {
332 [a_string deleteCharactersInRange:NSMakeRange(0, [a_string length])];
336 // The ellipses should be the last character, and everything before
337 // that should match the original string.
338 const size_t i(elided.length() - 1);
339 DCHECK_NE(0, elided.compare(0, i, original_string));
341 // Replace the end of |aString| with the ellipses from |elided|.
342 NSString* s = base::SysUTF16ToNSString(elided.substr(i));
343 [a_string replaceCharactersInRange:NSMakeRange(i, [a_string length] - i)
349 const AutocompleteResult& OmniboxPopupViewMac::GetResult() const {
350 return model_->result();
353 void OmniboxPopupViewMac::CreatePopupIfNeeded() {
356 [[NSWindow alloc] initWithContentRect:ui::kWindowSizeDeterminedLater
357 styleMask:NSBorderlessWindowMask
358 backing:NSBackingStoreBuffered
360 [popup_ setBackgroundColor:[NSColor clearColor]];
361 [popup_ setOpaque:NO];
363 // Use a flipped view to pin the matrix top the top left. This is needed
364 // for animated resize.
365 base::scoped_nsobject<FlippedView> contentView(
366 [[FlippedView alloc] initWithFrame:NSZeroRect]);
367 [popup_ setContentView:contentView];
369 // View to draw a background beneath the matrix.
370 background_view_.reset([[NSBox alloc] initWithFrame:NSZeroRect]);
371 [background_view_ setBoxType:NSBoxCustom];
372 [background_view_ setBorderType:NSNoBorder];
373 [background_view_ setFillColor:BackgroundColor()];
374 [background_view_ setContentViewMargins:NSZeroSize];
375 [contentView addSubview:background_view_];
377 matrix_.reset([[OmniboxPopupMatrix alloc] initWithDelegate:this]);
378 [background_view_ addSubview:matrix_];
380 top_separator_view_.reset(
381 [[OmniboxPopupTopSeparatorView alloc] initWithFrame:NSZeroRect]);
382 [contentView addSubview:top_separator_view_];
384 bottom_separator_view_.reset(
385 [[OmniboxPopupBottomSeparatorView alloc] initWithFrame:NSZeroRect]);
386 [contentView addSubview:bottom_separator_view_];
388 // TODO(dtseng): Ignore until we provide NSAccessibility support.
389 [popup_ accessibilitySetOverrideValue:NSAccessibilityUnknownRole
390 forAttribute:NSAccessibilityRoleAttribute];
394 void OmniboxPopupViewMac::PositionPopup(const CGFloat matrixHeight) {
395 BrowserWindowController* controller =
396 [BrowserWindowController browserWindowControllerForView:field_];
397 NSRect anchor_rect_base = [controller omniboxPopupAnchorRect];
399 // Calculate the popup's position on the screen.
400 NSRect popup_frame = anchor_rect_base;
401 // Size to fit the matrix and shift down by the size.
402 popup_frame.size.height = matrixHeight + kPopupPaddingVertical * 2.0;
403 popup_frame.size.height += [OmniboxPopupTopSeparatorView preferredHeight];
404 popup_frame.size.height += [OmniboxPopupBottomSeparatorView preferredHeight];
405 popup_frame.origin.y -= NSHeight(popup_frame);
406 // Shift to screen coordinates.
408 [[controller window] convertBaseToScreen:popup_frame.origin];
410 // Do nothing if the popup is already animating to the given |frame|.
411 if (NSEqualRects(popup_frame, target_popup_frame_))
415 NSRect top_separator_frame = NSZeroRect;
416 top_separator_frame.size.width = NSWidth(popup_frame);
417 top_separator_frame.size.height =
418 [OmniboxPopupTopSeparatorView preferredHeight];
419 [top_separator_view_ setFrame:top_separator_frame];
422 NSRect bottom_separator_frame = NSZeroRect;
423 bottom_separator_frame.size.width = NSWidth(popup_frame);
424 bottom_separator_frame.size.height =
425 [OmniboxPopupBottomSeparatorView preferredHeight];
426 bottom_separator_frame.origin.y =
427 NSHeight(popup_frame) - NSHeight(bottom_separator_frame);
428 [bottom_separator_view_ setFrame:bottom_separator_frame];
431 NSRect background_rect = NSZeroRect;
432 background_rect.size.width = NSWidth(popup_frame);
433 background_rect.size.height = NSHeight(popup_frame) -
434 NSHeight(top_separator_frame) - NSHeight(bottom_separator_frame);
435 background_rect.origin.y = NSMaxY(top_separator_frame);
436 [background_view_ setFrame:background_rect];
439 NSPoint field_origin_base =
440 [field_ convertPoint:[field_ bounds].origin toView:nil];
441 NSRect matrix_frame = NSZeroRect;
442 matrix_frame.origin.x = field_origin_base.x - NSMinX(anchor_rect_base);
443 matrix_frame.origin.y = kPopupPaddingVertical;
444 matrix_frame.size.width = [matrix_ cellSize].width;
445 matrix_frame.size.height = matrixHeight;
446 [matrix_ setFrame:matrix_frame];
448 NSRect current_poup_frame = [popup_ frame];
449 target_popup_frame_ = popup_frame;
451 // Animate the frame change if the only change is that the height got smaller.
452 // Otherwise, resize immediately.
453 bool animate = (NSHeight(popup_frame) < NSHeight(current_poup_frame) &&
454 NSWidth(popup_frame) == NSWidth(current_poup_frame));
456 base::scoped_nsobject<NSDictionary> savedAnimations;
458 // In an ideal world, running a zero-length animation would cancel any
459 // running animations and set the new frame value immediately. In practice,
460 // zero-length animations are ignored entirely. Work around this AppKit bug
461 // by explicitly setting an NSNull animation for the "frame" key and then
462 // running the animation with a non-zero(!!) duration. This somehow
463 // convinces AppKit to do the right thing. Save off the current animations
464 // dictionary so it can be restored later.
465 savedAnimations.reset([[popup_ animations] copy]);
466 [popup_ setAnimations:@{@"frame" : [NSNull null]}];
469 [NSAnimationContext beginGrouping];
470 // Don't use the GTM additon for the "Steve" slowdown because this can happen
471 // async from user actions and the effects could be a surprise.
472 [[NSAnimationContext currentContext] setDuration:kShrinkAnimationDuration];
473 [[popup_ animator] setFrame:popup_frame display:YES];
474 [NSAnimationContext endGrouping];
477 // Restore the original animations dictionary. This does not reinstate any
478 // previously running animations.
479 [popup_ setAnimations:savedAnimations];
482 if (![popup_ isVisible])
483 [[field_ window] addChildWindow:popup_ ordered:NSWindowAbove];
486 NSImage* OmniboxPopupViewMac::ImageForMatch(const AutocompleteMatch& match) {
487 gfx::Image image = model_->GetIconIfExtensionMatch(match);
488 if (!image.IsEmpty())
489 return image.AsNSImage();
491 const int resource_id = match.starred ?
492 IDR_OMNIBOX_STAR : AutocompleteMatch::TypeToIcon(match.type);
493 return OmniboxViewMac::ImageForResource(resource_id);
496 void OmniboxPopupViewMac::OpenURLForRow(size_t row,
497 WindowOpenDisposition disposition) {
498 size_t start_match = model_->result().ShouldHideTopMatch() ? 1 : 0;
500 DCHECK_LT(row, GetResult().size());
501 omnibox_view_->OpenMatch(GetResult().match_at(row), disposition, GURL(), row);