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