[Metrics] Make MetricsStateManager take a callback param to check if UMA is enabled.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / omnibox / omnibox_popup_cell.mm
blob1c83402649c87130e7e4c4fc3c0341950f6f6e6e
1 // Copyright 2013 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 #import "chrome/browser/ui/cocoa/omnibox/omnibox_popup_cell.h"
7 #include <algorithm>
8 #include <cmath>
10 #include "base/i18n/rtl.h"
11 #include "base/mac/scoped_nsobject.h"
12 #include "base/strings/string_number_conversions.h"
13 #include "base/strings/string_util.h"
14 #include "base/strings/sys_string_conversions.h"
15 #include "base/strings/utf_string_conversions.h"
16 #include "chrome/browser/ui/cocoa/omnibox/omnibox_view_mac.h"
17 #include "chrome/browser/ui/cocoa/omnibox/omnibox_popup_view_mac.h"
18 #include "chrome/browser/ui/omnibox/omnibox_popup_model.h"
19 #include "grit/generated_resources.h"
20 #include "ui/base/l10n/l10n_util.h"
21 #include "ui/gfx/font.h"
23 namespace {
25 // How far to offset image column from the left.
26 const CGFloat kImageXOffset = 5.0;
28 // How far to offset the text column from the left.
29 const CGFloat kTextStartOffset = 28.0;
31 // Rounding radius of selection and hover background on popup items.
32 const CGFloat kCellRoundingRadius = 2.0;
34 // Flips the given |rect| in context of the given |frame|.
35 NSRect FlipIfRTL(NSRect rect, NSRect frame) {
36   DCHECK_LE(NSMinX(frame), NSMinX(rect));
37   DCHECK_GE(NSMaxX(frame), NSMaxX(rect));
38   if (base::i18n::IsRTL()) {
39     NSRect result = rect;
40     result.origin.x = NSMinX(frame) + (NSMaxX(frame) - NSMaxX(rect));
41     return result;
42   }
43   return rect;
46 // Shifts the left edge of the given |rect| by |dX|
47 NSRect ShiftRect(NSRect rect, CGFloat dX) {
48   DCHECK_LE(dX, NSWidth(rect));
49   NSRect result = rect;
50   result.origin.x += dX;
51   result.size.width -= dX;
52   return result;
55 NSColor* SelectedBackgroundColor() {
56   return [NSColor selectedControlColor];
58 NSColor* HoveredBackgroundColor() {
59   return [NSColor controlHighlightColor];
62 NSColor* ContentTextColor() {
63   return [NSColor blackColor];
65 NSColor* DimTextColor() {
66   return [NSColor darkGrayColor];
68 NSColor* URLTextColor() {
69   return [NSColor colorWithCalibratedRed:0.0 green:0.55 blue:0.0 alpha:1.0];
72 NSFont* FieldFont() {
73   return OmniboxViewMac::GetFieldFont(gfx::Font::NORMAL);
75 NSFont* BoldFieldFont() {
76   return OmniboxViewMac::GetFieldFont(gfx::Font::BOLD);
79 CGFloat GetContentAreaWidth(NSRect cellFrame) {
80   return NSWidth(cellFrame) - kTextStartOffset;
83 NSMutableAttributedString* CreateAttributedString(
84     const base::string16& text,
85     NSColor* text_color,
86     NSTextAlignment textAlignment) {
87   // Start out with a string using the default style info.
88   NSString* s = base::SysUTF16ToNSString(text);
89   NSDictionary* attributes = @{
90       NSFontAttributeName : FieldFont(),
91       NSForegroundColorAttributeName : text_color
92   };
93   NSMutableAttributedString* as =
94       [[[NSMutableAttributedString alloc] initWithString:s
95                                               attributes:attributes]
96         autorelease];
98   NSMutableParagraphStyle* style =
99       [[[NSMutableParagraphStyle alloc] init] autorelease];
100   [style setLineBreakMode:NSLineBreakByTruncatingTail];
101   [style setTighteningFactorForTruncation:0.0];
102   [style setAlignment:textAlignment];
103   [as addAttribute:NSParagraphStyleAttributeName
104              value:style
105              range:NSMakeRange(0, [as length])];
107   return as;
110 NSMutableAttributedString* CreateAttributedString(
111     const base::string16& text,
112     NSColor* text_color) {
113   return CreateAttributedString(text, text_color, NSNaturalTextAlignment);
116 NSAttributedString* CreateClassifiedAttributedString(
117     const base::string16& text,
118     NSColor* text_color,
119     const ACMatchClassifications& classifications) {
120   NSMutableAttributedString* as = CreateAttributedString(text, text_color);
121   NSUInteger match_length = [as length];
123   // Mark up the runs which differ from the default.
124   for (ACMatchClassifications::const_iterator i = classifications.begin();
125        i != classifications.end(); ++i) {
126     const bool is_last = ((i + 1) == classifications.end());
127     const NSUInteger next_offset =
128         (is_last ? match_length : static_cast<NSUInteger>((i + 1)->offset));
129     const NSUInteger location = static_cast<NSUInteger>(i->offset);
130     const NSUInteger length = next_offset - static_cast<NSUInteger>(i->offset);
131     // Guard against bad, off-the-end classification ranges.
132     if (location >= match_length || length <= 0)
133       break;
134     const NSRange range =
135         NSMakeRange(location, std::min(length, match_length - location));
137     if (0 != (i->style & ACMatchClassification::MATCH)) {
138       [as addAttribute:NSFontAttributeName value:BoldFieldFont() range:range];
139     }
141     if (0 != (i->style & ACMatchClassification::URL)) {
142       [as addAttribute:NSForegroundColorAttributeName
143                  value:URLTextColor()
144                  range:range];
145     } else if (0 != (i->style & ACMatchClassification::DIM)) {
146       [as addAttribute:NSForegroundColorAttributeName
147                  value:DimTextColor()
148                  range:range];
149     }
150   }
152   return as;
155 }  // namespace
157 @implementation OmniboxPopupCell
159 - (id)init {
160   self = [super init];
161   if (self) {
162     [self setImagePosition:NSImageLeft];
163     [self setBordered:NO];
164     [self setButtonType:NSRadioButton];
166     // Without this highlighting messes up white areas of images.
167     [self setHighlightsBy:NSNoCellMask];
169     const base::string16& raw_separator =
170         l10n_util::GetStringUTF16(IDS_AUTOCOMPLETE_MATCH_DESCRIPTION_SEPARATOR);
171     separator_.reset(
172         [CreateAttributedString(raw_separator, DimTextColor()) retain]);
173   }
174   return self;
177 - (void)setMatch:(const AutocompleteMatch&)match {
178   match_ = match;
179   NSAttributedString *contents = CreateClassifiedAttributedString(
180       match_.contents, ContentTextColor(), match_.contents_class);
181   [self setAttributedTitle:contents];
183   if (match_.description.empty()) {
184     description_.reset();
185   } else {
186     description_.reset([CreateClassifiedAttributedString(
187         match_.description, DimTextColor(), match_.description_class) retain]);
188   }
191 - (void)setMaxMatchContentsWidth:(CGFloat)maxMatchContentsWidth {
192   maxMatchContentsWidth_ = maxMatchContentsWidth;
195 - (void)setContentsOffset:(CGFloat)contentsOffset {
196   contentsOffset_ = contentsOffset;
199 // The default NSButtonCell drawing leaves the image flush left and
200 // the title next to the image.  This spaces things out to line up
201 // with the star button and autocomplete field.
202 - (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
203   if ([self state] == NSOnState || [self isHighlighted]) {
204     if ([self state] == NSOnState)
205       [SelectedBackgroundColor() set];
206     else
207       [HoveredBackgroundColor() set];
208     NSBezierPath* path =
209         [NSBezierPath bezierPathWithRoundedRect:cellFrame
210                                         xRadius:kCellRoundingRadius
211                                         yRadius:kCellRoundingRadius];
212     [path fill];
213   }
215   // Put the image centered vertically but in a fixed column.
216   NSImage* image = [self image];
217   if (image) {
218     NSRect imageRect = cellFrame;
219     imageRect.size = [image size];
220     imageRect.origin.y +=
221         std::floor((NSHeight(cellFrame) - NSHeight(imageRect)) / 2.0);
222     imageRect.origin.x += kImageXOffset;
223     [image drawInRect:FlipIfRTL(imageRect, cellFrame)
224              fromRect:NSZeroRect  // Entire image
225             operation:NSCompositeSourceOver
226              fraction:1.0
227        respectFlipped:YES
228                 hints:nil];
229   }
231   [self drawMatchWithFrame:cellFrame inView:controlView];
234 - (void)drawMatchWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
235   NSAttributedString* contents = [self attributedTitle];
237   CGFloat remainingWidth = GetContentAreaWidth(cellFrame);
238   CGFloat contentsWidth = [self getMatchContentsWidth];
239   CGFloat separatorWidth = [separator_ size].width;
240   CGFloat descriptionWidth = description_.get() ? [description_ size].width : 0;
241   int contentsMaxWidth, descriptionMaxWidth;
242   OmniboxPopupModel::ComputeMatchMaxWidths(
243       ceilf(contentsWidth),
244       ceilf(separatorWidth),
245       ceilf(descriptionWidth),
246       ceilf(remainingWidth),
247       !AutocompleteMatch::IsSearchType(match_.type),
248       &contentsMaxWidth,
249       &descriptionMaxWidth);
251   CGFloat offset = kTextStartOffset;
252   if (match_.type == AutocompleteMatchType::SEARCH_SUGGEST_INFINITE) {
253     // Infinite suggestions are rendered with a prefix (usually ellipsis), which
254     // appear vertically stacked.
255     offset += [self drawMatchPrefixWithFrame:cellFrame
256                                       inView:controlView
257                         withContentsMaxWidth:&contentsMaxWidth];
258   }
259   offset += [self drawMatchPart:contents
260                       withFrame:cellFrame
261                        atOffset:offset
262                    withMaxWidth:contentsMaxWidth
263                          inView:controlView];
265   if (descriptionMaxWidth != 0) {
266     offset += [self drawMatchPart:separator_
267                         withFrame:cellFrame
268                          atOffset:offset
269                      withMaxWidth:separatorWidth
270                            inView:controlView];
271     offset += [self drawMatchPart:description_
272                         withFrame:cellFrame
273                          atOffset:offset
274                      withMaxWidth:descriptionMaxWidth
275                            inView:controlView];
276   }
279 - (CGFloat)drawMatchPrefixWithFrame:(NSRect)cellFrame
280                              inView:(NSView*)controlView
281                withContentsMaxWidth:(int*)contentsMaxWidth {
282   CGFloat offset = 0.0f;
283   CGFloat remainingWidth = GetContentAreaWidth(cellFrame);
284   bool isRTL = base::i18n::IsRTL();
285   bool isContentsRTL = (base::i18n::RIGHT_TO_LEFT ==
286       base::i18n::GetFirstStrongCharacterDirection(match_.contents));
287   // Prefix may not have any characters with strong directionality, and may take
288   // the UI directionality. But prefix needs to appear in continuation of the
289   // contents so we force the directionality.
290   NSTextAlignment textAlignment = isContentsRTL ?
291       NSRightTextAlignment : NSLeftTextAlignment;
292   prefix_.reset([CreateAttributedString(base::UTF8ToUTF16(
293       match_.GetAdditionalInfo(kACMatchPropertyContentsPrefix)),
294       ContentTextColor(), textAlignment) retain]);
295   CGFloat prefixWidth = [prefix_ size].width;
297   CGFloat prefixOffset = 0.0f;
298   if (isRTL != isContentsRTL) {
299     // The contents is rendered between the contents offset extending towards
300     // the start edge, while prefix is rendered in opposite direction. Ideally
301     // the prefix should be rendered at |contentsOffset_|. If that is not
302     // sufficient to render the widest suggestion, we increase it to
303     // |maxMatchContentsWidth_|.  If |remainingWidth| is not sufficient to
304     // accomodate that, we reduce the offset so that the prefix gets rendered.
305     prefixOffset = std::min(
306         remainingWidth - prefixWidth, std::max(contentsOffset_,
307                                                maxMatchContentsWidth_));
308     offset = std::max<CGFloat>(0.0, prefixOffset - *contentsMaxWidth);
309   } else { // The direction of contents is same as UI direction.
310     // Ideally the offset should be |contentsOffset_|. If the max total width
311     // (|prefixWidth| + |maxMatchContentsWidth_|) from offset will exceed the
312     // |remainingWidth|, then we shift the offset to the left , so that all
313     // postfix suggestions are visible.
314     // We have to render the prefix, so offset has to be at least |prefixWidth|.
315     offset = std::max(prefixWidth,
316         std::min(remainingWidth - maxMatchContentsWidth_, contentsOffset_));
317     prefixOffset = offset - prefixWidth;
318   }
319   *contentsMaxWidth = std::min((int)ceilf(remainingWidth - prefixWidth),
320                                *contentsMaxWidth);
321   [self drawMatchPart:prefix_
322             withFrame:cellFrame
323              atOffset:prefixOffset + kTextStartOffset
324          withMaxWidth:prefixWidth
325                inView:controlView];
326   return offset;
329 - (CGFloat)drawMatchPart:(NSAttributedString*)as
330                withFrame:(NSRect)cellFrame
331                 atOffset:(CGFloat)offset
332             withMaxWidth:(int)maxWidth
333                   inView:(NSView*)controlView {
334   if (offset > NSWidth(cellFrame))
335     return 0.0f;
336   NSRect renderRect = ShiftRect(cellFrame, offset);
337   renderRect.size.width =
338       std::min(NSWidth(renderRect), static_cast<CGFloat>(maxWidth));
339   if (renderRect.size.width != 0) {
340     [self drawTitle:as
341           withFrame:FlipIfRTL(renderRect, cellFrame)
342              inView:controlView];
343   }
344   return NSWidth(renderRect);
347 - (CGFloat)getMatchContentsWidth {
348   NSAttributedString* contents = [self attributedTitle];
349   return contents ? [contents size].width : 0;
353 + (CGFloat)computeContentsOffset:(const AutocompleteMatch&)match {
354   const base::string16& inputText = base::UTF8ToUTF16(
355       match.GetAdditionalInfo(kACMatchPropertyInputText));
356   int contentsStartIndex = 0;
357   base::StringToInt(
358       match.GetAdditionalInfo(kACMatchPropertyContentsStartIndex),
359       &contentsStartIndex);
360   // Ignore invalid state.
361   if (!StartsWith(match.fill_into_edit, inputText, true)
362       || !EndsWith(match.fill_into_edit, match.contents, true)
363       || ((size_t)contentsStartIndex >= inputText.length())) {
364     return 0;
365   }
366   bool isRTL = base::i18n::IsRTL();
367   bool isContentsRTL = (base::i18n::RIGHT_TO_LEFT ==
368       base::i18n::GetFirstStrongCharacterDirection(match.contents));
370   // Color does not matter.
371   NSAttributedString* as = CreateAttributedString(inputText, DimTextColor());
372   base::scoped_nsobject<NSTextStorage> textStorage([[NSTextStorage alloc]
373       initWithAttributedString:as]);
374   base::scoped_nsobject<NSLayoutManager> layoutManager(
375       [[NSLayoutManager alloc] init]);
376   base::scoped_nsobject<NSTextContainer> textContainer(
377       [[NSTextContainer alloc] init]);
378   [layoutManager addTextContainer:textContainer];
379   [textStorage addLayoutManager:layoutManager];
381   NSUInteger charIndex = static_cast<NSUInteger>(contentsStartIndex);
382   NSUInteger glyphIndex =
383       [layoutManager glyphIndexForCharacterAtIndex:charIndex];
385   // This offset is computed from the left edge of the glyph always from the
386   // left edge of the string, irrespective of the directionality of UI or text.
387   CGFloat glyphOffset = [layoutManager locationForGlyphAtIndex:glyphIndex].x;
389   CGFloat inputWidth = [as size].width;
391   // The offset obtained above may need to be corrected because the left-most
392   // glyph may not have 0 offset. So we find the offset of left-most glyph, and
393   // subtract it from the offset of the glyph we obtained above.
394   CGFloat minOffset = glyphOffset;
396   // If content is RTL, we are interested in the right-edge of the glyph.
397   // Unfortunately the bounding rect computation methods from NSLayoutManager or
398   // NSFont don't work correctly with bidirectional text. So we compute the
399   // glyph width by finding the closest glyph offset to the right of the glyph
400   // we are looking for.
401   CGFloat glyphWidth = inputWidth;
403   for (NSUInteger i = 0; i < [as length]; i++) {
404     if (i == charIndex) continue;
405     glyphIndex = [layoutManager glyphIndexForCharacterAtIndex:i];
406     CGFloat offset = [layoutManager locationForGlyphAtIndex:glyphIndex].x;
407     minOffset = std::min(minOffset, offset);
408     if (offset > glyphOffset)
409       glyphWidth = std::min(glyphWidth, offset - glyphOffset);
410   }
411   glyphOffset -= minOffset;
412   if (glyphWidth == 0)
413     glyphWidth = inputWidth - glyphOffset;
414   if (isContentsRTL)
415     glyphOffset += glyphWidth;
416   return isRTL ? (inputWidth - glyphOffset) : glyphOffset;
419 @end