Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / omnibox / omnibox_popup_view_mac.mm
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.
4
5 #include "chrome/browser/ui/cocoa/omnibox/omnibox_popup_view_mac.h"
6
7 #include <cmath>
8
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/scoped_ns_graphics_context_save_gstate_mac.h"
29 #include "ui/gfx/text_elider.h"
30
31 namespace {
32
33 // How much to adjust the cell sizing up from the default determined
34 // by the font.
35 const CGFloat kCellHeightAdjust = 6.0;
36
37 // Padding between matrix and the top and bottom of the popup window.
38 const CGFloat kPopupPaddingVertical = 5.0;
39
40 // How far to offset the text column from the left.
41 const CGFloat kTextXOffset = 28.0;
42
43 // Animation duration when animating the popup window smaller.
44 const NSTimeInterval kShrinkAnimationDuration = 0.1;
45
46 // Maximum fraction of the popup width that can be used to display match
47 // contents.
48 const CGFloat kMaxContentsFraction = 0.7;
49
50 // Background colors for different states of the popup elements.
51 NSColor* BackgroundColor() {
52   return [NSColor controlBackgroundColor];
53 }
54
55 NSColor* ContentTextColor() {
56   return [NSColor blackColor];
57 }
58 NSColor* DimContentTextColor() {
59   return [NSColor darkGrayColor];
60 }
61 NSColor* URLTextColor() {
62   return [NSColor colorWithCalibratedRed:0.0 green:0.55 blue:0.0 alpha:1.0];
63 }
64
65 }  // namespace
66
67 OmniboxPopupViewMac::OmniboxPopupViewMac(OmniboxView* omnibox_view,
68                                          OmniboxEditModel* edit_model,
69                                          NSTextField* field)
70     : omnibox_view_(omnibox_view),
71       model_(new OmniboxPopupModel(this, edit_model)),
72       field_(field),
73       popup_(nil),
74       target_popup_frame_(NSZeroRect) {
75   DCHECK(omnibox_view);
76   DCHECK(edit_model);
77 }
78
79 OmniboxPopupViewMac::~OmniboxPopupViewMac() {
80   // Destroy the popup model before this object is destroyed, because
81   // it can call back to us in the destructor.
82   model_.reset();
83
84   // Break references to |this| because the popup may not be
85   // deallocated immediately.
86   [matrix_ setDelegate:NULL];
87 }
88
89 bool OmniboxPopupViewMac::IsOpen() const {
90   return popup_ != nil;
91 }
92
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;
98   if (rows == 0) {
99     [[popup_ parentWindow] removeChildWindow:popup_];
100     [popup_ orderOut:nil];
101
102     // Break references to |this| because the popup may not be
103     // deallocated immediately.
104     [matrix_ setDelegate:nil];
105     matrix_.reset();
106
107     popup_.reset(nil);
108
109     target_popup_frame_ = NSZeroRect;
110
111     return;
112   }
113
114   CreatePopupIfNeeded();
115
116   gfx::Font result_font(OmniboxViewMac::GetFieldFont());
117
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);
122
123   // Load the results into the popup's matrix.
124   DCHECK_GT(rows, 0U);
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)];
131   }
132
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)];
140
141   // Update the selection before placing (and displaying) the window.
142   PaintUpdatesNow();
143
144   // Calculate the matrix size manually rather than using -sizeToCells
145   // because actually resizing the matrix messed up the popup size
146   // animation.
147   DCHECK_EQ([matrix_ intercellSpacing].height, 0.0);
148   PositionPopup(rows * cell_height);
149 }
150
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());
157   return bounds;
158 }
159
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];
166   } else {
167     [matrix_ selectCellAtRow:model_->selected_line() - start_match column:0];
168   }
169
170 }
171
172 void OmniboxPopupViewMac::OnMatrixRowSelected(OmniboxPopupMatrix* matrix,
173                                               size_t row) {
174   size_t start_match = model_->result().ShouldHideTopMatch() ? 1 : 0;
175   model_->SetSelectedLine(row + start_match, false, false);
176 }
177
178 void OmniboxPopupViewMac::OnMatrixRowClicked(OmniboxPopupMatrix* matrix,
179                                              size_t row) {
180   OpenURLForRow(row,
181                 ui::WindowOpenDispositionFromNSEvent([NSApp currentEvent]));
182 }
183
184 void OmniboxPopupViewMac::OnMatrixRowMiddleClicked(OmniboxPopupMatrix* matrix,
185                                                    size_t row) {
186   OpenURLForRow(row, NEW_BACKGROUND_TAB);
187 }
188
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,
194     gfx::Font& font,
195     float cell_width) {
196   NSMutableAttributedString *as =
197       DecorateMatchedString(match.contents,
198                             match.contents_class,
199                             ContentTextColor(),
200                             DimContentTextColor(),
201                             font);
202
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);
214
215     NSDictionary* attributes = @{
216         NSFontAttributeName :  font.GetNativeFont(),
217         NSForegroundColorAttributeName : ContentTextColor()
218     };
219     NSString* raw_en_dash = @" \u2013 ";
220     NSAttributedString* en_dash =
221         [[[NSAttributedString alloc] initWithString:raw_en_dash
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:en_dash];
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 // static
248 NSMutableAttributedString* OmniboxPopupViewMac::DecorateMatchedString(
249     const base::string16& match_string,
250     const AutocompleteMatch::ACMatchClassifications& classifications,
251     NSColor* text_color,
252     NSColor* dim_text_color,
253     gfx::Font& font) {
254   // Cache for on-demand computation of the bold version of |font|.
255   NSFont* bold_font = nil;
256
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
262   };
263   NSMutableAttributedString* as =
264       [[[NSMutableAttributedString alloc] initWithString:s
265                                               attributes:attributes]
266         autorelease];
267
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())
271     return as;
272
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)
283       break;
284     const NSRange range = NSMakeRange(location,
285         MIN(length, static_cast<NSInteger>([s length]) - location));
286
287     if (0 != (i->style & ACMatchClassification::URL)) {
288       [as addAttribute:NSForegroundColorAttributeName
289                  value:URLTextColor()
290                  range:range];
291     }
292
293     if (0 != (i->style & ACMatchClassification::MATCH)) {
294       if (!bold_font) {
295         NSFontManager* font_manager = [NSFontManager sharedFontManager];
296         bold_font = [font_manager convertFont:font.GetNativeFont()
297                                   toHaveTrait:NSBoldFontMask];
298       }
299       [as addAttribute:NSFontAttributeName value:bold_font range:range];
300     }
301
302     if (0 != (i->style & ACMatchClassification::DIM)) {
303       [as addAttribute:NSForegroundColorAttributeName
304                  value:dim_text_color
305                  range:range];
306     }
307   }
308
309   return as;
310 }
311
312 NSMutableAttributedString* OmniboxPopupViewMac::ElideString(
313     NSMutableAttributedString* a_string,
314     const base::string16& original_string,
315     const gfx::Font& font,
316     const float width) {
317   // If it already fits, nothing to be done.
318   if ([a_string size].width <= width) {
319     return a_string;
320   }
321
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,
325                      gfx::ELIDE_AT_END);
326   if (0 == elided.compare(original_string)) {
327     return a_string;
328   }
329
330   // If everything was elided away, clear the string.
331   if (elided.empty()) {
332     [a_string deleteCharactersInRange:NSMakeRange(0, [a_string length])];
333     return a_string;
334   }
335
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));
340
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)
344                           withString:s];
345
346   return a_string;
347 }
348
349 const AutocompleteResult& OmniboxPopupViewMac::GetResult() const {
350   return model_->result();
351 }
352
353 void OmniboxPopupViewMac::CreatePopupIfNeeded() {
354   if (!popup_) {
355     popup_.reset(
356         [[NSWindow alloc] initWithContentRect:ui::kWindowSizeDeterminedLater
357                                     styleMask:NSBorderlessWindowMask
358                                       backing:NSBackingStoreBuffered
359                                         defer:YES]);
360     [popup_ setBackgroundColor:[NSColor clearColor]];
361     [popup_ setOpaque:NO];
362
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];
368
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_];
376
377     matrix_.reset([[OmniboxPopupMatrix alloc] initWithDelegate:this]);
378     [background_view_ addSubview:matrix_];
379
380     top_separator_view_.reset(
381         [[OmniboxPopupTopSeparatorView alloc] initWithFrame:NSZeroRect]);
382     [contentView addSubview:top_separator_view_];
383
384     bottom_separator_view_.reset(
385         [[OmniboxPopupBottomSeparatorView alloc] initWithFrame:NSZeroRect]);
386     [contentView addSubview:bottom_separator_view_];
387
388     // TODO(dtseng): Ignore until we provide NSAccessibility support.
389     [popup_ accessibilitySetOverrideValue:NSAccessibilityUnknownRole
390                              forAttribute:NSAccessibilityRoleAttribute];
391   }
392 }
393
394 void OmniboxPopupViewMac::PositionPopup(const CGFloat matrixHeight) {
395   BrowserWindowController* controller =
396       [BrowserWindowController browserWindowControllerForView:field_];
397   NSRect anchor_rect_base = [controller omniboxPopupAnchorRect];
398
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.
407   popup_frame.origin =
408       [[controller window] convertBaseToScreen:popup_frame.origin];
409
410   // Do nothing if the popup is already animating to the given |frame|.
411   if (NSEqualRects(popup_frame, target_popup_frame_))
412     return;
413
414   // Top separator.
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];
420
421   // Bottom separator.
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];
429
430   // Background view.
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];
437
438   // Matrix.
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];
447
448   NSRect current_poup_frame = [popup_ frame];
449   target_popup_frame_ = popup_frame;
450
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));
455
456   base::scoped_nsobject<NSDictionary> savedAnimations;
457   if (!animate) {
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]}];
467   }
468
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];
475
476   if (!animate) {
477     // Restore the original animations dictionary.  This does not reinstate any
478     // previously running animations.
479     [popup_ setAnimations:savedAnimations];
480   }
481
482   if (![popup_ isVisible])
483     [[field_ window] addChildWindow:popup_ ordered:NSWindowAbove];
484 }
485
486 NSImage* OmniboxPopupViewMac::ImageForMatch(const AutocompleteMatch& match) {
487   gfx::Image image = model_->GetIconIfExtensionMatch(match);
488   if (!image.IsEmpty())
489     return image.AsNSImage();
490
491   const int resource_id = match.starred ?
492       IDR_OMNIBOX_STAR : AutocompleteMatch::TypeToIcon(match.type);
493   return OmniboxViewMac::ImageForResource(resource_id);
494 }
495
496 void OmniboxPopupViewMac::OpenURLForRow(size_t row,
497                                         WindowOpenDisposition disposition) {
498   size_t start_match = model_->result().ShouldHideTopMatch() ? 1 : 0;
499   row += start_match;
500   DCHECK_LT(row, GetResult().size());
501   omnibox_view_->OpenMatch(GetResult().match_at(row), disposition, GURL(),
502                            base::string16(), row);
503 }