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"
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"
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()) {
40 result.origin.x = NSMinX(frame) + (NSMaxX(frame) - NSMaxX(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));
50 result.origin.x += dX;
51 result.size.width -= dX;
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];
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,
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
93 NSMutableAttributedString* as =
94 [[[NSMutableAttributedString alloc] initWithString:s
95 attributes:attributes]
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
105 range:NSMakeRange(0, [as length])];
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,
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)
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];
141 if (0 != (i->style & ACMatchClassification::URL)) {
142 [as addAttribute:NSForegroundColorAttributeName
145 } else if (0 != (i->style & ACMatchClassification::DIM)) {
146 [as addAttribute:NSForegroundColorAttributeName
157 @implementation OmniboxPopupCell
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);
172 [CreateAttributedString(raw_separator, DimTextColor()) retain]);
177 - (void)setMatch:(const AutocompleteMatch&)match {
179 NSAttributedString *contents = CreateClassifiedAttributedString(
180 match_.contents, ContentTextColor(), match_.contents_class);
181 [self setAttributedTitle:contents];
183 if (match_.description.empty()) {
184 description_.reset();
186 description_.reset([CreateClassifiedAttributedString(
187 match_.description, DimTextColor(), match_.description_class) retain]);
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];
207 [HoveredBackgroundColor() set];
209 [NSBezierPath bezierPathWithRoundedRect:cellFrame
210 xRadius:kCellRoundingRadius
211 yRadius:kCellRoundingRadius];
215 // Put the image centered vertically but in a fixed column.
216 NSImage* image = [self 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
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),
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
257 withContentsMaxWidth:&contentsMaxWidth];
259 offset += [self drawMatchPart:contents
262 withMaxWidth:contentsMaxWidth
265 if (descriptionMaxWidth != 0) {
266 offset += [self drawMatchPart:separator_
269 withMaxWidth:separatorWidth
271 offset += [self drawMatchPart:description_
274 withMaxWidth:descriptionMaxWidth
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;
319 *contentsMaxWidth = std::min((int)ceilf(remainingWidth - prefixWidth),
321 [self drawMatchPart:prefix_
323 atOffset:prefixOffset + kTextStartOffset
324 withMaxWidth:prefixWidth
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))
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) {
341 withFrame:FlipIfRTL(renderRect, cellFrame)
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;
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())) {
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);
411 glyphOffset -= minOffset;
413 glyphWidth = inputWidth - glyphOffset;
415 glyphOffset += glyphWidth;
416 return isRTL ? (inputWidth - glyphOffset) : glyphOffset;