| // Copyright (c) 2012 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 "ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.h" |
| |
| #import <CoreText/CoreText.h> |
| |
| #include "base/command_line.h" |
| #include "base/ios/ios_util.h" |
| #include "base/logging.h" |
| #include "base/mac/foundation_util.h" |
| |
| #include "base/strings/sys_string_conversions.h" |
| #include "components/grit/components_scaled_resources.h" |
| #include "components/omnibox/browser/autocomplete_input.h" |
| #include "ios/chrome/browser/application_context.h" |
| #include "ios/chrome/browser/autocomplete/autocomplete_scheme_classifier_impl.h" |
| #include "ios/chrome/browser/experimental_flags.h" |
| #import "ios/chrome/browser/ui/animation_util.h" |
| #include "ios/chrome/browser/ui/omnibox/omnibox_util.h" |
| #import "ios/chrome/browser/ui/reversed_animation.h" |
| #include "ios/chrome/browser/ui/rtl_geometry.h" |
| #import "ios/chrome/browser/ui/toolbar/buttons/toolbar_constants.h" |
| #import "ios/chrome/browser/ui/toolbar/public/features.h" |
| #include "ios/chrome/browser/ui/ui_util.h" |
| #import "ios/chrome/browser/ui/uikit_ui_util.h" |
| #import "ios/chrome/common/material_timing.h" |
| #include "ios/chrome/grit/ios_strings.h" |
| #include "ios/chrome/grit/ios_theme_resources.h" |
| #include "skia/ext/skia_utils_ios.h" |
| #include "third_party/google_toolbox_for_mac/src/iPhone/GTMFadeTruncatingLabel.h" |
| #include "ui/base/l10n/l10n_util_mac.h" |
| #include "ui/gfx/color_palette.h" |
| #include "ui/gfx/image/image.h" |
| #import "ui/gfx/ios/NSString+CrStringDrawing.h" |
| #include "ui/gfx/scoped_cg_context_save_gstate_mac.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| namespace { |
| |
| const CGFloat kFontSize = 16; |
| const CGFloat kEditingRectWidthInset = 12; |
| const CGFloat kClearButtonRightMarginIphone = 7; |
| |
| const CGFloat kVoiceSearchButtonWidth = 36.0; |
| |
| // When rendering the same string in a UITextField and a UILabel with the same |
| // frame and the same font, the text is slightly offset. |
| const CGFloat kUILabelUITextfieldBaselineDeltaInPoints = 1.0; |
| const CGFloat kUILabelUITextfieldBaselineDeltaIpadIOS10InPixels = 1.0; |
| |
| // The default omnibox text color (used while editing). |
| UIColor* TextColor() { |
| return [UIColor colorWithWhite:(51 / 255.0) alpha:1.0]; |
| } |
| |
| NSString* const kOmniboxFadeAnimationKey = @"OmniboxFadeAnimation"; |
| |
| } // namespace |
| |
| @interface OmniboxTextFieldIOS () |
| |
| // Font to use in regular x regular size class. If not set, the regular font is |
| // used instead. |
| @property(nonatomic, strong) UIFont* largerFont; |
| // Font to use in Compact x Any and Any x Compact size class. |
| @property(nonatomic, strong) UIFont* normalFont; |
| |
| // Gets the bounds of the rect covering the URL. |
| - (CGRect)preEditLabelRectForBounds:(CGRect)bounds; |
| // Creates the UILabel if it doesn't already exist and adds it as a |
| // subview. |
| - (void)createSelectionViewIfNecessary; |
| // Helper method used to set the text of this field. Updates the selection view |
| // to contain the correct inline autocomplete text. |
| - (void)setTextInternal:(NSAttributedString*)text |
| autocompleteLength:(NSUInteger)autocompleteLength; |
| // Override deleteBackward so that backspace can clear query refinement chips. |
| - (void)deleteBackward; |
| // Returns the layers affected by animations added by |-animateFadeWithStyle:|. |
| - (NSArray*)fadeAnimationLayers; |
| // Returns the text that is displayed in the field, including any inline |
| // autocomplete text that may be present as an NSString. Returns the same |
| // value as -|displayedText| but prefer to use this to avoid unnecessary |
| // conversion from NSString to base::string16 if possible. |
| - (NSString*)nsDisplayedText; |
| // Font that should be used in current size class. |
| - (UIFont*)currentFont; |
| |
| @end |
| |
| @implementation OmniboxTextFieldIOS { |
| UILabel* _selection; |
| UILabel* _preEditStaticLabel; |
| UIColor* _displayedTextColor; |
| UIColor* _displayedTintColor; |
| } |
| |
| @synthesize preEditText = _preEditText; |
| @synthesize clearingPreEditText = _clearingPreEditText; |
| @synthesize selectedTextBackgroundColor = _selectedTextBackgroundColor; |
| @synthesize placeholderTextColor = _placeholderTextColor; |
| @synthesize incognito = _incognito; |
| @synthesize largerFont = _largerFont; |
| @synthesize normalFont = _normalFont; |
| @synthesize suggestionCommandsEndpoint = _suggestionCommandsEndpoint; |
| |
| #pragma mark - Public methods |
| // Overload to allow for code-based initialization. |
| - (instancetype)initWithFrame:(CGRect)frame { |
| return [self initWithFrame:frame |
| font:[UIFont systemFontOfSize:kFontSize] |
| textColor:TextColor() |
| tintColor:nil]; |
| } |
| |
| - (instancetype)initWithFrame:(CGRect)frame |
| font:(UIFont*)font |
| largerFont:(UIFont*)largerFont |
| textColor:(UIColor*)textColor |
| tintColor:(UIColor*)tintColor { |
| self = [self initWithFrame:frame |
| font:font |
| textColor:textColor |
| tintColor:tintColor]; |
| _largerFont = largerFont; |
| return self; |
| } |
| |
| - (instancetype)initWithFrame:(CGRect)frame |
| font:(UIFont*)font |
| textColor:(UIColor*)textColor |
| tintColor:(UIColor*)tintColor { |
| self = [super initWithFrame:frame]; |
| if (self) { |
| _normalFont = font; |
| _displayedTextColor = textColor; |
| if (tintColor) { |
| [self setTintColor:tintColor]; |
| _displayedTintColor = tintColor; |
| } else { |
| _displayedTintColor = self.tintColor; |
| } |
| [self setTextColor:_displayedTextColor]; |
| [self setAutocorrectionType:UITextAutocorrectionTypeNo]; |
| [self setAutocapitalizationType:UITextAutocapitalizationTypeNone]; |
| [self setEnablesReturnKeyAutomatically:YES]; |
| [self setReturnKeyType:UIReturnKeyGo]; |
| [self setContentVerticalAlignment:UIControlContentVerticalAlignmentCenter]; |
| [self setSpellCheckingType:UITextSpellCheckingTypeNo]; |
| [self setTextAlignment:NSTextAlignmentNatural]; |
| [self setKeyboardType:(UIKeyboardType)UIKeyboardTypeWebSearch]; |
| |
| if (IsRefreshLocationBarEnabled()) { |
| // The right view mode is managed by the view controller. |
| } else { |
| [self setClearButtonMode:UITextFieldViewModeNever]; |
| [self setRightViewMode:UITextFieldViewModeAlways]; |
| } |
| |
| #if defined(__IPHONE_11_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0) |
| if (@available(iOS 11.0, *)) { |
| [self setSmartQuotesType:UITextSmartQuotesTypeNo]; |
| } |
| #endif |
| |
| // Sanity check: |
| DCHECK([self conformsToProtocol:@protocol(UITextInput)]); |
| |
| // Force initial layout of internal text label. Needed for omnibox |
| // animations that will otherwise animate the text label from origin {0, 0}. |
| [super setText:@" "]; |
| } |
| return self; |
| } |
| |
| - (instancetype)initWithCoder:(nonnull NSCoder*)aDecoder { |
| NOTREACHED(); |
| return nil; |
| } |
| |
| - (void)setText:(NSAttributedString*)text |
| userTextLength:(size_t)userTextLength { |
| DCHECK_LE(userTextLength, [text length]); |
| |
| NSUInteger autocompleteLength = [text length] - userTextLength; |
| [self setTextInternal:text autocompleteLength:autocompleteLength]; |
| } |
| |
| - (void)insertTextWhileEditing:(NSString*)text { |
| // This method should only be called while editing. |
| DCHECK([self isFirstResponder]); |
| |
| if ([self markedTextRange] != nil) |
| [self unmarkText]; |
| |
| NSRange selectedNSRange = [self selectedNSRange]; |
| if (![self delegate] || [[self delegate] textField:self |
| shouldChangeCharactersInRange:selectedNSRange |
| replacementString:text]) { |
| [self replaceRange:[self selectedTextRange] withText:text]; |
| } |
| } |
| |
| - (base::string16)displayedText { |
| return base::SysNSStringToUTF16([self nsDisplayedText]); |
| } |
| |
| - (base::string16)autocompleteText { |
| DCHECK_LT([[self text] length], [[_selection text] length]) |
| << "[_selection text] and [self text] are out of sync. " |
| << "Please email justincohen@ and rohitrao@ if you see this."; |
| if (_selection && [[_selection text] length] > [[self text] length]) { |
| return base::SysNSStringToUTF16( |
| [[_selection text] substringFromIndex:[[self text] length]]); |
| } |
| return base::string16(); |
| } |
| |
| - (BOOL)hasAutocompleteText { |
| return !!_selection; |
| } |
| |
| - (void)clearAutocompleteText { |
| if (_selection) { |
| [_selection removeFromSuperview]; |
| _selection = nil; |
| [self showTextAndCursor]; |
| } |
| } |
| |
| - (NSString*)markedText { |
| DCHECK([self conformsToProtocol:@protocol(UITextInput)]); |
| return [self textInRange:[self markedTextRange]]; |
| } |
| |
| - (NSRange)selectedNSRange { |
| DCHECK([self isFirstResponder]); |
| UITextPosition* beginning = [self beginningOfDocument]; |
| UITextRange* selectedRange = [self selectedTextRange]; |
| NSInteger start = |
| [self offsetFromPosition:beginning toPosition:[selectedRange start]]; |
| NSInteger length = [self offsetFromPosition:[selectedRange start] |
| toPosition:[selectedRange end]]; |
| return NSMakeRange(start, length); |
| } |
| |
| - (NSTextAlignment)bestTextAlignment { |
| if ([self isFirstResponder]) { |
| return [self bestAlignmentForText:[self text]]; |
| } |
| return NSTextAlignmentNatural; |
| } |
| |
| // Normally NSTextAlignmentNatural would handle text alignment automatically, |
| // but there are numerous edge case issues with it, so it's simpler to just |
| // manually update the text alignment and writing direction of the UITextField. |
| - (void)updateTextDirection { |
| // Setting the empty field to Natural seems to let iOS update the cursor |
| // position when the keyboard language is changed. |
| if (![self text].length) { |
| [self setTextAlignment:NSTextAlignmentNatural]; |
| return; |
| } |
| |
| NSTextAlignment alignment = [self bestTextAlignment]; |
| [self setTextAlignment:alignment]; |
| if (!base::ios::IsRunningOnIOS11OrLater()) { |
| // TODO(crbug.com/730461): Remove this entire block once it's been tested |
| // on trunk. |
| UITextWritingDirection writingDirection = |
| alignment == NSTextAlignmentLeft ? UITextWritingDirectionLeftToRight |
| : UITextWritingDirectionRightToLeft; |
| [self |
| setBaseWritingDirection:writingDirection |
| forRange: |
| [self |
| textRangeFromPosition:[self beginningOfDocument] |
| toPosition:[self endOfDocument]]]; |
| } |
| } |
| |
| - (UIColor*)displayedTextColor { |
| return _displayedTextColor; |
| } |
| |
| #pragma mark animations |
| |
| - (void)animateFadeWithStyle:(OmniboxTextFieldFadeStyle)style { |
| // Animation values |
| BOOL isFadingIn = (style == OMNIBOX_TEXT_FIELD_FADE_STYLE_IN); |
| CGFloat beginOpacity = isFadingIn ? 0.0 : 1.0; |
| CGFloat endOpacity = isFadingIn ? 1.0 : 0.0; |
| CAMediaTimingFunction* opacityTiming = ios::material::TimingFunction( |
| isFadingIn ? ios::material::CurveEaseOut : ios::material::CurveEaseIn); |
| CFTimeInterval delay = isFadingIn ? ios::material::kDuration8 : 0.0; |
| |
| CAAnimation* labelAnimation = OpacityAnimationMake(beginOpacity, endOpacity); |
| labelAnimation.duration = |
| isFadingIn ? ios::material::kDuration6 : ios::material::kDuration8; |
| labelAnimation.timingFunction = opacityTiming; |
| labelAnimation = DelayedAnimationMake(labelAnimation, delay); |
| CAAnimation* auxillaryViewAnimation = |
| OpacityAnimationMake(beginOpacity, endOpacity); |
| auxillaryViewAnimation.duration = ios::material::kDuration8; |
| auxillaryViewAnimation.timingFunction = opacityTiming; |
| auxillaryViewAnimation = DelayedAnimationMake(auxillaryViewAnimation, delay); |
| |
| for (UIView* subview in self.subviews) { |
| if ([subview isKindOfClass:[UILabel class]]) { |
| [subview.layer addAnimation:labelAnimation |
| forKey:kOmniboxFadeAnimationKey]; |
| } else { |
| [subview.layer addAnimation:auxillaryViewAnimation |
| forKey:kOmniboxFadeAnimationKey]; |
| } |
| } |
| } |
| |
| - (void)cleanUpFadeAnimations { |
| RemoveAnimationForKeyFromLayers(kOmniboxFadeAnimationKey, |
| [self fadeAnimationLayers]); |
| } |
| |
| - (void)addExpandOmniboxAnimations:(UIViewPropertyAnimator*)animator |
| completionAnimator:(UIViewPropertyAnimator*)completionAnimator { |
| DCHECK(!IsRefreshLocationBarEnabled()); |
| |
| // Hide the rightView button so it's not visible on its initial layout |
| // while the expand animation is happening. |
| self.clearButtonView.hidden = YES; |
| self.clearButtonView.alpha = 0; |
| self.clearButtonView.frame = |
| CGRectLayoutOffset([self rightViewRectForBounds:self.bounds], |
| [self clearButtonAnimationOffset]); |
| |
| [completionAnimator addAnimations:^{ |
| self.clearButtonView.hidden = NO; |
| self.clearButtonView.alpha = 1.0; |
| |
| self.clearButtonView.frame = CGRectLayoutOffset( |
| self.clearButtonView.frame, -[self clearButtonAnimationOffset]); |
| }]; |
| } |
| |
| - (void)addContractOmniboxAnimations:(UIViewPropertyAnimator*)animator { |
| DCHECK(!IsRefreshLocationBarEnabled()); |
| |
| [animator addAnimations:^{ |
| self.clearButtonView.alpha = 0; |
| self.clearButtonView.frame = CGRectLayoutOffset( |
| self.clearButtonView.frame, kToolbarButtonAnimationOffset); |
| }]; |
| [animator addCompletion:^(UIViewAnimatingPosition finalPosition) { |
| [self resetClearButton]; |
| }]; |
| } |
| |
| #pragma mark - UI Refresh animation public helpers |
| |
| - (CGFloat)offsetForString:(NSString*)string { |
| // Sometimes |string| is not contained in self.text, for example for |
| // https://en.m.wikipedia.org/foo the |string| might be "en.wikipedia.org" if |
| // the scheme and the "m." trivial subdomain are stripped. In this case, |
| // default to a reasonable prefix string to give a plausible offset. |
| NSString* prefixString = @"https://"; |
| |
| if ([self.text containsString:string]) { |
| NSRange range = [self.text rangeOfString:string]; |
| NSRange prefixRange = NSMakeRange(0, range.location); |
| prefixString = [self.text substringWithRange:prefixRange]; |
| } |
| |
| return [prefixString |
| sizeWithAttributes:@{NSFontAttributeName : self.currentFont}] |
| .width + |
| self.preEditStaticLabel.frame.origin.x; |
| } |
| |
| #pragma mark pre-edit |
| |
| // Creates a UILabel based on the current dimension of the text field and |
| // displays the URL in the UILabel so it appears properly aligned to the URL. |
| - (void)enterPreEditState { |
| // Empty omnibox should show the insertion point immediately. There is |
| // nothing to erase. |
| if (!self.text.length || UIAccessibilityIsVoiceOverRunning()) |
| return; |
| |
| // Remembers the initial text input to compute the diff of what was there |
| // and what was typed. |
| [self setPreEditText:self.text]; |
| |
| // Adjusts the placement so static URL lines up perfectly with UITextField. |
| DCHECK(!_preEditStaticLabel); |
| CGRect rect = [self preEditLabelRectForBounds:self.bounds]; |
| _preEditStaticLabel = [[UILabel alloc] initWithFrame:rect]; |
| _preEditStaticLabel.backgroundColor = [UIColor clearColor]; |
| _preEditStaticLabel.opaque = YES; |
| _preEditStaticLabel.font = self.currentFont; |
| _preEditStaticLabel.textColor = _displayedTextColor; |
| _preEditStaticLabel.lineBreakMode = NSLineBreakByTruncatingHead; |
| |
| NSMutableParagraphStyle* style = [[NSMutableParagraphStyle alloc] init]; |
| // URLs have their text direction set to to LTR (avoids RTL characters |
| // making the URL render from right to left, as per the URL rendering standard |
| // described here: https://url.spec.whatwg.org/#url-rendering |
| [style setBaseWritingDirection:NSWritingDirectionLeftToRight]; |
| NSDictionary* attributes = @{ |
| NSBackgroundColorAttributeName : [self selectedTextBackgroundColor], |
| NSParagraphStyleAttributeName : style |
| }; |
| |
| NSAttributedString* preEditString = |
| [[NSAttributedString alloc] initWithString:self.text |
| attributes:attributes]; |
| [_preEditStaticLabel setAttributedText:preEditString]; |
| _preEditStaticLabel.textAlignment = [self preEditTextAlignment]; |
| [self addSubview:_preEditStaticLabel]; |
| } |
| |
| // Finishes pre-edit state by removing the UILabel with the URL. |
| - (void)exitPreEditState { |
| [self setPreEditText:nil]; |
| if (_preEditStaticLabel) { |
| [_preEditStaticLabel removeFromSuperview]; |
| _preEditStaticLabel = nil; |
| [self showTextAndCursor]; |
| } |
| } |
| |
| // Returns whether we are processing the first touch event on the text field. |
| - (BOOL)isPreEditing { |
| return !![self preEditText]; |
| } |
| |
| #pragma mark - TestingUtilities category |
| |
| // Exposed for testing. |
| - (UILabel*)preEditStaticLabel { |
| return _preEditStaticLabel; |
| } |
| |
| #pragma mark - Properties |
| |
| // Enforces that the delegate is an OmniboxTextFieldDelegate. |
| - (id<OmniboxTextFieldDelegate>)delegate { |
| id delegate = [super delegate]; |
| DCHECK(delegate == nil || |
| [[delegate class] |
| conformsToProtocol:@protocol(OmniboxTextFieldDelegate)]); |
| return delegate; |
| } |
| |
| // Overridden to require an OmniboxTextFieldDelegate. |
| - (void)setDelegate:(id<OmniboxTextFieldDelegate>)delegate { |
| [super setDelegate:delegate]; |
| } |
| |
| - (UIFont*)currentFont { |
| if (!self.largerFont) { |
| return self.normalFont; |
| } |
| |
| return IsCompactWidth() ? self.normalFont : self.largerFont; |
| } |
| |
| #pragma mark - Private methods |
| |
| #pragma mark - UITextField |
| |
| // Ensures that attributedText always uses the proper style attributes. |
| - (void)setAttributedText:(NSAttributedString*)attributedText { |
| NSMutableAttributedString* mutableText = [attributedText mutableCopy]; |
| NSRange entireString = NSMakeRange(0, [mutableText length]); |
| |
| // Set the font. |
| [mutableText addAttribute:NSFontAttributeName |
| value:self.currentFont |
| range:entireString]; |
| |
| // When editing, use the default text color for all text. |
| if (self.editing) { |
| // Hide the text when the |_selection| label is displayed. |
| UIColor* textColor = |
| _selection ? [UIColor clearColor] : _displayedTextColor; |
| [mutableText addAttribute:NSForegroundColorAttributeName |
| value:textColor |
| range:entireString]; |
| } else { |
| NSMutableParagraphStyle* style = [[NSMutableParagraphStyle alloc] init]; |
| // URLs have their text direction set to to LTR (avoids RTL characters |
| // making the URL render from right to left, as per the URL rendering |
| // standard described here: https://url.spec.whatwg.org/#url-rendering |
| [style setBaseWritingDirection:NSWritingDirectionLeftToRight]; |
| |
| // Set linebreak mode to 'clipping' to ensure the text is never elided. |
| // This is a workaround for iOS 6, where it appears that |
| // [self.attributedText size] is not wide enough for the string (e.g. a URL |
| // else ending with '.com' will be elided to end with '.c...'). It appears |
| // to be off by one point so clipping is acceptable as it doesn't actually |
| // cut off any of the text. |
| [style setLineBreakMode:NSLineBreakByClipping]; |
| |
| [mutableText addAttribute:NSParagraphStyleAttributeName |
| value:style |
| range:entireString]; |
| } |
| |
| [super setAttributedText:mutableText]; |
| } |
| |
| - (void)setPlaceholder:(NSString*)placeholder { |
| if (placeholder && _placeholderTextColor) { |
| NSDictionary* attributes = |
| @{NSForegroundColorAttributeName : _placeholderTextColor}; |
| self.attributedPlaceholder = |
| [[NSAttributedString alloc] initWithString:placeholder |
| attributes:attributes]; |
| } else { |
| [super setPlaceholder:placeholder]; |
| } |
| } |
| |
| - (void)setText:(NSString*)text { |
| NSAttributedString* as = [[NSAttributedString alloc] initWithString:text]; |
| if (self.text.length > 0 && as.length == 0) { |
| // Remove the fade animations before the subviews are removed. |
| [self cleanUpFadeAnimations]; |
| } |
| [self setTextInternal:as autocompleteLength:0]; |
| } |
| |
| - (CGRect)textRectForBounds:(CGRect)bounds { |
| CGRect newBounds = [super textRectForBounds:bounds]; |
| |
| LayoutRect textRectLayout = |
| LayoutRectForRectInBoundingRect(newBounds, bounds); |
| |
| return LayoutRectGetRect(textRectLayout); |
| } |
| |
| - (CGRect)editingRectForBounds:(CGRect)bounds { |
| CGRect superBounds = [super editingRectForBounds:bounds]; |
| CGRect newBounds = [self adjustedEditingRectForBounds:superBounds]; |
| [self layoutSelectionViewWithNewEditingRectBounds:newBounds]; |
| return newBounds; |
| } |
| |
| // Overriding this method to offset the rightView property |
| // (containing a clear text button). |
| - (CGRect)rightViewRectForBounds:(CGRect)bounds { |
| if (IsRefreshLocationBarEnabled()) { |
| return [super rightViewRectForBounds:bounds]; |
| } |
| |
| // iOS9 added updated RTL support, but only half implemented it for |
| // UITextField. leftView and rightView were not renamed, but are are correctly |
| // swapped and treated as leadingView / trailingView. However, |
| // -leftViewRectForBounds and -rightViewRectForBounds are *not* treated as |
| // leading and trailing. Hence the swapping below. |
| if ([self isTextFieldLTR]) { |
| return [self layoutRightViewForBounds:bounds]; |
| } |
| return [self layoutLeftViewForBounds:bounds]; |
| } |
| |
| // Overriding this method to offset the leftView property |
| // (containing a placeholder image) consistently with omnibox text padding. |
| - (CGRect)leftViewRectForBounds:(CGRect)bounds { |
| if (IsRefreshLocationBarEnabled()) { |
| return [super leftViewRectForBounds:bounds]; |
| } |
| |
| // iOS9 added updated RTL support, but only half implemented it for |
| // UITextField. leftView and rightView were not renamed, but are correctly |
| // swapped and treated as leadingView / trailingView. However, |
| // -leftViewRectForBounds and -rightViewRectForBounds are *not* treated as |
| // leading and trailing. Hence the swapping below. |
| if ([self isTextFieldLTR]) { |
| return [self layoutLeftViewForBounds:bounds]; |
| } |
| return [self layoutRightViewForBounds:bounds]; |
| } |
| |
| #pragma mark - UITextInput |
| |
| - (void)beginFloatingCursorAtPoint:(CGPoint)point { |
| // Exit preedit because it blocks the view of the textfield. |
| [self exitPreEditState]; |
| [super beginFloatingCursorAtPoint:point]; |
| } |
| |
| #pragma mark - UIView |
| |
| - (void)layoutSubviews { |
| [super layoutSubviews]; |
| if ([self isPreEditing]) { |
| CGRect rect = [self preEditLabelRectForBounds:self.bounds]; |
| [_preEditStaticLabel setFrame:rect]; |
| |
| // Update text alignment since the pre-edit label's frame changed. |
| _preEditStaticLabel.textAlignment = [self preEditTextAlignment]; |
| [self hideTextAndCursor]; |
| } else if (!_selection) { |
| [self showTextAndCursor]; |
| } |
| |
| if (_selection) { |
| // Trigger a layout of _selection label. |
| CGRect superBounds = [super editingRectForBounds:self.bounds]; |
| CGRect newBounds = [self adjustedEditingRectForBounds:superBounds]; |
| [self layoutSelectionViewWithNewEditingRectBounds:newBounds]; |
| } |
| } |
| |
| - (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event { |
| // Anything in the narrow bar above OmniboxTextFieldIOS view |
| // will also activate the text field. |
| if (point.y < 0) |
| point.y = 0; |
| return [super hitTest:point withEvent:event]; |
| } |
| |
| #pragma mark - UITraitCollection |
| |
| - (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection { |
| [super traitCollectionDidChange:previousTraitCollection]; |
| |
| // Reset the fonts to the appropriate ones in this size class. |
| [self setFont:self.currentFont]; |
| // Reset the attributed text to apply the new font. |
| [self setAttributedText:self.attributedText]; |
| if (_selection) { |
| _selection.font = self.currentFont; |
| } |
| if (_preEditStaticLabel) { |
| _preEditStaticLabel.font = self.currentFont; |
| } |
| } |
| |
| #pragma mark - UIResponder |
| |
| // Method called when the users touches the text input. This will accept the |
| // autocompleted text. |
| - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { |
| if ([self isPreEditing]) { |
| [self exitPreEditState]; |
| [super selectAll:nil]; |
| } |
| |
| if (!_selection) { |
| [super touchesBegan:touches withEvent:event]; |
| return; |
| } |
| |
| // Only consider a single touch. |
| UITouch* touch = [touches anyObject]; |
| if (!touch) |
| return; |
| |
| // Accept selection. |
| NSString* newText = [[self nsDisplayedText] copy]; |
| [self clearAutocompleteText]; |
| [self setText:newText]; |
| } |
| |
| - (void)select:(id)sender { |
| if ([self isPreEditing]) { |
| [self exitPreEditState]; |
| } |
| [super select:sender]; |
| } |
| |
| - (void)selectAll:(id)sender { |
| if ([self isPreEditing]) { |
| [self exitPreEditState]; |
| } |
| if (_selection) { |
| NSString* newText = [[self nsDisplayedText] copy]; |
| [self clearAutocompleteText]; |
| [self setText:newText]; |
| } |
| [super selectAll:sender]; |
| } |
| |
| - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { |
| // If there is selected text, show copy and cut. |
| if ([self textInRange:self.selectedTextRange].length > 0 && |
| (action == @selector(cut:) || action == @selector(copy:))) { |
| return YES; |
| } |
| |
| // If there is no selected text, show select and selectAll. |
| if ([self textInRange:self.selectedTextRange].length == 0 && |
| (action == @selector(select:) || action == @selector(selectAll:))) { |
| return YES; |
| } |
| |
| // If there is pasteboard content, show paste. |
| if (UIPasteboard.generalPasteboard.string.length > 0 && action == @selector |
| (paste:)) { |
| return YES; |
| } |
| |
| // Note that this NO does not keep other elements in the responder chain from |
| // adding actions they handle to the menu. |
| // No special handling is necessary for pre-edit and autocomplete states. |
| // In pre-edit, the text in the textfield is selected even though it is not |
| // shown. so the behavior is correct. As an aside, the only way to access the |
| // editing menu without exiting the pre-edit state is via accessibility |
| // features. For inline autocomplete, any action on the textfield first |
| // accepts the autocompletion and unselects the text. It is therefore not |
| // possible to open the editing menu in this state. |
| return NO; |
| } |
| |
| #pragma mark Copy/Paste |
| |
| // Overridden to allow for custom omnibox copy behavior. This includes |
| // preprending http:// to the copied URL if needed. |
| - (void)copy:(id)sender { |
| id<OmniboxTextFieldDelegate> delegate = [self delegate]; |
| BOOL handled = NO; |
| |
| // Must test for the onCopy method, since it's optional. |
| if ([delegate respondsToSelector:@selector(onCopy)]) |
| handled = [delegate onCopy]; |
| |
| // iOS 4 doesn't expose an API that allows the delegate to handle the copy |
| // operation, so let the superclass perform the copy if the delegate couldn't. |
| if (!handled) |
| [super copy:sender]; |
| } |
| |
| - (void)cut:(id)sender { |
| if ([self isPreEditing]) { |
| [self copy:sender]; |
| [self exitPreEditState]; |
| NSAttributedString* emptyString = [[NSAttributedString alloc] init]; |
| [self setText:emptyString userTextLength:0]; |
| } else { |
| [super cut:sender]; |
| } |
| } |
| |
| // Overridden to notify the delegate that a paste is in progress. |
| - (void)paste:(id)sender { |
| id delegate = [self delegate]; |
| if ([delegate respondsToSelector:@selector(willPaste)]) |
| [delegate willPaste]; |
| [super paste:sender]; |
| } |
| |
| #pragma mark UIKeyInput |
| |
| - (void)deleteBackward { |
| // Must test for the onDeleteBackward method, since it's optional. |
| if ([[self delegate] respondsToSelector:@selector(onDeleteBackward)]) |
| [[self delegate] onDeleteBackward]; |
| [super deleteBackward]; |
| } |
| |
| #pragma mark Key Commands |
| |
| - (NSArray<UIKeyCommand*>*)upDownCommands { |
| // These up/down arrow key commands override the standard UITextInput handling |
| // of up/down arrow key. The standard behavior is to go to the beginning/end |
| // of the text. Instead, the omnibox popup needs to highlight suggestions. |
| UIKeyCommand* commandUp = |
| [UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow |
| modifierFlags:0 |
| action:@selector(keyCommandUp)]; |
| UIKeyCommand* commandDown = |
| [UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow |
| modifierFlags:0 |
| action:@selector(keyCommandDown)]; |
| |
| return @[ commandUp, commandDown ]; |
| } |
| |
| - (NSArray<UIKeyCommand*>*)keyCommands { |
| NSMutableArray<UIKeyCommand*>* commands = [[self upDownCommands] mutableCopy]; |
| if ([self isPreEditing] || [self hasAutocompleteText]) { |
| [commands addObjectsFromArray:[self leftRightCommands]]; |
| } |
| |
| return commands; |
| } |
| |
| - (void)keyCommandUp { |
| [self.suggestionCommandsEndpoint highlightNextSuggestion]; |
| } |
| |
| - (void)keyCommandDown { |
| [self.suggestionCommandsEndpoint highlightPreviousSuggestion]; |
| } |
| |
| #pragma mark preedit and inline autocomplete key commands |
| |
| // React to left and right keys when in preedit state to exit preedit and put |
| // cursor to the beginning/end of the textfield; or if there is inline |
| // suggestion displayed, accept it and put the cursor before/after the |
| // suggested text. |
| - (NSArray<UIKeyCommand*>*)leftRightCommands { |
| UIKeyCommand* commandLeft = |
| [UIKeyCommand keyCommandWithInput:UIKeyInputLeftArrow |
| modifierFlags:0 |
| action:@selector(keyCommandLeft)]; |
| UIKeyCommand* commandRight = |
| [UIKeyCommand keyCommandWithInput:UIKeyInputRightArrow |
| modifierFlags:0 |
| action:@selector(keyCommandRight)]; |
| |
| return @[ commandLeft, commandRight ]; |
| } |
| |
| - (void)keyCommandLeft { |
| DCHECK([self isPreEditing] || [self hasAutocompleteText]); |
| |
| // Cursor offset. |
| NSInteger offset = 0; |
| if ([self isPreEditing]) { |
| [self exitPreEditState]; |
| } |
| |
| if ([self hasAutocompleteText]) { |
| // The cursor should stay in the end of the user input. |
| offset = self.text.length; |
| |
| // Accept autocomplete suggestion. |
| [self acceptAutocompleteText]; |
| } |
| |
| UITextPosition* beginning = self.beginningOfDocument; |
| UITextPosition* cursorPosition = |
| [self positionFromPosition:beginning offset:offset]; |
| UITextRange* textRange = |
| [self textRangeFromPosition:cursorPosition toPosition:cursorPosition]; |
| self.selectedTextRange = textRange; |
| } |
| |
| - (void)keyCommandRight { |
| DCHECK([self isPreEditing] || [self hasAutocompleteText]); |
| |
| if ([self isPreEditing]) { |
| [self exitPreEditState]; |
| } |
| |
| if ([self hasAutocompleteText]) { |
| [self acceptAutocompleteText]; |
| } |
| |
| // Put the cursor to the end of the input. |
| UITextPosition* end = self.endOfDocument; |
| UITextRange* textRange = [self textRangeFromPosition:end toPosition:end]; |
| |
| self.selectedTextRange = textRange; |
| } |
| |
| // A helper to accept the current autocomplete text. |
| - (void)acceptAutocompleteText { |
| DCHECK([self hasAutocompleteText]); |
| // Strip attributes and set text as if the user typed it. |
| NSAttributedString* string = |
| [[NSAttributedString alloc] initWithString:_selection.text]; |
| [self setText:string userTextLength:string.length]; |
| } |
| |
| #pragma mark - helpers |
| |
| // Gets the bounds of the rect covering the URL. |
| - (CGRect)preEditLabelRectForBounds:(CGRect)bounds { |
| return [self editingRectForBounds:self.bounds]; |
| } |
| |
| - (NSTextAlignment)bestAlignmentForText:(NSString*)text { |
| if (text.length) { |
| NSString* lang = CFBridgingRelease(CFStringTokenizerCopyBestStringLanguage( |
| (CFStringRef)text, CFRangeMake(0, text.length))); |
| |
| if ([NSLocale characterDirectionForLanguage:lang] == |
| NSLocaleLanguageDirectionRightToLeft) { |
| return NSTextAlignmentRight; |
| } |
| } |
| return NSTextAlignmentLeft; |
| } |
| |
| - (NSTextAlignment)preEditTextAlignment { |
| // If the pre-edit text is wider than the omnibox, right-align the text so it |
| // ends at the same x coord as the blue selection box. |
| CGSize textSize = |
| [_preEditStaticLabel.text cr_pixelAlignedSizeWithFont:self.currentFont]; |
| // Note, this does not need to support RTL, as URLs are always LTR. |
| return textSize.width < _preEditStaticLabel.frame.size.width |
| ? NSTextAlignmentLeft |
| : NSTextAlignmentRight; |
| } |
| |
| - (NSString*)nsDisplayedText { |
| if (_selection) |
| return [_selection text]; |
| return [self text]; |
| } |
| |
| // Creates the SelectedTextLabel if it doesn't already exist and adds it as a |
| // subview. |
| - (void)createSelectionViewIfNecessary { |
| if (_selection) |
| return; |
| |
| _selection = [[UILabel alloc] initWithFrame:CGRectZero]; |
| [_selection setFont:self.currentFont]; |
| [_selection setTextColor:_displayedTextColor]; |
| [_selection setOpaque:NO]; |
| [_selection setBackgroundColor:[UIColor clearColor]]; |
| [self addSubview:_selection]; |
| [self hideTextAndCursor]; |
| } |
| |
| // Helper method used to set the text of this field. Updates the selection view |
| // to contain the correct inline autocomplete text. |
| - (void)setTextInternal:(NSAttributedString*)text |
| autocompleteLength:(NSUInteger)autocompleteLength { |
| // Extract substrings for the permanent text and the autocomplete text. The |
| // former needs to retain any text attributes from the original string. |
| NSRange fieldRange = NSMakeRange(0, [text length] - autocompleteLength); |
| NSAttributedString* fieldText = |
| [text attributedSubstringFromRange:fieldRange]; |
| |
| if (autocompleteLength > 0) { |
| // Creating |autocompleteText| from |[text string]| has the added bonus of |
| // removing all the previously set attributes. This way the autocomplete |
| // text doesn't have a highlighted protocol, etc. |
| NSMutableAttributedString* autocompleteText = |
| [[NSMutableAttributedString alloc] initWithString:[text string]]; |
| |
| [self createSelectionViewIfNecessary]; |
| DCHECK(_selection); |
| [autocompleteText |
| addAttribute:NSBackgroundColorAttributeName |
| value:[self selectedTextBackgroundColor] |
| range:NSMakeRange([fieldText length], autocompleteLength)]; |
| [_selection setAttributedText:autocompleteText]; |
| [_selection setTextAlignment:[self bestTextAlignment]]; |
| } else { |
| [self clearAutocompleteText]; |
| } |
| |
| // The following BOOL was introduced to workaround a UIKit bug |
| // (crbug.com/737589, rdar/32817402). The bug relates to third party keyboards |
| // that check the value of textDocumentProxy.documentContextBeforeInput to |
| // show keyboard suggestions. It appears that calling setAttributedText during |
| // an EditingChanged UIControlEvent somehow triggers this bug. The reason we |
| // update the attributed text here is to change the colors of the omnibox |
| // (such as host, protocol) when !self.editing, but also to hide real |
| // UITextField text under the _selection text when self.editing. Since we will |
| // correct the omnibox editing text color anytime |self.text| is different |
| // than |fieldText|, it seems it's OK to skip calling self.attributedText |
| // during the condition added below. If we change mobile omnibox to match |
| // desktop and also color the omnibox while self.editing, this workaround will |
| // no longer work. The check for |autocompleteLength| reduces the scope of |
| // this workaround, without it having introduced crbug.com/740075. |
| BOOL updateText = YES; |
| if (experimental_flags::IsThirdPartyKeyboardWorkaroundEnabled()) { |
| updateText = |
| (!self.editing || ![self.text isEqualToString:fieldText.string] || |
| autocompleteLength == 0); |
| } |
| if (updateText) { |
| self.attributedText = fieldText; |
| } |
| |
| // iOS changes the font to .LastResort when some unexpected unicode strings |
| // are used (e.g. 𝗲𝗺𝗽𝗵𝗮𝘀𝗶𝘀). Setting the NSFontAttributeName in the |
| // attributed string to -systemFontOfSize fixes part of the problem, but the |
| // baseline changes so text is out of alignment. |
| [self setFont:self.currentFont]; |
| [self updateTextDirection]; |
| } |
| |
| - (UIColor*)selectedTextBackgroundColor { |
| if (!_selectedTextBackgroundColor) { |
| if (IsUIRefreshPhase1Enabled()) { |
| _selectedTextBackgroundColor = |
| [self.tintColor colorWithAlphaComponent:0.2]; |
| } else { |
| _selectedTextBackgroundColor = [UIColor colorWithRed:204.0 / 255 |
| green:221.0 / 255 |
| blue:237.0 / 255 |
| alpha:1.0]; |
| } |
| } |
| return _selectedTextBackgroundColor; |
| } |
| |
| - (BOOL)isColorHidden:(UIColor*)color { |
| return ([color isEqual:[UIColor clearColor]] || |
| CGColorGetAlpha(color.CGColor) < 0.05); |
| } |
| |
| // Set the text field's text and cursor to their displayed colors. To be called |
| // when there are no overlaid views displayed. |
| - (void)showTextAndCursor { |
| if ([self isColorHidden:self.textColor]) { |
| [self setTextColor:_displayedTextColor]; |
| } |
| if ([self isColorHidden:self.tintColor]) { |
| [self setTintColor:_displayedTintColor]; |
| } |
| } |
| |
| // Set the text field's text and cursor to clear so that they don't show up |
| // behind any overlaid views. |
| - (void)hideTextAndCursor { |
| [self setTintColor:[UIColor clearColor]]; |
| [self setTextColor:[UIColor clearColor]]; |
| } |
| |
| - (NSArray*)fadeAnimationLayers { |
| NSMutableArray* layers = [NSMutableArray array]; |
| for (UIView* subview in self.subviews) |
| [layers addObject:subview.layer]; |
| return layers; |
| } |
| |
| - (BOOL)isTextFieldLTR { |
| return [[self class] userInterfaceLayoutDirectionForSemanticContentAttribute: |
| self.semanticContentAttribute] == |
| UIUserInterfaceLayoutDirectionLeftToRight; |
| } |
| |
| - (CGRect)layoutRightViewForBounds:(CGRect)bounds { |
| DCHECK(!IsRefreshLocationBarEnabled()); |
| |
| if ([self rightView]) { |
| CGSize rightViewSize = self.rightView.bounds.size; |
| CGFloat leadingOffset = 0; |
| leadingOffset = |
| bounds.size.width - rightViewSize.width - kClearButtonRightMarginIphone; |
| LayoutRect rightViewLayout; |
| rightViewLayout.position.leading = leadingOffset; |
| rightViewLayout.boundingWidth = CGRectGetWidth(bounds); |
| rightViewLayout.position.originY = |
| floor((bounds.size.height - rightViewSize.height) / 2.0); |
| rightViewLayout.size = rightViewSize; |
| return LayoutRectGetRect(rightViewLayout); |
| } |
| return CGRectZero; |
| } |
| |
| - (CGRect)layoutLeftViewForBounds:(CGRect)bounds { |
| return CGRectZero; |
| } |
| |
| // Accesses the clear button view when it's available; correctly resolves RTL. |
| // This method must not be named -clearButton, because that conflicts with an |
| // internal UITextField method. |
| - (UIView*)clearButtonView { |
| DCHECK(!IsRefreshLocationBarEnabled()); |
| if ([self isTextFieldLTR]) { |
| return self.rightView; |
| } else { |
| return self.leftView; |
| } |
| } |
| |
| - (void)resetClearButton { |
| DCHECK(!IsRefreshLocationBarEnabled()); |
| |
| if ([self isTextFieldLTR]) { |
| self.rightView = nil; |
| } else { |
| self.rightView = nil; |
| } |
| } |
| |
| - (CGFloat)clearButtonAnimationOffset { |
| DCHECK(!IsRefreshLocationBarEnabled()); |
| |
| if ([self isTextFieldLTR]) { |
| return kToolbarButtonAnimationOffset; |
| } else { |
| return -kToolbarButtonAnimationOffset; |
| } |
| } |
| |
| // Calculates editing rect from |bounds| rect by adjusting for in-bounds |
| // decorations such as left/right view. |
| - (CGRect)adjustedEditingRectForBounds:(CGRect)bounds { |
| CGRect newBounds = bounds; |
| |
| if (!IsRefreshLocationBarEnabled()) { |
| // -editingRectForBounds doesn't account for rightViews that aren't flush |
| // with the right edge, it just looks at the rightView's width. Account for |
| // the offset here. |
| CGFloat rightViewMaxX = CGRectGetMaxX([self rightViewRectForBounds:bounds]); |
| if (rightViewMaxX) |
| newBounds.size.width -= bounds.size.width - rightViewMaxX; |
| |
| LayoutRect editingRectLayout = |
| LayoutRectForRectInBoundingRect(newBounds, bounds); |
| editingRectLayout.size.width -= kEditingRectWidthInset; |
| if (IsIPadIdiom()) { |
| if (!IsCompactTablet() && !self.rightView) { |
| // Normally the clear button shrinks the edit box, but if the rightView |
| // isn't set, shrink behind the mic icons. |
| editingRectLayout.size.width -= kVoiceSearchButtonWidth; |
| } |
| } |
| // Don't let the edit rect extend over the clear button. The right view |
| // is hidden during animations, so fake its width here. |
| if (self.rightViewMode == UITextFieldViewModeNever) |
| editingRectLayout.size.width -= self.rightView.bounds.size.width; |
| |
| newBounds = LayoutRectGetRect(editingRectLayout); |
| } |
| |
| return newBounds; |
| } |
| |
| // Aligns the selection UILabel to match the editing rect bounds. Takes iOS |
| // version-specific text rendering differences into account. |
| - (void)layoutSelectionViewWithNewEditingRectBounds:(CGRect)newBounds { |
| // The goal is to visually align the _selection label and the |self| textfield |
| // to avoid text jumping when inline autocomplete is shown or hidden. |
| CGFloat baselineDifference = kUILabelUITextfieldBaselineDeltaInPoints; |
| if (IsIPadIdiom() && !base::ios::IsRunningOnIOS11OrLater()) { |
| // On iOS 10, there is a difference between iPad and iPhone rendering. |
| baselineDifference = kUILabelUITextfieldBaselineDeltaIpadIOS10InPixels / |
| UIScreen.mainScreen.scale; |
| } |
| |
| newBounds.origin.y -= baselineDifference; |
| |
| // Position the selection view appropriately. |
| [_selection setFrame:newBounds]; |
| |
| newBounds.origin.y += baselineDifference; |
| } |
| |
| @end |