Home | History | Annotate | Download | only in omnibox
      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.
      4 
      5 #import "chrome/browser/ui/cocoa/omnibox/omnibox_popup_cell.h"
      6 
      7 #include <algorithm>
      8 #include <cmath>
      9 
     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"
     22 
     23 namespace {
     24 
     25 // How far to offset image column from the left.
     26 const CGFloat kImageXOffset = 5.0;
     27 
     28 // How far to offset the text column from the left.
     29 const CGFloat kTextStartOffset = 28.0;
     30 
     31 // Rounding radius of selection and hover background on popup items.
     32 const CGFloat kCellRoundingRadius = 2.0;
     33 
     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;
     44 }
     45 
     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;
     53 }
     54 
     55 NSColor* SelectedBackgroundColor() {
     56   return [NSColor selectedControlColor];
     57 }
     58 NSColor* HoveredBackgroundColor() {
     59   return [NSColor controlHighlightColor];
     60 }
     61 
     62 NSColor* ContentTextColor() {
     63   return [NSColor blackColor];
     64 }
     65 NSColor* DimTextColor() {
     66   return [NSColor darkGrayColor];
     67 }
     68 NSColor* URLTextColor() {
     69   return [NSColor colorWithCalibratedRed:0.0 green:0.55 blue:0.0 alpha:1.0];
     70 }
     71 
     72 NSFont* FieldFont() {
     73   return OmniboxViewMac::GetFieldFont(gfx::Font::NORMAL);
     74 }
     75 NSFont* BoldFieldFont() {
     76   return OmniboxViewMac::GetFieldFont(gfx::Font::BOLD);
     77 }
     78 
     79 CGFloat GetContentAreaWidth(NSRect cellFrame) {
     80   return NSWidth(cellFrame) - kTextStartOffset;
     81 }
     82 
     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];
     97 
     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])];
    106 
    107   return as;
    108 }
    109 
    110 NSMutableAttributedString* CreateAttributedString(
    111     const base::string16& text,
    112     NSColor* text_color) {
    113   return CreateAttributedString(text, text_color, NSNaturalTextAlignment);
    114 }
    115 
    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];
    122 
    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));
    136 
    137     if (0 != (i->style & ACMatchClassification::MATCH)) {
    138       [as addAttribute:NSFontAttributeName value:BoldFieldFont() range:range];
    139     }
    140 
    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   }
    151 
    152   return as;
    153 }
    154 
    155 }  // namespace
    156 
    157 @implementation OmniboxPopupCell
    158 
    159 - (id)init {
    160   self = [super init];
    161   if (self) {
    162     [self setImagePosition:NSImageLeft];
    163     [self setBordered:NO];
    164     [self setButtonType:NSRadioButton];
    165 
    166     // Without this highlighting messes up white areas of images.
    167     [self setHighlightsBy:NSNoCellMask];
    168 
    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;
    175 }
    176 
    177 - (void)setMatch:(const AutocompleteMatch&)match {
    178   match_ = match;
    179   NSAttributedString *contents = CreateClassifiedAttributedString(
    180       match_.contents, ContentTextColor(), match_.contents_class);
    181   [self setAttributedTitle:contents];
    182 
    183   if (match_.description.empty()) {
    184     description_.reset();
    185   } else {
    186     description_.reset([CreateClassifiedAttributedString(
    187         match_.description, DimTextColor(), match_.description_class) retain]);
    188   }
    189 }
    190 
    191 - (void)setMaxMatchContentsWidth:(CGFloat)maxMatchContentsWidth {
    192   maxMatchContentsWidth_ = maxMatchContentsWidth;
    193 }
    194 
    195 - (void)setContentsOffset:(CGFloat)contentsOffset {
    196   contentsOffset_ = contentsOffset;
    197 }
    198 
    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   }
    214 
    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   }
    230 
    231   [self drawMatchWithFrame:cellFrame inView:controlView];
    232 }
    233 
    234 - (void)drawMatchWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
    235   NSAttributedString* contents = [self attributedTitle];
    236 
    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);
    250 
    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];
    264 
    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   }
    277 }
    278 
    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;
    296 
    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;
    327 }
    328 
    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);
    345 }
    346 
    347 - (CGFloat)getMatchContentsWidth {
    348   NSAttributedString* contents = [self attributedTitle];
    349   return contents ? [contents size].width : 0;
    350 }
    351 
    352 
    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));
    369 
    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];
    380 
    381   NSUInteger charIndex = static_cast<NSUInteger>(contentsStartIndex);
    382   NSUInteger glyphIndex =
    383       [layoutManager glyphIndexForCharacterAtIndex:charIndex];
    384 
    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;
    388 
    389   CGFloat inputWidth = [as size].width;
    390 
    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;
    395 
    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;
    402 
    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;
    417 }
    418 
    419 @end
    420