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