Index: ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.mm |
diff --git a/ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.mm b/ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..e67c3ab36619a5c57976d7c493a639e1a0e5568a |
--- /dev/null |
+++ b/ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.mm |
@@ -0,0 +1,1053 @@ |
+// 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/mac/objc_property_releaser.h" |
+#include "base/mac/scoped_nsobject.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/autocomplete/autocomplete_scheme_classifier_impl.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" |
+#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/base/resource/resource_bundle.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" |
+ |
+namespace { |
+const CGFloat kFontSize = 16; |
+const CGFloat kEditingRectX = 16; |
+const CGFloat kEditingRectWidthInset = 10; |
+const CGFloat kTextInset = 8; |
+const CGFloat kTextInsetWithChip = 3; |
+const CGFloat kTextInsetNoLeftView = 12; |
+const CGFloat kImageInset = 9; |
+const CGFloat kClearButtonRightMarginIphone = 7; |
+const CGFloat kClearButtonRightMarginIpad = 12; |
+// Amount to shift the origin.x of the text areas so they're centered within the |
+// omnibox border. |
+const CGFloat kTextAreaLeadingOffset = -2; |
+ |
+// TODO(rohitrao): Should this be pulled from somewhere else? |
+const CGFloat kStarButtonWidth = 36; |
+const CGFloat kVoiceSearchButtonWidth = 36.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 () |
+ |
+// Current image id used in left view. |
+@property(nonatomic, assign) NSUInteger leftViewImageId; |
+ |
+// 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; |
+// Display an image or chip text in the left accessory view. |
+- (void)updateLeftView; |
+// 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; |
+ |
+@end |
+ |
+#pragma mark - |
+#pragma mark OmniboxTextFieldIOS |
+ |
+@implementation OmniboxTextFieldIOS { |
+ // Currently selected chip text. Nil if no chip. |
+ base::scoped_nsobject<NSString> _chipText; |
+ base::scoped_nsobject<UILabel> _selection; |
+ base::scoped_nsobject<UILabel> _preEditStaticLabel; |
+ NSString* _preEditText; |
+ base::scoped_nsobject<UIFont> _font; |
+ base::scoped_nsobject<UIColor> _displayedTextColor; |
+ base::scoped_nsobject<UIColor> _displayedTintColor; |
+ UIColor* _selectedTextBackgroundColor; |
+ UIColor* _placeholderTextColor; |
+ |
+ // The 'Copy URL' menu item is sometimes shown in the edit menu, so keep it |
+ // around to make adding/removing easier. |
+ base::scoped_nsobject<UIMenuItem> _copyUrlMenuItem; |
+ |
+ base::mac::ObjCPropertyReleaser _propertyReleaser_OmniboxTextFieldIOS; |
+} |
+ |
+@synthesize leftViewImageId = _leftViewImageId; |
+@synthesize preEditText = _preEditText; |
+@synthesize clearingPreEditText = _clearingPreEditText; |
+@synthesize selectedTextBackgroundColor = _selectedTextBackgroundColor; |
+@synthesize placeholderTextColor = _placeholderTextColor; |
+@synthesize incognito = _incognito; |
+ |
+// 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 |
+ textColor:(UIColor*)textColor |
+ tintColor:(UIColor*)tintColor { |
+ self = [super initWithFrame:frame]; |
+ if (self) { |
+ _propertyReleaser_OmniboxTextFieldIOS.Init(self, |
+ [OmniboxTextFieldIOS class]); |
+ _font.reset([font retain]); |
+ _displayedTextColor.reset([textColor retain]); |
+ if (tintColor) { |
+ [self setTintColor:tintColor]; |
+ _displayedTintColor.reset([tintColor retain]); |
+ } else { |
+ _displayedTintColor.reset([self.tintColor retain]); |
+ } |
+ [self setFont:_font]; |
+ [self setTextColor:_displayedTextColor]; |
+ [self setClearButtonMode:UITextFieldViewModeNever]; |
+ [self setRightViewMode:UITextFieldViewModeAlways]; |
+ [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]; |
+ |
+ // 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; |
+} |
+ |
+// 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]; |
+} |
+ |
+// Exposed for testing. |
+- (UILabel*)preEditStaticLabel { |
+ return _preEditStaticLabel; |
+} |
+ |
+- (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]; |
+ } |
+} |
+ |
+// 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.get()) { |
+ [super touchesBegan:touches withEvent:event]; |
+ return; |
+ } |
+ |
+ // Only consider a single touch. |
+ UITouch* touch = [touches anyObject]; |
+ if (!touch) |
+ return; |
+ |
+ // Accept selection. |
+ base::scoped_nsobject<NSString> newText([[self nsDisplayedText] copy]); |
+ [self clearAutocompleteText]; |
+ [self setText:newText]; |
+} |
+ |
+// Gets the bounds of the rect covering the URL. |
+- (CGRect)preEditLabelRectForBounds:(CGRect)bounds { |
+ return [self editingRectForBounds:self.bounds]; |
+} |
+ |
+// 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.get()); |
+ CGRect rect = [self preEditLabelRectForBounds:self.bounds]; |
+ _preEditStaticLabel.reset([[UILabel alloc] initWithFrame:rect]); |
+ _preEditStaticLabel.get().backgroundColor = [UIColor clearColor]; |
+ _preEditStaticLabel.get().opaque = YES; |
+ _preEditStaticLabel.get().font = _font; |
+ _preEditStaticLabel.get().textColor = _displayedTextColor; |
+ _preEditStaticLabel.get().lineBreakMode = NSLineBreakByTruncatingHead; |
+ |
+ NSDictionary* attributes = |
+ @{NSBackgroundColorAttributeName : [self selectedTextBackgroundColor]}; |
+ base::scoped_nsobject<NSAttributedString> preEditString( |
+ [[NSAttributedString alloc] initWithString:self.text |
+ attributes:attributes]); |
+ [_preEditStaticLabel setAttributedText:preEditString]; |
+ _preEditStaticLabel.get().textAlignment = [self preEditTextAlignment]; |
+ [self addSubview:_preEditStaticLabel]; |
+} |
+ |
+- (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)bestTextAlignment { |
+ if (!base::ios::IsRunningOnIOS9OrLater() || [self isFirstResponder]) { |
+ return [self bestAlignmentForText:[self text]]; |
+ } |
+ return NSTextAlignmentNatural; |
+} |
+ |
+- (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.get().text cr_pixelAlignedSizeWithFont:_font]; |
+ BOOL isLTR = [self bestTextAlignment] == NSTextAlignmentLeft; |
+ return textSize.width < _preEditStaticLabel.get().frame.size.width |
+ ? (isLTR ? NSTextAlignmentLeft : NSTextAlignmentRight) |
+ : (isLTR ? NSTextAlignmentRight : NSTextAlignmentLeft); |
+} |
+ |
+- (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.get().textAlignment = [self preEditTextAlignment]; |
+ [self hideTextAndCursor]; |
+ } else if (!_selection) { |
+ [self showTextAndCursor]; |
+ } |
+} |
+ |
+// Finishes pre-edit state by removing the UILabel with the URL. |
+- (void)exitPreEditState { |
+ [self setPreEditText:nil]; |
+ if (_preEditStaticLabel) { |
+ [_preEditStaticLabel removeFromSuperview]; |
+ _preEditStaticLabel.reset(nil); |
+ [self showTextAndCursor]; |
+ } |
+} |
+ |
+- (UIColor*)displayedTextColor { |
+ return _displayedTextColor; |
+} |
+ |
+// Returns whether we are processing the first touch event on the text field. |
+- (BOOL)isPreEditing { |
+ return !![self preEditText]; |
+} |
+ |
+- (void)enableLeftViewButton:(BOOL)isEnabled { |
+ if ([self leftView]) |
+ [(UIButton*)[self leftView] setEnabled:isEnabled]; |
+} |
+ |
+- (NSString*)nsDisplayedText { |
+ if (_selection.get()) |
+ return [_selection text]; |
+ return [self 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.get() && [[_selection text] length] > [[self text] length]) { |
+ return base::SysNSStringToUTF16( |
+ [[_selection text] substringFromIndex:[[self text] length]]); |
+ } |
+ return base::string16(); |
+} |
+ |
+- (void)select:(id)sender { |
+ if ([self isPreEditing]) { |
+ [self exitPreEditState]; |
+ } |
+ [super select:sender]; |
+} |
+ |
+- (void)selectAll:(id)sender { |
+ if ([self isPreEditing]) { |
+ [self exitPreEditState]; |
+ } |
+ if (_selection.get()) { |
+ base::scoped_nsobject<NSString> newText([[self nsDisplayedText] copy]); |
+ [self clearAutocompleteText]; |
+ [self setText:newText]; |
+ } |
+ [super selectAll:sender]; |
+} |
+ |
+// Creates the SelectedTextLabel if it doesn't already exist and adds it as a |
+// subview. |
+- (void)createSelectionViewIfNecessary { |
+ if (_selection.get()) |
+ return; |
+ |
+ _selection.reset([[UILabel alloc] initWithFrame:CGRectZero]); |
+ [_selection setFont:_font]; |
+ [_selection setTextColor:_displayedTextColor]; |
+ [_selection setOpaque:NO]; |
+ [_selection setBackgroundColor:[UIColor clearColor]]; |
+ [self addSubview:_selection]; |
+ [self hideTextAndCursor]; |
+} |
+ |
+- (BOOL)isShowingQueryRefinementChip { |
+ return (_chipText && ([self isFirstResponder] || [self isPreEditing])); |
+} |
+ |
+- (void)updateLeftView { |
+ const CGFloat kChipTextTopInset = 3.0; |
+ const CGFloat kChipTextLeftInset = 3.0; |
+ |
+ UIButton* leftViewButton = (UIButton*)self.leftView; |
+ // Only set the chip image if the omnibox is in focus. |
+ if ([self isShowingQueryRefinementChip]) { |
+ [leftViewButton setTitle:_chipText forState:UIControlStateNormal]; |
+ [leftViewButton setImage:nil forState:UIControlStateNormal]; |
+ [leftViewButton |
+ setTitleEdgeInsets:UIEdgeInsetsMake(kChipTextTopInset, |
+ kChipTextLeftInset, 0, 0)]; |
+ // For iPhone, the left view is only updated when not in editing mode (i.e. |
+ // the text field is not first responder). |
+ } else if (_leftViewImageId && (IsIPadIdiom() || ![self isFirstResponder])) { |
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance(); |
+ gfx::Image defaultImage = rb.GetNativeImageNamed(_leftViewImageId); |
+ UIImage* image = [defaultImage.ToUIImage() |
+ imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; |
+ UIImageView* imageView = |
+ [[[UIImageView alloc] initWithImage:image] autorelease]; |
+ [leftViewButton setImage:imageView.image forState:UIControlStateNormal]; |
+ [leftViewButton setTitle:nil forState:UIControlStateNormal]; |
+ UIColor* tint = [UIColor whiteColor]; |
+ if (!_incognito) { |
+ switch (_leftViewImageId) { |
+ case IDR_IOS_LOCATION_BAR_HTTP: |
+ tint = [UIColor darkGrayColor]; |
+ break; |
+ case IDR_IOS_OMNIBOX_HTTPS_VALID: |
+ tint = skia::UIColorFromSkColor(gfx::kGoogleGreen700); |
+ break; |
+ case IDR_IOS_OMNIBOX_HTTPS_POLICY_WARNING: |
+ tint = skia::UIColorFromSkColor(gfx::kGoogleYellow700); |
+ break; |
+ case IDR_IOS_OMNIBOX_HTTPS_INVALID: |
+ tint = skia::UIColorFromSkColor(gfx::kGoogleRed700); |
+ break; |
+ default: |
+ tint = [UIColor darkGrayColor]; |
+ } |
+ } |
+ [leftViewButton setTintColor:tint]; |
+ } else { |
+ // Reset the chip text. |
+ [leftViewButton setTitle:_chipText forState:UIControlStateNormal]; |
+ } |
+ // Normally this isn't needed, but there is a bug in iOS 7.1+ where setting |
+ // the image while disabled doesn't always honor UIControlStateNormal. |
+ // crbug.com/355077 |
+ [leftViewButton setNeedsLayout]; |
+ |
+ [leftViewButton sizeToFit]; |
+ |
+ // -sizeToFit doesn't take into account the left inset, so expand the width of |
+ // the button by |kChipTextLeftInset|. |
+ if ([self isShowingQueryRefinementChip]) { |
+ CGRect frame = leftViewButton.frame; |
+ frame.size.width += kChipTextLeftInset; |
+ leftViewButton.frame = frame; |
+ } |
+} |
+ |
+- (void)deleteBackward { |
+ // Must test for the onDeleteBackward method, since it's optional. |
+ if ([[self delegate] respondsToSelector:@selector(onDeleteBackward)]) |
+ [[self delegate] onDeleteBackward]; |
+ [super deleteBackward]; |
+} |
+ |
+// 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. |
+ base::scoped_nsobject<NSMutableAttributedString> autocompleteText( |
+ [[NSMutableAttributedString alloc] initWithString:[text string]]); |
+ |
+ [self createSelectionViewIfNecessary]; |
+ DCHECK(_selection.get()); |
+ [autocompleteText |
+ addAttribute:NSBackgroundColorAttributeName |
+ value:[self selectedTextBackgroundColor] |
+ range:NSMakeRange([fieldText length], autocompleteLength)]; |
+ [_selection setAttributedText:autocompleteText]; |
+ [_selection setTextAlignment:[self bestTextAlignment]]; |
+ } else { |
+ [self clearAutocompleteText]; |
+ } |
+ |
+ 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:_font]; |
+ // TODO(justincohen): Find a better place to put this, and consolidate it with |
+ // the same call in omniboxViewIOS. |
+ [self updateTextDirection]; |
+} |
+ |
+- (UIColor*)selectedTextBackgroundColor { |
+ return _selectedTextBackgroundColor ? _selectedTextBackgroundColor |
+ : [UIColor colorWithRed:204.0 / 255 |
+ green:221.0 / 255 |
+ blue:237.0 / 255 |
+ alpha:1.0]; |
+} |
+ |
+// Ensures that attributedText always uses the proper style attributes. |
+- (void)setAttributedText:(NSAttributedString*)attributedText { |
+ base::scoped_nsobject<NSMutableAttributedString> mutableText( |
+ [attributedText mutableCopy]); |
+ NSRange entireString = NSMakeRange(0, [mutableText length]); |
+ |
+ // Set the font. |
+ [mutableText addAttribute:NSFontAttributeName value:_font 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.get(); |
+ [mutableText addAttribute:NSForegroundColorAttributeName |
+ value:textColor |
+ range:entireString]; |
+ } else { |
+ base::scoped_nsobject<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 RFC 3987 Section 4.1). |
+ [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]; |
+} |
+ |
+// 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]; |
+ UITextWritingDirection writingDirection = |
+ alignment == NSTextAlignmentLeft ? UITextWritingDirectionLeftToRight |
+ : UITextWritingDirectionRightToLeft; |
+ [self |
+ setBaseWritingDirection:writingDirection |
+ forRange:[self |
+ textRangeFromPosition:[self |
+ beginningOfDocument] |
+ toPosition:[self endOfDocument]]]; |
+} |
+ |
+- (void)setPlaceholder:(NSString*)placeholder { |
+ if (placeholder && _placeholderTextColor) { |
+ NSDictionary* attributes = |
+ @{NSForegroundColorAttributeName : _placeholderTextColor}; |
+ self.attributedPlaceholder = |
+ [[[NSAttributedString alloc] initWithString:placeholder |
+ attributes:attributes] autorelease]; |
+ } else { |
+ [super setPlaceholder:placeholder]; |
+ } |
+} |
+ |
+- (void)setText:(NSString*)text { |
+ NSAttributedString* as = |
+ [[[NSAttributedString alloc] initWithString:text] autorelease]; |
+ if (self.text.length > 0 && as.length == 0) { |
+ // Remove the fade animations before the subviews are removed. |
+ [self cleanUpFadeAnimations]; |
+ } |
+ [self setTextInternal:as autocompleteLength:0]; |
+} |
+ |
+- (void)setText:(NSAttributedString*)text |
+ userTextLength:(size_t)userTextLength { |
+ DCHECK_LE(userTextLength, [text length]); |
+ |
+ NSUInteger autocompleteLength = [text length] - userTextLength; |
+ [self setTextInternal:text autocompleteLength:autocompleteLength]; |
+} |
+ |
+- (void)setChipText:(NSString*)chipName { |
+ _chipText.reset(); |
+ if ([chipName length]) { |
+ if ([self bestAlignmentForText:chipName] == NSTextAlignmentLeft) |
+ chipName = [chipName stringByAppendingString:@":"]; |
+ _chipText.reset([chipName copy]); |
+ } |
+ [self updateLeftView]; |
+} |
+ |
+- (BOOL)hasAutocompleteText { |
+ return !!_selection.get(); |
+} |
+ |
+- (void)clearAutocompleteText { |
+ if (_selection) { |
+ [_selection removeFromSuperview]; |
+ _selection.reset(nil); |
+ [self showTextAndCursor]; |
+ } |
+} |
+ |
+- (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]]; |
+} |
+ |
+- (NSString*)markedText { |
+ DCHECK([self conformsToProtocol:@protocol(UITextInput)]); |
+ return [self textInRange:[self markedTextRange]]; |
+} |
+ |
+- (CGRect)textRectForBounds:(CGRect)bounds { |
+ CGRect newBounds = [super textRectForBounds:bounds]; |
+ |
+ LayoutRect textRectLayout = |
+ LayoutRectForRectInBoundingRect(newBounds, bounds); |
+ CGFloat textInset = [self leftViewMode] == UITextFieldViewModeAlways |
+ ? kTextInset |
+ : kTextInsetNoLeftView; |
+ // Shift the text right and reduce the width to create empty space between the |
+ // left view and the omnibox text. |
+ textRectLayout.position.leading += textInset + kTextAreaLeadingOffset; |
+ textRectLayout.size.width -= textInset - kTextAreaLeadingOffset; |
+ |
+ if (IsIPadIdiom()) { |
+ if (!IsCompactTablet()) { |
+ // Adjust the width so that the text doesn't overlap with the bookmark and |
+ // voice search buttons which are displayed inside the omnibox. |
+ textRectLayout.size.width += self.rightView.bounds.size.width - |
+ kVoiceSearchButtonWidth - kStarButtonWidth; |
+ } |
+ } else if (![self isShowingQueryRefinementChip] && self.leftView.alpha == 0) { |
+ CGFloat xDiff = textRectLayout.position.leading - kEditingRectX; |
+ textRectLayout.position.leading = kEditingRectX; |
+ textRectLayout.size.width += xDiff; |
+ } |
+ |
+ return LayoutRectGetRect(textRectLayout); |
+} |
+ |
+- (CGRect)editingRectForBounds:(CGRect)bounds { |
+ CGRect newBounds = [super editingRectForBounds:bounds]; |
+ |
+ // -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.position.leading += kTextAreaLeadingOffset; |
+ editingRectLayout.position.leading += |
+ ([self isShowingQueryRefinementChip]) ? kTextInsetWithChip : kTextInset; |
+ editingRectLayout.size.width -= kTextInset + 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; |
+ } |
+ } else if (![self isShowingQueryRefinementChip]) { |
+ CGFloat xDiff = editingRectLayout.position.leading - kEditingRectX; |
+ editingRectLayout.position.leading = kEditingRectX; |
+ editingRectLayout.size.width += xDiff; |
+ } |
+ // 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); |
+ |
+ // Position the selection view appropriately. |
+ [_selection setFrame:newBounds]; |
+ |
+ return newBounds; |
+} |
+ |
+- (CGRect)rectForDrawTextInRect:(CGRect)rect { |
+ // The goal is to always show the most significant part of the hostname |
+ // (i.e. the end of the TLD). |
+ // |
+ // -------------------- |
+ // www.somereallyreally|longdomainname.com|/path/gets/clipped |
+ // -------------------- |
+ // { clipped prefix } { visible text } { clipped suffix } |
+ |
+ // First find how much (if any) of the scheme/host needs to be clipped so that |
+ // the end of the TLD fits in |rect|. Note that if the omnibox is currently |
+ // displaying a search query the prefix is not clipped. |
+ CGFloat widthOfClippedPrefix = 0; |
+ url::Component scheme, host; |
+ AutocompleteInput::ParseForEmphasizeComponents( |
+ base::SysNSStringToUTF16(self.text), AutocompleteSchemeClassifierImpl(), |
+ &scheme, &host); |
+ if (host.len < 0) { |
+ return rect; |
+ } |
+ NSRange hostRange = NSMakeRange(0, host.begin + host.len); |
+ NSAttributedString* hostString = |
+ [self.attributedText attributedSubstringFromRange:hostRange]; |
+ CGFloat widthOfHost = ceil([hostString size].width); |
+ widthOfClippedPrefix = MAX(widthOfHost - rect.size.width, 0); |
+ |
+ // Now determine if there is any text that will need to be truncated because |
+ // there's not enough room. |
+ int textWidth = ceil([self.attributedText size].width); |
+ CGFloat widthOfClippedSuffix = |
+ MAX(textWidth - rect.size.width - widthOfClippedPrefix, 0); |
+ BOOL suffixClipped = widthOfClippedSuffix > 0; |
+ |
+ // Fade the beginning and/or end of the visible string to indicate to the user |
+ // that the URL has been clipped. |
+ BOOL prefixClipped = widthOfClippedPrefix > 0; |
+ if (prefixClipped || suffixClipped) { |
+ UIImage* fade = nil; |
+ if ([self textAlignment] == NSTextAlignmentRight) { |
+ // Swap prefix and suffix for RTL. |
+ fade = [GTMFadeTruncatingLabel getLinearGradient:rect |
+ fadeHead:suffixClipped |
+ fadeTail:prefixClipped]; |
+ } else { |
+ fade = [GTMFadeTruncatingLabel getLinearGradient:rect |
+ fadeHead:prefixClipped |
+ fadeTail:suffixClipped]; |
+ } |
+ CGContextClipToMask(UIGraphicsGetCurrentContext(), rect, fade.CGImage); |
+ } |
+ |
+ // If necessary, expand the rect so the entire string fits and shift it to the |
+ // left (right for RTL) so the clipped prefix is not shown. |
+ if ([self textAlignment] == NSTextAlignmentRight) { |
+ rect.origin.x -= widthOfClippedSuffix; |
+ } else { |
+ rect.origin.x -= widthOfClippedPrefix; |
+ } |
+ rect.size.width = MAX(rect.size.width, textWidth); |
+ return rect; |
+} |
+ |
+// Enumerate url components (host, path) and draw each one in different rect. |
+- (void)drawTextInRect:(CGRect)rect { |
+ // Save and restore the graphics state because rectForDrawTextInRect may |
+ // apply an image mask to fade out beginning and/or end of the URL. |
+ gfx::ScopedCGContextSaveGState saver(UIGraphicsGetCurrentContext()); |
+ [super drawTextInRect:[self rectForDrawTextInRect:rect]]; |
+} |
+ |
+- (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; |
+ UIView* view = [super hitTest:point withEvent:event]; |
+ |
+ // For some reason when the |leftView| has interaction enabled, hitTest |
+ // returns the leftView even when |point| is 50 pixels to the right. Tapping |
+ // the hint text will fire the leftView, causing b/6281652. Fails especially |
+ // on iPad and iPhone devices in landscape mode. |
+ // TODO(crbug.com/546295): Check to see if this UIKit bug is fixed, and remove |
+ // this workaround. |
+ UIView* leftView = [self leftView]; |
+ if (leftView) { |
+ if (leftView == view && !CGRectContainsPoint([leftView frame], point)) { |
+ return self; |
+ } else if ([self leftViewMode] == UITextFieldViewModeAlways) { |
+ CGRect targetFrame = CGRectInset([leftView frame], -5, -5); |
+ if (CGRectContainsPoint(targetFrame, point)) { |
+ return leftView; |
+ } |
+ } |
+ } |
+ return view; |
+} |
+ |
+- (BOOL)isTextFieldLTR { |
+ return [[self class] userInterfaceLayoutDirectionForSemanticContentAttribute: |
+ self.semanticContentAttribute] == |
+ UIUserInterfaceLayoutDirectionLeftToRight; |
+} |
+ |
+// Overriding this method to offset the rightView property |
+// (containing a clear text button). |
+- (CGRect)rightViewRectForBounds:(CGRect)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]; |
+} |
+ |
+- (CGRect)layoutRightViewForBounds:(CGRect)bounds { |
+ if ([self rightView]) { |
+ CGSize rightViewSize = self.rightView.bounds.size; |
+ CGFloat leadingOffset = 0; |
+ if (IsIPadIdiom() && !IsCompactTablet()) { |
+ leadingOffset = bounds.size.width - kVoiceSearchButtonWidth - |
+ rightViewSize.width - kClearButtonRightMarginIpad; |
+ } else { |
+ 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; |
+} |
+ |
+// Overriding this method to offset the leftView property |
+// (containing a placeholder image) consistently with omnibox text padding. |
+- (CGRect)leftViewRectForBounds:(CGRect)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 layoutLeftViewForBounds:bounds]; |
+ } |
+ return [self layoutRightViewForBounds:bounds]; |
+} |
+ |
+- (CGRect)layoutLeftViewForBounds:(CGRect)bounds { |
+ if ([self leftView]) { |
+ CGSize imageSize = [[self leftView] bounds].size; |
+ LayoutRect leftViewLayout = |
+ LayoutRectMake(kImageInset, CGRectGetWidth(bounds), |
+ floor((bounds.size.height - imageSize.height) / 2.0), |
+ imageSize.width, imageSize.height); |
+ return LayoutRectGetRect(leftViewLayout); |
+ } |
+ return CGRectZero; |
+} |
+ |
+- (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]; |
+ } |
+ } |
+} |
+ |
+- (NSArray*)fadeAnimationLayers { |
+ NSMutableArray* layers = [NSMutableArray array]; |
+ for (UIView* subview in self.subviews) |
+ [layers addObject:subview.layer]; |
+ return layers; |
+} |
+ |
+- (void)reverseFadeAnimations { |
+ ReverseAnimationsForKeyForLayers(kOmniboxFadeAnimationKey, |
+ [self fadeAnimationLayers]); |
+} |
+ |
+- (void)cleanUpFadeAnimations { |
+ RemoveAnimationForKeyFromLayers(kOmniboxFadeAnimationKey, |
+ [self fadeAnimationLayers]); |
+} |
+ |
+#pragma mark - Placeholder image handling methods. |
+ |
+- (void)setPlaceholderImage:(int)imageId { |
+ _leftViewImageId = imageId; |
+ [self updateLeftView]; |
+} |
+ |
+- (void)showPlaceholderImage { |
+ [self setLeftViewMode:UITextFieldViewModeAlways]; |
+} |
+ |
+- (void)hidePlaceholderImage { |
+ [self setLeftViewMode:UITextFieldViewModeNever]; |
+} |
+ |
+#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]; |
+} |
+ |
+// 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]; |
+} |
+ |
+- (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); |
+} |
+ |
+- (BOOL)becomeFirstResponder { |
+ if (![super becomeFirstResponder]) |
+ return NO; |
+ |
+ if (!_copyUrlMenuItem.get()) { |
+ NSString* const kTitle = l10n_util::GetNSString(IDS_IOS_COPY_URL); |
+ _copyUrlMenuItem.reset( |
+ [[UIMenuItem alloc] initWithTitle:kTitle action:@selector(copyUrl:)]); |
+ } |
+ |
+ // Add the "Copy URL" menu item to the |sharedMenuController| if necessary. |
+ UIMenuController* menuController = [UIMenuController sharedMenuController]; |
+ if (menuController.menuItems) { |
+ if (![menuController.menuItems containsObject:_copyUrlMenuItem]) { |
+ menuController.menuItems = |
+ [menuController.menuItems arrayByAddingObject:_copyUrlMenuItem]; |
+ } |
+ } else { |
+ menuController.menuItems = [NSArray arrayWithObject:_copyUrlMenuItem]; |
+ } |
+ return YES; |
+} |
+ |
+- (BOOL)resignFirstResponder { |
+ if (![super resignFirstResponder]) |
+ return NO; |
+ |
+ // Remove the "Copy URL" menu item from the |sharedMenuController|. |
+ UIMenuController* menuController = [UIMenuController sharedMenuController]; |
+ NSMutableArray* menuItems = |
+ [NSMutableArray arrayWithArray:menuController.menuItems]; |
+ [menuItems removeObject:_copyUrlMenuItem]; |
+ menuController.menuItems = menuItems; |
+ return YES; |
+} |
+ |
+- (void)copyUrl:(id)sender { |
+ [[self delegate] onCopyURL]; |
+} |
+ |
+- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { |
+ if (action == @selector(copyUrl:)) { |
+ return [[self delegate] canCopyURL]; |
+ } |
+ |
+ // Disable the "Define" menu item. iOS7 implements this with a private |
+ // selector. Avoid using private APIs by instead doing a string comparison. |
+ if ([NSStringFromSelector(action) hasSuffix:@"define:"]) { |
+ return NO; |
+ } |
+ |
+ // Disable the RTL arrow menu item. The omnibox sets alignment based on the |
+ // text in the field, and should not be overridden. |
+ if ([NSStringFromSelector(action) hasPrefix:@"makeTextWritingDirection"]) { |
+ return NO; |
+ } |
+ |
+ return [super canPerformAction:action withSender:sender]; |
+} |
+ |
+@end |