Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(96)

Side by Side Diff: ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.mm

Issue 2589803002: Upstream Chrome on iOS source code [6/11]. (Closed)
Patch Set: Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
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 #import "ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.h"
6
7 #import <CoreText/CoreText.h>
8
9 #include "base/command_line.h"
10 #include "base/ios/ios_util.h"
11 #include "base/logging.h"
12 #include "base/mac/foundation_util.h"
13 #include "base/mac/objc_property_releaser.h"
14 #include "base/mac/scoped_nsobject.h"
15 #include "base/strings/sys_string_conversions.h"
16 #include "components/grit/components_scaled_resources.h"
17 #include "components/omnibox/browser/autocomplete_input.h"
18 #include "ios/chrome/browser/autocomplete/autocomplete_scheme_classifier_impl.h"
19 #import "ios/chrome/browser/ui/animation_util.h"
20 #include "ios/chrome/browser/ui/omnibox/omnibox_util.h"
21 #import "ios/chrome/browser/ui/reversed_animation.h"
22 #include "ios/chrome/browser/ui/rtl_geometry.h"
23 #include "ios/chrome/browser/ui/ui_util.h"
24 #import "ios/chrome/browser/ui/uikit_ui_util.h"
25 #import "ios/chrome/common/material_timing.h"
26 #include "ios/chrome/grit/ios_strings.h"
27 #include "ios/chrome/grit/ios_theme_resources.h"
28 #include "skia/ext/skia_utils_ios.h"
29 #include "third_party/google_toolbox_for_mac/src/iPhone/GTMFadeTruncatingLabel.h "
30 #include "ui/base/l10n/l10n_util_mac.h"
31 #include "ui/base/resource/resource_bundle.h"
32 #include "ui/gfx/color_palette.h"
33 #include "ui/gfx/image/image.h"
34 #import "ui/gfx/ios/NSString+CrStringDrawing.h"
35 #include "ui/gfx/scoped_cg_context_save_gstate_mac.h"
36
37 namespace {
38 const CGFloat kFontSize = 16;
39 const CGFloat kEditingRectX = 16;
40 const CGFloat kEditingRectWidthInset = 10;
41 const CGFloat kTextInset = 8;
42 const CGFloat kTextInsetWithChip = 3;
43 const CGFloat kTextInsetNoLeftView = 12;
44 const CGFloat kImageInset = 9;
45 const CGFloat kClearButtonRightMarginIphone = 7;
46 const CGFloat kClearButtonRightMarginIpad = 12;
47 // Amount to shift the origin.x of the text areas so they're centered within the
48 // omnibox border.
49 const CGFloat kTextAreaLeadingOffset = -2;
50
51 // TODO(rohitrao): Should this be pulled from somewhere else?
52 const CGFloat kStarButtonWidth = 36;
53 const CGFloat kVoiceSearchButtonWidth = 36.0;
54
55 // The default omnibox text color (used while editing).
56 UIColor* TextColor() {
57 return [UIColor colorWithWhite:(51 / 255.0) alpha:1.0];
58 }
59
60 NSString* const kOmniboxFadeAnimationKey = @"OmniboxFadeAnimation";
61
62 } // namespace
63
64 @interface OmniboxTextFieldIOS ()
65
66 // Current image id used in left view.
67 @property(nonatomic, assign) NSUInteger leftViewImageId;
68
69 // Gets the bounds of the rect covering the URL.
70 - (CGRect)preEditLabelRectForBounds:(CGRect)bounds;
71 // Creates the UILabel if it doesn't already exist and adds it as a
72 // subview.
73 - (void)createSelectionViewIfNecessary;
74 // Helper method used to set the text of this field. Updates the selection view
75 // to contain the correct inline autocomplete text.
76 - (void)setTextInternal:(NSAttributedString*)text
77 autocompleteLength:(NSUInteger)autocompleteLength;
78 // Display an image or chip text in the left accessory view.
79 - (void)updateLeftView;
80 // Override deleteBackward so that backspace can clear query refinement chips.
81 - (void)deleteBackward;
82 // Returns the layers affected by animations added by |-animateFadeWithStyle:|.
83 - (NSArray*)fadeAnimationLayers;
84 // Returns the text that is displayed in the field, including any inline
85 // autocomplete text that may be present as an NSString. Returns the same
86 // value as -|displayedText| but prefer to use this to avoid unnecessary
87 // conversion from NSString to base::string16 if possible.
88 - (NSString*)nsDisplayedText;
89
90 @end
91
92 #pragma mark -
93 #pragma mark OmniboxTextFieldIOS
94
95 @implementation OmniboxTextFieldIOS {
96 // Currently selected chip text. Nil if no chip.
97 base::scoped_nsobject<NSString> _chipText;
98 base::scoped_nsobject<UILabel> _selection;
99 base::scoped_nsobject<UILabel> _preEditStaticLabel;
100 NSString* _preEditText;
101 base::scoped_nsobject<UIFont> _font;
102 base::scoped_nsobject<UIColor> _displayedTextColor;
103 base::scoped_nsobject<UIColor> _displayedTintColor;
104 UIColor* _selectedTextBackgroundColor;
105 UIColor* _placeholderTextColor;
106
107 // The 'Copy URL' menu item is sometimes shown in the edit menu, so keep it
108 // around to make adding/removing easier.
109 base::scoped_nsobject<UIMenuItem> _copyUrlMenuItem;
110
111 base::mac::ObjCPropertyReleaser _propertyReleaser_OmniboxTextFieldIOS;
112 }
113
114 @synthesize leftViewImageId = _leftViewImageId;
115 @synthesize preEditText = _preEditText;
116 @synthesize clearingPreEditText = _clearingPreEditText;
117 @synthesize selectedTextBackgroundColor = _selectedTextBackgroundColor;
118 @synthesize placeholderTextColor = _placeholderTextColor;
119 @synthesize incognito = _incognito;
120
121 // Overload to allow for code-based initialization.
122 - (instancetype)initWithFrame:(CGRect)frame {
123 return [self initWithFrame:frame
124 font:[UIFont systemFontOfSize:kFontSize]
125 textColor:TextColor()
126 tintColor:nil];
127 }
128
129 - (instancetype)initWithFrame:(CGRect)frame
130 font:(UIFont*)font
131 textColor:(UIColor*)textColor
132 tintColor:(UIColor*)tintColor {
133 self = [super initWithFrame:frame];
134 if (self) {
135 _propertyReleaser_OmniboxTextFieldIOS.Init(self,
136 [OmniboxTextFieldIOS class]);
137 _font.reset([font retain]);
138 _displayedTextColor.reset([textColor retain]);
139 if (tintColor) {
140 [self setTintColor:tintColor];
141 _displayedTintColor.reset([tintColor retain]);
142 } else {
143 _displayedTintColor.reset([self.tintColor retain]);
144 }
145 [self setFont:_font];
146 [self setTextColor:_displayedTextColor];
147 [self setClearButtonMode:UITextFieldViewModeNever];
148 [self setRightViewMode:UITextFieldViewModeAlways];
149 [self setAutocorrectionType:UITextAutocorrectionTypeNo];
150 [self setAutocapitalizationType:UITextAutocapitalizationTypeNone];
151 [self setEnablesReturnKeyAutomatically:YES];
152 [self setReturnKeyType:UIReturnKeyGo];
153 [self setContentVerticalAlignment:UIControlContentVerticalAlignmentCenter];
154 [self setSpellCheckingType:UITextSpellCheckingTypeNo];
155 [self setTextAlignment:NSTextAlignmentNatural];
156 [self setKeyboardType:(UIKeyboardType)UIKeyboardTypeWebSearch];
157
158 // Sanity check:
159 DCHECK([self conformsToProtocol:@protocol(UITextInput)]);
160
161 // Force initial layout of internal text label. Needed for omnibox
162 // animations that will otherwise animate the text label from origin {0, 0}.
163 [super setText:@" "];
164 }
165 return self;
166 }
167
168 - (instancetype)initWithCoder:(nonnull NSCoder*)aDecoder {
169 NOTREACHED();
170 return nil;
171 }
172
173 // Enforces that the delegate is an OmniboxTextFieldDelegate.
174 - (id<OmniboxTextFieldDelegate>)delegate {
175 id delegate = [super delegate];
176 DCHECK(delegate == nil ||
177 [[delegate class]
178 conformsToProtocol:@protocol(OmniboxTextFieldDelegate)]);
179 return delegate;
180 }
181
182 // Overridden to require an OmniboxTextFieldDelegate.
183 - (void)setDelegate:(id<OmniboxTextFieldDelegate>)delegate {
184 [super setDelegate:delegate];
185 }
186
187 // Exposed for testing.
188 - (UILabel*)preEditStaticLabel {
189 return _preEditStaticLabel;
190 }
191
192 - (void)insertTextWhileEditing:(NSString*)text {
193 // This method should only be called while editing.
194 DCHECK([self isFirstResponder]);
195
196 if ([self markedTextRange] != nil)
197 [self unmarkText];
198
199 NSRange selectedNSRange = [self selectedNSRange];
200 if (![self delegate] || [[self delegate] textField:self
201 shouldChangeCharactersInRange:selectedNSRange
202 replacementString:text]) {
203 [self replaceRange:[self selectedTextRange] withText:text];
204 }
205 }
206
207 // Method called when the users touches the text input. This will accept the
208 // autocompleted text.
209 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
210 if ([self isPreEditing]) {
211 [self exitPreEditState];
212 [super selectAll:nil];
213 }
214
215 if (!_selection.get()) {
216 [super touchesBegan:touches withEvent:event];
217 return;
218 }
219
220 // Only consider a single touch.
221 UITouch* touch = [touches anyObject];
222 if (!touch)
223 return;
224
225 // Accept selection.
226 base::scoped_nsobject<NSString> newText([[self nsDisplayedText] copy]);
227 [self clearAutocompleteText];
228 [self setText:newText];
229 }
230
231 // Gets the bounds of the rect covering the URL.
232 - (CGRect)preEditLabelRectForBounds:(CGRect)bounds {
233 return [self editingRectForBounds:self.bounds];
234 }
235
236 // Creates a UILabel based on the current dimension of the text field and
237 // displays the URL in the UILabel so it appears properly aligned to the URL.
238 - (void)enterPreEditState {
239 // Empty omnibox should show the insertion point immediately. There is
240 // nothing to erase.
241 if (!self.text.length || UIAccessibilityIsVoiceOverRunning())
242 return;
243
244 // Remembers the initial text input to compute the diff of what was there
245 // and what was typed.
246 [self setPreEditText:self.text];
247
248 // Adjusts the placement so static URL lines up perfectly with UITextField.
249 DCHECK(!_preEditStaticLabel.get());
250 CGRect rect = [self preEditLabelRectForBounds:self.bounds];
251 _preEditStaticLabel.reset([[UILabel alloc] initWithFrame:rect]);
252 _preEditStaticLabel.get().backgroundColor = [UIColor clearColor];
253 _preEditStaticLabel.get().opaque = YES;
254 _preEditStaticLabel.get().font = _font;
255 _preEditStaticLabel.get().textColor = _displayedTextColor;
256 _preEditStaticLabel.get().lineBreakMode = NSLineBreakByTruncatingHead;
257
258 NSDictionary* attributes =
259 @{NSBackgroundColorAttributeName : [self selectedTextBackgroundColor]};
260 base::scoped_nsobject<NSAttributedString> preEditString(
261 [[NSAttributedString alloc] initWithString:self.text
262 attributes:attributes]);
263 [_preEditStaticLabel setAttributedText:preEditString];
264 _preEditStaticLabel.get().textAlignment = [self preEditTextAlignment];
265 [self addSubview:_preEditStaticLabel];
266 }
267
268 - (NSTextAlignment)bestAlignmentForText:(NSString*)text {
269 if (text.length) {
270 NSString* lang = CFBridgingRelease(CFStringTokenizerCopyBestStringLanguage(
271 (CFStringRef)text, CFRangeMake(0, text.length)));
272
273 if ([NSLocale characterDirectionForLanguage:lang] ==
274 NSLocaleLanguageDirectionRightToLeft) {
275 return NSTextAlignmentRight;
276 }
277 }
278 return NSTextAlignmentLeft;
279 }
280
281 - (NSTextAlignment)bestTextAlignment {
282 if (!base::ios::IsRunningOnIOS9OrLater() || [self isFirstResponder]) {
283 return [self bestAlignmentForText:[self text]];
284 }
285 return NSTextAlignmentNatural;
286 }
287
288 - (NSTextAlignment)preEditTextAlignment {
289 // If the pre-edit text is wider than the omnibox, right-align the text so it
290 // ends at the same x coord as the blue selection box.
291 CGSize textSize =
292 [_preEditStaticLabel.get().text cr_pixelAlignedSizeWithFont:_font];
293 BOOL isLTR = [self bestTextAlignment] == NSTextAlignmentLeft;
294 return textSize.width < _preEditStaticLabel.get().frame.size.width
295 ? (isLTR ? NSTextAlignmentLeft : NSTextAlignmentRight)
296 : (isLTR ? NSTextAlignmentRight : NSTextAlignmentLeft);
297 }
298
299 - (void)layoutSubviews {
300 [super layoutSubviews];
301 if ([self isPreEditing]) {
302 CGRect rect = [self preEditLabelRectForBounds:self.bounds];
303 [_preEditStaticLabel setFrame:rect];
304
305 // Update text alignment since the pre-edit label's frame changed.
306 _preEditStaticLabel.get().textAlignment = [self preEditTextAlignment];
307 [self hideTextAndCursor];
308 } else if (!_selection) {
309 [self showTextAndCursor];
310 }
311 }
312
313 // Finishes pre-edit state by removing the UILabel with the URL.
314 - (void)exitPreEditState {
315 [self setPreEditText:nil];
316 if (_preEditStaticLabel) {
317 [_preEditStaticLabel removeFromSuperview];
318 _preEditStaticLabel.reset(nil);
319 [self showTextAndCursor];
320 }
321 }
322
323 - (UIColor*)displayedTextColor {
324 return _displayedTextColor;
325 }
326
327 // Returns whether we are processing the first touch event on the text field.
328 - (BOOL)isPreEditing {
329 return !![self preEditText];
330 }
331
332 - (void)enableLeftViewButton:(BOOL)isEnabled {
333 if ([self leftView])
334 [(UIButton*)[self leftView] setEnabled:isEnabled];
335 }
336
337 - (NSString*)nsDisplayedText {
338 if (_selection.get())
339 return [_selection text];
340 return [self text];
341 }
342
343 - (base::string16)displayedText {
344 return base::SysNSStringToUTF16([self nsDisplayedText]);
345 }
346
347 - (base::string16)autocompleteText {
348 DCHECK_LT([[self text] length], [[_selection text] length])
349 << "[_selection text] and [self text] are out of sync. "
350 << "Please email justincohen@ and rohitrao@ if you see this.";
351 if (_selection.get() && [[_selection text] length] > [[self text] length]) {
352 return base::SysNSStringToUTF16(
353 [[_selection text] substringFromIndex:[[self text] length]]);
354 }
355 return base::string16();
356 }
357
358 - (void)select:(id)sender {
359 if ([self isPreEditing]) {
360 [self exitPreEditState];
361 }
362 [super select:sender];
363 }
364
365 - (void)selectAll:(id)sender {
366 if ([self isPreEditing]) {
367 [self exitPreEditState];
368 }
369 if (_selection.get()) {
370 base::scoped_nsobject<NSString> newText([[self nsDisplayedText] copy]);
371 [self clearAutocompleteText];
372 [self setText:newText];
373 }
374 [super selectAll:sender];
375 }
376
377 // Creates the SelectedTextLabel if it doesn't already exist and adds it as a
378 // subview.
379 - (void)createSelectionViewIfNecessary {
380 if (_selection.get())
381 return;
382
383 _selection.reset([[UILabel alloc] initWithFrame:CGRectZero]);
384 [_selection setFont:_font];
385 [_selection setTextColor:_displayedTextColor];
386 [_selection setOpaque:NO];
387 [_selection setBackgroundColor:[UIColor clearColor]];
388 [self addSubview:_selection];
389 [self hideTextAndCursor];
390 }
391
392 - (BOOL)isShowingQueryRefinementChip {
393 return (_chipText && ([self isFirstResponder] || [self isPreEditing]));
394 }
395
396 - (void)updateLeftView {
397 const CGFloat kChipTextTopInset = 3.0;
398 const CGFloat kChipTextLeftInset = 3.0;
399
400 UIButton* leftViewButton = (UIButton*)self.leftView;
401 // Only set the chip image if the omnibox is in focus.
402 if ([self isShowingQueryRefinementChip]) {
403 [leftViewButton setTitle:_chipText forState:UIControlStateNormal];
404 [leftViewButton setImage:nil forState:UIControlStateNormal];
405 [leftViewButton
406 setTitleEdgeInsets:UIEdgeInsetsMake(kChipTextTopInset,
407 kChipTextLeftInset, 0, 0)];
408 // For iPhone, the left view is only updated when not in editing mode (i.e.
409 // the text field is not first responder).
410 } else if (_leftViewImageId && (IsIPadIdiom() || ![self isFirstResponder])) {
411 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
412 gfx::Image defaultImage = rb.GetNativeImageNamed(_leftViewImageId);
413 UIImage* image = [defaultImage.ToUIImage()
414 imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
415 UIImageView* imageView =
416 [[[UIImageView alloc] initWithImage:image] autorelease];
417 [leftViewButton setImage:imageView.image forState:UIControlStateNormal];
418 [leftViewButton setTitle:nil forState:UIControlStateNormal];
419 UIColor* tint = [UIColor whiteColor];
420 if (!_incognito) {
421 switch (_leftViewImageId) {
422 case IDR_IOS_LOCATION_BAR_HTTP:
423 tint = [UIColor darkGrayColor];
424 break;
425 case IDR_IOS_OMNIBOX_HTTPS_VALID:
426 tint = skia::UIColorFromSkColor(gfx::kGoogleGreen700);
427 break;
428 case IDR_IOS_OMNIBOX_HTTPS_POLICY_WARNING:
429 tint = skia::UIColorFromSkColor(gfx::kGoogleYellow700);
430 break;
431 case IDR_IOS_OMNIBOX_HTTPS_INVALID:
432 tint = skia::UIColorFromSkColor(gfx::kGoogleRed700);
433 break;
434 default:
435 tint = [UIColor darkGrayColor];
436 }
437 }
438 [leftViewButton setTintColor:tint];
439 } else {
440 // Reset the chip text.
441 [leftViewButton setTitle:_chipText forState:UIControlStateNormal];
442 }
443 // Normally this isn't needed, but there is a bug in iOS 7.1+ where setting
444 // the image while disabled doesn't always honor UIControlStateNormal.
445 // crbug.com/355077
446 [leftViewButton setNeedsLayout];
447
448 [leftViewButton sizeToFit];
449
450 // -sizeToFit doesn't take into account the left inset, so expand the width of
451 // the button by |kChipTextLeftInset|.
452 if ([self isShowingQueryRefinementChip]) {
453 CGRect frame = leftViewButton.frame;
454 frame.size.width += kChipTextLeftInset;
455 leftViewButton.frame = frame;
456 }
457 }
458
459 - (void)deleteBackward {
460 // Must test for the onDeleteBackward method, since it's optional.
461 if ([[self delegate] respondsToSelector:@selector(onDeleteBackward)])
462 [[self delegate] onDeleteBackward];
463 [super deleteBackward];
464 }
465
466 // Helper method used to set the text of this field. Updates the selection view
467 // to contain the correct inline autocomplete text.
468 - (void)setTextInternal:(NSAttributedString*)text
469 autocompleteLength:(NSUInteger)autocompleteLength {
470 // Extract substrings for the permanent text and the autocomplete text. The
471 // former needs to retain any text attributes from the original string.
472 NSRange fieldRange = NSMakeRange(0, [text length] - autocompleteLength);
473 NSAttributedString* fieldText =
474 [text attributedSubstringFromRange:fieldRange];
475
476 if (autocompleteLength > 0) {
477 // Creating |autocompleteText| from |[text string]| has the added bonus of
478 // removing all the previously set attributes. This way the autocomplete
479 // text doesn't have a highlighted protocol, etc.
480 base::scoped_nsobject<NSMutableAttributedString> autocompleteText(
481 [[NSMutableAttributedString alloc] initWithString:[text string]]);
482
483 [self createSelectionViewIfNecessary];
484 DCHECK(_selection.get());
485 [autocompleteText
486 addAttribute:NSBackgroundColorAttributeName
487 value:[self selectedTextBackgroundColor]
488 range:NSMakeRange([fieldText length], autocompleteLength)];
489 [_selection setAttributedText:autocompleteText];
490 [_selection setTextAlignment:[self bestTextAlignment]];
491 } else {
492 [self clearAutocompleteText];
493 }
494
495 self.attributedText = fieldText;
496
497 // iOS changes the font to .LastResort when some unexpected unicode strings
498 // are used (e.g. 𝗲𝗺𝗽𝗵𝗮𝘀𝗶𝘀). Setting the NSFontAttributeName in the
499 // attributed string to -systemFontOfSize fixes part of the problem, but the
500 // baseline changes so text is out of alignment.
501 [self setFont:_font];
502 // TODO(justincohen): Find a better place to put this, and consolidate it with
503 // the same call in omniboxViewIOS.
504 [self updateTextDirection];
505 }
506
507 - (UIColor*)selectedTextBackgroundColor {
508 return _selectedTextBackgroundColor ? _selectedTextBackgroundColor
509 : [UIColor colorWithRed:204.0 / 255
510 green:221.0 / 255
511 blue:237.0 / 255
512 alpha:1.0];
513 }
514
515 // Ensures that attributedText always uses the proper style attributes.
516 - (void)setAttributedText:(NSAttributedString*)attributedText {
517 base::scoped_nsobject<NSMutableAttributedString> mutableText(
518 [attributedText mutableCopy]);
519 NSRange entireString = NSMakeRange(0, [mutableText length]);
520
521 // Set the font.
522 [mutableText addAttribute:NSFontAttributeName value:_font range:entireString];
523
524 // When editing, use the default text color for all text.
525 if (self.editing) {
526 // Hide the text when the |_selection| label is displayed.
527 UIColor* textColor =
528 _selection ? [UIColor clearColor] : _displayedTextColor.get();
529 [mutableText addAttribute:NSForegroundColorAttributeName
530 value:textColor
531 range:entireString];
532 } else {
533 base::scoped_nsobject<NSMutableParagraphStyle> style(
534 [[NSMutableParagraphStyle alloc] init]);
535 // URLs have their text direction set to to LTR (avoids RTL characters
536 // making the URL render from right to left, as per RFC 3987 Section 4.1).
537 [style setBaseWritingDirection:NSWritingDirectionLeftToRight];
538
539 // Set linebreak mode to 'clipping' to ensure the text is never elided.
540 // This is a workaround for iOS 6, where it appears that
541 // [self.attributedText size] is not wide enough for the string (e.g. a URL
542 // else ending with '.com' will be elided to end with '.c...'). It appears
543 // to be off by one point so clipping is acceptable as it doesn't actually
544 // cut off any of the text.
545 [style setLineBreakMode:NSLineBreakByClipping];
546
547 [mutableText addAttribute:NSParagraphStyleAttributeName
548 value:style
549 range:entireString];
550 }
551
552 [super setAttributedText:mutableText];
553 }
554
555 // Normally NSTextAlignmentNatural would handle text alignment automatically,
556 // but there are numerous edge case issues with it, so it's simpler to just
557 // manually update the text alignment and writing direction of the UITextField.
558 - (void)updateTextDirection {
559 // Setting the empty field to Natural seems to let iOS update the cursor
560 // position when the keyboard language is changed.
561 if (![self text].length) {
562 [self setTextAlignment:NSTextAlignmentNatural];
563 return;
564 }
565
566 NSTextAlignment alignment = [self bestTextAlignment];
567 [self setTextAlignment:alignment];
568 UITextWritingDirection writingDirection =
569 alignment == NSTextAlignmentLeft ? UITextWritingDirectionLeftToRight
570 : UITextWritingDirectionRightToLeft;
571 [self
572 setBaseWritingDirection:writingDirection
573 forRange:[self
574 textRangeFromPosition:[self
575 beginningOfDocument]
576 toPosition:[self endOfDocument]]];
577 }
578
579 - (void)setPlaceholder:(NSString*)placeholder {
580 if (placeholder && _placeholderTextColor) {
581 NSDictionary* attributes =
582 @{NSForegroundColorAttributeName : _placeholderTextColor};
583 self.attributedPlaceholder =
584 [[[NSAttributedString alloc] initWithString:placeholder
585 attributes:attributes] autorelease];
586 } else {
587 [super setPlaceholder:placeholder];
588 }
589 }
590
591 - (void)setText:(NSString*)text {
592 NSAttributedString* as =
593 [[[NSAttributedString alloc] initWithString:text] autorelease];
594 if (self.text.length > 0 && as.length == 0) {
595 // Remove the fade animations before the subviews are removed.
596 [self cleanUpFadeAnimations];
597 }
598 [self setTextInternal:as autocompleteLength:0];
599 }
600
601 - (void)setText:(NSAttributedString*)text
602 userTextLength:(size_t)userTextLength {
603 DCHECK_LE(userTextLength, [text length]);
604
605 NSUInteger autocompleteLength = [text length] - userTextLength;
606 [self setTextInternal:text autocompleteLength:autocompleteLength];
607 }
608
609 - (void)setChipText:(NSString*)chipName {
610 _chipText.reset();
611 if ([chipName length]) {
612 if ([self bestAlignmentForText:chipName] == NSTextAlignmentLeft)
613 chipName = [chipName stringByAppendingString:@":"];
614 _chipText.reset([chipName copy]);
615 }
616 [self updateLeftView];
617 }
618
619 - (BOOL)hasAutocompleteText {
620 return !!_selection.get();
621 }
622
623 - (void)clearAutocompleteText {
624 if (_selection) {
625 [_selection removeFromSuperview];
626 _selection.reset(nil);
627 [self showTextAndCursor];
628 }
629 }
630
631 - (BOOL)isColorHidden:(UIColor*)color {
632 return ([color isEqual:[UIColor clearColor]] ||
633 CGColorGetAlpha(color.CGColor) < 0.05);
634 }
635
636 // Set the text field's text and cursor to their displayed colors. To be called
637 // when there are no overlaid views displayed.
638 - (void)showTextAndCursor {
639 if ([self isColorHidden:self.textColor]) {
640 [self setTextColor:_displayedTextColor];
641 }
642 if ([self isColorHidden:self.tintColor]) {
643 [self setTintColor:_displayedTintColor];
644 }
645 }
646
647 // Set the text field's text and cursor to clear so that they don't show up
648 // behind any overlaid views.
649 - (void)hideTextAndCursor {
650 [self setTintColor:[UIColor clearColor]];
651 [self setTextColor:[UIColor clearColor]];
652 }
653
654 - (NSString*)markedText {
655 DCHECK([self conformsToProtocol:@protocol(UITextInput)]);
656 return [self textInRange:[self markedTextRange]];
657 }
658
659 - (CGRect)textRectForBounds:(CGRect)bounds {
660 CGRect newBounds = [super textRectForBounds:bounds];
661
662 LayoutRect textRectLayout =
663 LayoutRectForRectInBoundingRect(newBounds, bounds);
664 CGFloat textInset = [self leftViewMode] == UITextFieldViewModeAlways
665 ? kTextInset
666 : kTextInsetNoLeftView;
667 // Shift the text right and reduce the width to create empty space between the
668 // left view and the omnibox text.
669 textRectLayout.position.leading += textInset + kTextAreaLeadingOffset;
670 textRectLayout.size.width -= textInset - kTextAreaLeadingOffset;
671
672 if (IsIPadIdiom()) {
673 if (!IsCompactTablet()) {
674 // Adjust the width so that the text doesn't overlap with the bookmark and
675 // voice search buttons which are displayed inside the omnibox.
676 textRectLayout.size.width += self.rightView.bounds.size.width -
677 kVoiceSearchButtonWidth - kStarButtonWidth;
678 }
679 } else if (![self isShowingQueryRefinementChip] && self.leftView.alpha == 0) {
680 CGFloat xDiff = textRectLayout.position.leading - kEditingRectX;
681 textRectLayout.position.leading = kEditingRectX;
682 textRectLayout.size.width += xDiff;
683 }
684
685 return LayoutRectGetRect(textRectLayout);
686 }
687
688 - (CGRect)editingRectForBounds:(CGRect)bounds {
689 CGRect newBounds = [super editingRectForBounds:bounds];
690
691 // -editingRectForBounds doesn't account for rightViews that aren't flush
692 // with the right edge, it just looks at the rightView's width. Account for
693 // the offset here.
694 CGFloat rightViewMaxX = CGRectGetMaxX([self rightViewRectForBounds:bounds]);
695 if (rightViewMaxX)
696 newBounds.size.width -= bounds.size.width - rightViewMaxX;
697
698 LayoutRect editingRectLayout =
699 LayoutRectForRectInBoundingRect(newBounds, bounds);
700 editingRectLayout.position.leading += kTextAreaLeadingOffset;
701 editingRectLayout.position.leading +=
702 ([self isShowingQueryRefinementChip]) ? kTextInsetWithChip : kTextInset;
703 editingRectLayout.size.width -= kTextInset + kEditingRectWidthInset;
704 if (IsIPadIdiom()) {
705 if (!IsCompactTablet() && !self.rightView) {
706 // Normally the clear button shrinks the edit box, but if the rightView
707 // isn't set, shrink behind the mic icons.
708 editingRectLayout.size.width -= kVoiceSearchButtonWidth;
709 }
710 } else if (![self isShowingQueryRefinementChip]) {
711 CGFloat xDiff = editingRectLayout.position.leading - kEditingRectX;
712 editingRectLayout.position.leading = kEditingRectX;
713 editingRectLayout.size.width += xDiff;
714 }
715 // Don't let the edit rect extend over the clear button. The right view
716 // is hidden during animations, so fake its width here.
717 if (self.rightViewMode == UITextFieldViewModeNever)
718 editingRectLayout.size.width -= self.rightView.bounds.size.width;
719
720 newBounds = LayoutRectGetRect(editingRectLayout);
721
722 // Position the selection view appropriately.
723 [_selection setFrame:newBounds];
724
725 return newBounds;
726 }
727
728 - (CGRect)rectForDrawTextInRect:(CGRect)rect {
729 // The goal is to always show the most significant part of the hostname
730 // (i.e. the end of the TLD).
731 //
732 // --------------------
733 // www.somereallyreally|longdomainname.com|/path/gets/clipped
734 // --------------------
735 // { clipped prefix } { visible text } { clipped suffix }
736
737 // First find how much (if any) of the scheme/host needs to be clipped so that
738 // the end of the TLD fits in |rect|. Note that if the omnibox is currently
739 // displaying a search query the prefix is not clipped.
740 CGFloat widthOfClippedPrefix = 0;
741 url::Component scheme, host;
742 AutocompleteInput::ParseForEmphasizeComponents(
743 base::SysNSStringToUTF16(self.text), AutocompleteSchemeClassifierImpl(),
744 &scheme, &host);
745 if (host.len < 0) {
746 return rect;
747 }
748 NSRange hostRange = NSMakeRange(0, host.begin + host.len);
749 NSAttributedString* hostString =
750 [self.attributedText attributedSubstringFromRange:hostRange];
751 CGFloat widthOfHost = ceil([hostString size].width);
752 widthOfClippedPrefix = MAX(widthOfHost - rect.size.width, 0);
753
754 // Now determine if there is any text that will need to be truncated because
755 // there's not enough room.
756 int textWidth = ceil([self.attributedText size].width);
757 CGFloat widthOfClippedSuffix =
758 MAX(textWidth - rect.size.width - widthOfClippedPrefix, 0);
759 BOOL suffixClipped = widthOfClippedSuffix > 0;
760
761 // Fade the beginning and/or end of the visible string to indicate to the user
762 // that the URL has been clipped.
763 BOOL prefixClipped = widthOfClippedPrefix > 0;
764 if (prefixClipped || suffixClipped) {
765 UIImage* fade = nil;
766 if ([self textAlignment] == NSTextAlignmentRight) {
767 // Swap prefix and suffix for RTL.
768 fade = [GTMFadeTruncatingLabel getLinearGradient:rect
769 fadeHead:suffixClipped
770 fadeTail:prefixClipped];
771 } else {
772 fade = [GTMFadeTruncatingLabel getLinearGradient:rect
773 fadeHead:prefixClipped
774 fadeTail:suffixClipped];
775 }
776 CGContextClipToMask(UIGraphicsGetCurrentContext(), rect, fade.CGImage);
777 }
778
779 // If necessary, expand the rect so the entire string fits and shift it to the
780 // left (right for RTL) so the clipped prefix is not shown.
781 if ([self textAlignment] == NSTextAlignmentRight) {
782 rect.origin.x -= widthOfClippedSuffix;
783 } else {
784 rect.origin.x -= widthOfClippedPrefix;
785 }
786 rect.size.width = MAX(rect.size.width, textWidth);
787 return rect;
788 }
789
790 // Enumerate url components (host, path) and draw each one in different rect.
791 - (void)drawTextInRect:(CGRect)rect {
792 // Save and restore the graphics state because rectForDrawTextInRect may
793 // apply an image mask to fade out beginning and/or end of the URL.
794 gfx::ScopedCGContextSaveGState saver(UIGraphicsGetCurrentContext());
795 [super drawTextInRect:[self rectForDrawTextInRect:rect]];
796 }
797
798 - (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
799 // Anything in the narrow bar above OmniboxTextFieldIOS view
800 // will also activate the text field.
801 if (point.y < 0)
802 point.y = 0;
803 UIView* view = [super hitTest:point withEvent:event];
804
805 // For some reason when the |leftView| has interaction enabled, hitTest
806 // returns the leftView even when |point| is 50 pixels to the right. Tapping
807 // the hint text will fire the leftView, causing b/6281652. Fails especially
808 // on iPad and iPhone devices in landscape mode.
809 // TODO(crbug.com/546295): Check to see if this UIKit bug is fixed, and remove
810 // this workaround.
811 UIView* leftView = [self leftView];
812 if (leftView) {
813 if (leftView == view && !CGRectContainsPoint([leftView frame], point)) {
814 return self;
815 } else if ([self leftViewMode] == UITextFieldViewModeAlways) {
816 CGRect targetFrame = CGRectInset([leftView frame], -5, -5);
817 if (CGRectContainsPoint(targetFrame, point)) {
818 return leftView;
819 }
820 }
821 }
822 return view;
823 }
824
825 - (BOOL)isTextFieldLTR {
826 return [[self class] userInterfaceLayoutDirectionForSemanticContentAttribute:
827 self.semanticContentAttribute] ==
828 UIUserInterfaceLayoutDirectionLeftToRight;
829 }
830
831 // Overriding this method to offset the rightView property
832 // (containing a clear text button).
833 - (CGRect)rightViewRectForBounds:(CGRect)bounds {
834 // iOS9 added updated RTL support, but only half implemented it for
835 // UITextField. leftView and rightView were not renamed, but are are correctly
836 // swapped and treated as leadingView / trailingView. However,
837 // -leftViewRectForBounds and -rightViewRectForBounds are *not* treated as
838 // leading and trailing. Hence the swapping below.
839 if ([self isTextFieldLTR]) {
840 return [self layoutRightViewForBounds:bounds];
841 }
842 return [self layoutLeftViewForBounds:bounds];
843 }
844
845 - (CGRect)layoutRightViewForBounds:(CGRect)bounds {
846 if ([self rightView]) {
847 CGSize rightViewSize = self.rightView.bounds.size;
848 CGFloat leadingOffset = 0;
849 if (IsIPadIdiom() && !IsCompactTablet()) {
850 leadingOffset = bounds.size.width - kVoiceSearchButtonWidth -
851 rightViewSize.width - kClearButtonRightMarginIpad;
852 } else {
853 leadingOffset = bounds.size.width - rightViewSize.width -
854 kClearButtonRightMarginIphone;
855 }
856 LayoutRect rightViewLayout;
857 rightViewLayout.position.leading = leadingOffset;
858 rightViewLayout.boundingWidth = CGRectGetWidth(bounds);
859 rightViewLayout.position.originY =
860 floor((bounds.size.height - rightViewSize.height) / 2.0);
861 rightViewLayout.size = rightViewSize;
862 return LayoutRectGetRect(rightViewLayout);
863 }
864 return CGRectZero;
865 }
866
867 // Overriding this method to offset the leftView property
868 // (containing a placeholder image) consistently with omnibox text padding.
869 - (CGRect)leftViewRectForBounds:(CGRect)bounds {
870 // iOS9 added updated RTL support, but only half implemented it for
871 // UITextField. leftView and rightView were not renamed, but are are correctly
872 // swapped and treated as leadingView / trailingView. However,
873 // -leftViewRectForBounds and -rightViewRectForBounds are *not* treated as
874 // leading and trailing. Hence the swapping below.
875 if ([self isTextFieldLTR]) {
876 return [self layoutLeftViewForBounds:bounds];
877 }
878 return [self layoutRightViewForBounds:bounds];
879 }
880
881 - (CGRect)layoutLeftViewForBounds:(CGRect)bounds {
882 if ([self leftView]) {
883 CGSize imageSize = [[self leftView] bounds].size;
884 LayoutRect leftViewLayout =
885 LayoutRectMake(kImageInset, CGRectGetWidth(bounds),
886 floor((bounds.size.height - imageSize.height) / 2.0),
887 imageSize.width, imageSize.height);
888 return LayoutRectGetRect(leftViewLayout);
889 }
890 return CGRectZero;
891 }
892
893 - (void)animateFadeWithStyle:(OmniboxTextFieldFadeStyle)style {
894 // Animation values
895 BOOL isFadingIn = (style == OMNIBOX_TEXT_FIELD_FADE_STYLE_IN);
896 CGFloat beginOpacity = isFadingIn ? 0.0 : 1.0;
897 CGFloat endOpacity = isFadingIn ? 1.0 : 0.0;
898 CAMediaTimingFunction* opacityTiming = ios::material::TimingFunction(
899 isFadingIn ? ios::material::CurveEaseOut : ios::material::CurveEaseIn);
900 CFTimeInterval delay = isFadingIn ? ios::material::kDuration8 : 0.0;
901
902 CAAnimation* labelAnimation = OpacityAnimationMake(beginOpacity, endOpacity);
903 labelAnimation.duration =
904 isFadingIn ? ios::material::kDuration6 : ios::material::kDuration8;
905 labelAnimation.timingFunction = opacityTiming;
906 labelAnimation = DelayedAnimationMake(labelAnimation, delay);
907 CAAnimation* auxillaryViewAnimation =
908 OpacityAnimationMake(beginOpacity, endOpacity);
909 auxillaryViewAnimation.duration = ios::material::kDuration8;
910 auxillaryViewAnimation.timingFunction = opacityTiming;
911 auxillaryViewAnimation = DelayedAnimationMake(auxillaryViewAnimation, delay);
912
913 for (UIView* subview in self.subviews) {
914 if ([subview isKindOfClass:[UILabel class]]) {
915 [subview.layer addAnimation:labelAnimation
916 forKey:kOmniboxFadeAnimationKey];
917 } else {
918 [subview.layer addAnimation:auxillaryViewAnimation
919 forKey:kOmniboxFadeAnimationKey];
920 }
921 }
922 }
923
924 - (NSArray*)fadeAnimationLayers {
925 NSMutableArray* layers = [NSMutableArray array];
926 for (UIView* subview in self.subviews)
927 [layers addObject:subview.layer];
928 return layers;
929 }
930
931 - (void)reverseFadeAnimations {
932 ReverseAnimationsForKeyForLayers(kOmniboxFadeAnimationKey,
933 [self fadeAnimationLayers]);
934 }
935
936 - (void)cleanUpFadeAnimations {
937 RemoveAnimationForKeyFromLayers(kOmniboxFadeAnimationKey,
938 [self fadeAnimationLayers]);
939 }
940
941 #pragma mark - Placeholder image handling methods.
942
943 - (void)setPlaceholderImage:(int)imageId {
944 _leftViewImageId = imageId;
945 [self updateLeftView];
946 }
947
948 - (void)showPlaceholderImage {
949 [self setLeftViewMode:UITextFieldViewModeAlways];
950 }
951
952 - (void)hidePlaceholderImage {
953 [self setLeftViewMode:UITextFieldViewModeNever];
954 }
955
956 #pragma mark - Copy/Paste
957
958 // Overridden to allow for custom omnibox copy behavior. This includes
959 // preprending http:// to the copied URL if needed.
960 - (void)copy:(id)sender {
961 id<OmniboxTextFieldDelegate> delegate = [self delegate];
962 BOOL handled = NO;
963
964 // Must test for the onCopy method, since it's optional.
965 if ([delegate respondsToSelector:@selector(onCopy)])
966 handled = [delegate onCopy];
967
968 // iOS 4 doesn't expose an API that allows the delegate to handle the copy
969 // operation, so let the superclass perform the copy if the delegate couldn't.
970 if (!handled)
971 [super copy:sender];
972 }
973
974 // Overridden to notify the delegate that a paste is in progress.
975 - (void)paste:(id)sender {
976 id delegate = [self delegate];
977 if ([delegate respondsToSelector:@selector(willPaste)])
978 [delegate willPaste];
979 [super paste:sender];
980 }
981
982 - (NSRange)selectedNSRange {
983 DCHECK([self isFirstResponder]);
984 UITextPosition* beginning = [self beginningOfDocument];
985 UITextRange* selectedRange = [self selectedTextRange];
986 NSInteger start =
987 [self offsetFromPosition:beginning toPosition:[selectedRange start]];
988 NSInteger length = [self offsetFromPosition:[selectedRange start]
989 toPosition:[selectedRange end]];
990 return NSMakeRange(start, length);
991 }
992
993 - (BOOL)becomeFirstResponder {
994 if (![super becomeFirstResponder])
995 return NO;
996
997 if (!_copyUrlMenuItem.get()) {
998 NSString* const kTitle = l10n_util::GetNSString(IDS_IOS_COPY_URL);
999 _copyUrlMenuItem.reset(
1000 [[UIMenuItem alloc] initWithTitle:kTitle action:@selector(copyUrl:)]);
1001 }
1002
1003 // Add the "Copy URL" menu item to the |sharedMenuController| if necessary.
1004 UIMenuController* menuController = [UIMenuController sharedMenuController];
1005 if (menuController.menuItems) {
1006 if (![menuController.menuItems containsObject:_copyUrlMenuItem]) {
1007 menuController.menuItems =
1008 [menuController.menuItems arrayByAddingObject:_copyUrlMenuItem];
1009 }
1010 } else {
1011 menuController.menuItems = [NSArray arrayWithObject:_copyUrlMenuItem];
1012 }
1013 return YES;
1014 }
1015
1016 - (BOOL)resignFirstResponder {
1017 if (![super resignFirstResponder])
1018 return NO;
1019
1020 // Remove the "Copy URL" menu item from the |sharedMenuController|.
1021 UIMenuController* menuController = [UIMenuController sharedMenuController];
1022 NSMutableArray* menuItems =
1023 [NSMutableArray arrayWithArray:menuController.menuItems];
1024 [menuItems removeObject:_copyUrlMenuItem];
1025 menuController.menuItems = menuItems;
1026 return YES;
1027 }
1028
1029 - (void)copyUrl:(id)sender {
1030 [[self delegate] onCopyURL];
1031 }
1032
1033 - (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
1034 if (action == @selector(copyUrl:)) {
1035 return [[self delegate] canCopyURL];
1036 }
1037
1038 // Disable the "Define" menu item. iOS7 implements this with a private
1039 // selector. Avoid using private APIs by instead doing a string comparison.
1040 if ([NSStringFromSelector(action) hasSuffix:@"define:"]) {
1041 return NO;
1042 }
1043
1044 // Disable the RTL arrow menu item. The omnibox sets alignment based on the
1045 // text in the field, and should not be overridden.
1046 if ([NSStringFromSelector(action) hasPrefix:@"makeTextWritingDirection"]) {
1047 return NO;
1048 }
1049
1050 return [super canPerformAction:action withSender:sender];
1051 }
1052
1053 @end
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698