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

Unified 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 side-by-side diff with in-line comments
Download patch
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

Powered by Google App Engine
This is Rietveld 408576698