blob: 64020a711201f11c6d7ae81db8e1fc1865d881df [file] [log] [blame]
// 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/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/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/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 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.
NSString* _chipText;
UILabel* _selection;
UILabel* _preEditStaticLabel;
UIFont* _font;
UIColor* _displayedTextColor;
UIColor* _displayedTintColor;
// The 'Copy URL' menu item is sometimes shown in the edit menu, so keep it
// around to make adding/removing easier.
UIMenuItem* _copyUrlMenuItem;
}
@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) {
_font = font;
_displayedTextColor = textColor;
if (tintColor) {
[self setTintColor:tintColor];
_displayedTintColor = tintColor;
} else {
_displayedTintColor = self.tintColor;
}
[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) {
[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];
}
// 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);
CGRect rect = [self preEditLabelRectForBounds:self.bounds];
_preEditStaticLabel = [[UILabel alloc] initWithFrame:rect];
_preEditStaticLabel.backgroundColor = [UIColor clearColor];
_preEditStaticLabel.opaque = YES;
_preEditStaticLabel.font = _font;
_preEditStaticLabel.textColor = _displayedTextColor;
_preEditStaticLabel.lineBreakMode = NSLineBreakByTruncatingHead;
NSDictionary* attributes =
@{NSBackgroundColorAttributeName : [self selectedTextBackgroundColor]};
NSAttributedString* preEditString =
[[NSAttributedString alloc] initWithString:self.text
attributes:attributes];
[_preEditStaticLabel setAttributedText:preEditString];
_preEditStaticLabel.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 ([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.text cr_pixelAlignedSizeWithFont:_font];
BOOL isLTR = [self bestTextAlignment] == NSTextAlignmentLeft;
return textSize.width < _preEditStaticLabel.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.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 = 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)
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 && [[_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) {
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)
return;
_selection = [[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])) {
UIImage* image = [NativeImage(_leftViewImageId)
imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIImageView* imageView = [[UIImageView alloc] initWithImage:image];
[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];
self.leftView.isAccessibilityElement =
self.attributedText.length != 0 && leftViewButton.isEnabled;
// -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.
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];
}
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 {
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;
[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 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];
} 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];
}
- (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 = nil;
if ([chipName length]) {
if ([self bestAlignmentForText:chipName] == NSTextAlignmentLeft)
chipName = [chipName stringByAppendingString:@":"];
_chipText = [chipName copy];
}
[self updateLeftView];
}
- (BOOL)hasAutocompleteText {
return !!_selection;
}
- (void)clearAutocompleteText {
if (_selection) {
[_selection removeFromSuperview];
_selection = 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) {
NSString* const kTitle = l10n_util::GetNSString(IDS_IOS_COPY_URL);
_copyUrlMenuItem =
[[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