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 #include "ios/chrome/browser/ui/omnibox/omnibox_view_ios.h" |
| 6 |
| 7 #import <CoreText/CoreText.h> |
| 8 #import <MobileCoreServices/MobileCoreServices.h> |
| 9 |
| 10 #include "base/command_line.h" |
| 11 #include "base/ios/device_util.h" |
| 12 #include "base/memory/ptr_util.h" |
| 13 #include "base/metrics/user_metrics.h" |
| 14 #include "base/metrics/user_metrics_action.h" |
| 15 #include "base/strings/string16.h" |
| 16 #include "base/strings/sys_string_conversions.h" |
| 17 #include "components/omnibox/browser/autocomplete_input.h" |
| 18 #include "components/omnibox/browser/autocomplete_match.h" |
| 19 #include "components/omnibox/browser/omnibox_edit_model.h" |
| 20 #include "components/omnibox/browser/omnibox_popup_model.h" |
| 21 #include "components/toolbar/toolbar_model.h" |
| 22 #include "ios/chrome/browser/autocomplete/autocomplete_scheme_classifier_impl.h" |
| 23 #include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
| 24 #include "ios/chrome/browser/ui/omnibox/chrome_omnibox_client_ios.h" |
| 25 #include "ios/chrome/browser/ui/omnibox/omnibox_popup_view_ios.h" |
| 26 #include "ios/chrome/browser/ui/omnibox/omnibox_util.h" |
| 27 #include "ios/chrome/browser/ui/omnibox/preload_provider.h" |
| 28 #include "ios/chrome/browser/ui/omnibox/web_omnibox_edit_controller.h" |
| 29 #include "ios/chrome/browser/ui/ui_util.h" |
| 30 #include "ios/chrome/grit/ios_theme_resources.h" |
| 31 #include "ios/web/public/referrer.h" |
| 32 #import "net/base/mac/url_conversions.h" |
| 33 #include "skia/ext/skia_utils_ios.h" |
| 34 #include "ui/base/page_transition_types.h" |
| 35 #include "ui/base/resource/resource_bundle.h" |
| 36 #include "ui/base/window_open_disposition.h" |
| 37 #include "ui/gfx/color_palette.h" |
| 38 #include "ui/gfx/image/image.h" |
| 39 |
| 40 using base::UserMetricsAction; |
| 41 |
| 42 namespace { |
| 43 // The color of the rest of the URL (i.e. after the TLD) in the omnibox. |
| 44 UIColor* BaseTextColor() { |
| 45 return [UIColor colorWithWhite:(161 / 255.0) alpha:1.0]; |
| 46 } |
| 47 |
| 48 // The color of the https when there is an error. |
| 49 UIColor* ErrorTextColor() { |
| 50 return skia::UIColorFromSkColor(gfx::kGoogleRed700); |
| 51 } |
| 52 |
| 53 // The color of the https when there is a warning. |
| 54 UIColor* WarningTextColor() { |
| 55 return skia::UIColorFromSkColor(gfx::kGoogleYellow700); |
| 56 } |
| 57 |
| 58 // The color of the https when there is not an error. |
| 59 UIColor* SecureTextColor() { |
| 60 return skia::UIColorFromSkColor(gfx::kGoogleGreen700); |
| 61 } |
| 62 |
| 63 // The color of the https when highlighted in incognito. |
| 64 UIColor* IncognitoSecureTextColor() { |
| 65 return [UIColor colorWithWhite:(255 / 255.0) alpha:1.0]; |
| 66 } |
| 67 |
| 68 // Helper to make converting url_parse ranges to NSRange easier to |
| 69 // read. |
| 70 NSRange ComponentToNSRange(const url::Component& component) { |
| 71 return NSMakeRange(static_cast<NSInteger>(component.begin), |
| 72 static_cast<NSInteger>(component.len)); |
| 73 } |
| 74 |
| 75 } // namespace |
| 76 |
| 77 // Simple Obj-C object to forward UITextFieldDelegate method calls back to the |
| 78 // OmniboxViewIOS. |
| 79 @interface AutocompleteTextFieldDelegate : NSObject<OmniboxTextFieldDelegate> { |
| 80 @private |
| 81 OmniboxViewIOS* editView_; // weak, owns us |
| 82 |
| 83 // YES if we are already forwarding an OnDidChange() message to the edit view. |
| 84 // Needed to prevent infinite recursion. |
| 85 // TODO(rohitrao): There must be a better way. |
| 86 BOOL forwardingOnDidChange_; |
| 87 |
| 88 // YES if this text field is currently processing a user-initiated event, |
| 89 // such as typing in the omnibox or pressing the clear button. Used to |
| 90 // distinguish between calls to textDidChange that are triggered by the user |
| 91 // typing vs by calls to setText. |
| 92 BOOL processingUserEvent_; |
| 93 } |
| 94 @end |
| 95 |
| 96 @implementation AutocompleteTextFieldDelegate |
| 97 - (id)initWithEditView:(OmniboxViewIOS*)editView { |
| 98 if ((self = [super init])) { |
| 99 editView_ = editView; |
| 100 forwardingOnDidChange_ = NO; |
| 101 processingUserEvent_ = NO; |
| 102 } |
| 103 return self; |
| 104 } |
| 105 |
| 106 - (BOOL)textField:(UITextField*)textField |
| 107 shouldChangeCharactersInRange:(NSRange)range |
| 108 replacementString:(NSString*)newText { |
| 109 processingUserEvent_ = editView_->OnWillChange(range, newText); |
| 110 return processingUserEvent_; |
| 111 } |
| 112 |
| 113 - (void)textFieldDidChange:(id)sender { |
| 114 if (forwardingOnDidChange_) |
| 115 return; |
| 116 |
| 117 BOOL savedProcessingUserEvent = processingUserEvent_; |
| 118 processingUserEvent_ = NO; |
| 119 forwardingOnDidChange_ = YES; |
| 120 editView_->OnDidChange(savedProcessingUserEvent); |
| 121 forwardingOnDidChange_ = NO; |
| 122 } |
| 123 |
| 124 // Delegate method for UITextField, called when user presses the "go" button. |
| 125 - (BOOL)textFieldShouldReturn:(UITextField*)textField { |
| 126 editView_->OnAccept(); |
| 127 return NO; |
| 128 } |
| 129 |
| 130 // Always update the text field colors when we start editing. It's possible |
| 131 // for this method to be called when we are already editing (popup focus |
| 132 // change). In this case, OnDidBeginEditing will be called multiple times. |
| 133 // If that becomes an issue a boolean should be added to track editing state. |
| 134 - (void)textFieldDidBeginEditing:(UITextField*)textField { |
| 135 editView_->OnDidBeginEditing(); |
| 136 } |
| 137 |
| 138 // On phone, the omnibox may still be editing when the popup is open, so end |
| 139 // editing is called directly in OnDidEndEditing. |
| 140 - (void)textFieldDidEndEditing:(UITextField*)textField { |
| 141 if (!IsIPadIdiom() && editView_->IsPopupOpen()) |
| 142 return; |
| 143 |
| 144 editView_->OnDidEndEditing(); |
| 145 } |
| 146 |
| 147 // When editing, forward the message on to |editView_|. |
| 148 - (BOOL)textFieldShouldClear:(UITextField*)textField { |
| 149 editView_->OnClear(); |
| 150 processingUserEvent_ = YES; |
| 151 return YES; |
| 152 } |
| 153 |
| 154 - (BOOL)onCopy { |
| 155 return editView_->OnCopy(); |
| 156 } |
| 157 |
| 158 - (BOOL)onCopyURL { |
| 159 return editView_->OnCopyURL(); |
| 160 } |
| 161 |
| 162 - (BOOL)canCopyURL { |
| 163 return editView_->CanCopyURL(); |
| 164 } |
| 165 |
| 166 - (void)willPaste { |
| 167 editView_->WillPaste(); |
| 168 } |
| 169 |
| 170 - (void)onDeleteBackward { |
| 171 editView_->OnDeleteBackward(); |
| 172 } |
| 173 |
| 174 @end |
| 175 |
| 176 OmniboxViewIOS::OmniboxViewIOS(OmniboxTextFieldIOS* field, |
| 177 WebOmniboxEditController* controller, |
| 178 ios::ChromeBrowserState* browser_state, |
| 179 id<PreloadProvider> preloader, |
| 180 id<OmniboxPopupPositioner> positioner) |
| 181 : OmniboxView( |
| 182 controller, |
| 183 base::MakeUnique<ChromeOmniboxClientIOS>(controller, browser_state)), |
| 184 browser_state_(browser_state), |
| 185 field_([field retain]), |
| 186 controller_(controller), |
| 187 preloader_(preloader), |
| 188 ignore_popup_updates_(false) { |
| 189 popup_view_.reset(new OmniboxPopupViewIOS(this, model(), positioner)); |
| 190 field_delegate_.reset( |
| 191 [[AutocompleteTextFieldDelegate alloc] initWithEditView:this]); |
| 192 [field_ setDelegate:field_delegate_]; |
| 193 [field_ addTarget:field_delegate_ |
| 194 action:@selector(textFieldDidChange:) |
| 195 forControlEvents:UIControlEventEditingChanged]; |
| 196 } |
| 197 |
| 198 OmniboxViewIOS::~OmniboxViewIOS() { |
| 199 // |field_| outlives this object. |
| 200 [field_ setDelegate:nil]; |
| 201 |
| 202 [field_ removeTarget:field_delegate_ |
| 203 action:@selector(textFieldDidChange:) |
| 204 forControlEvents:UIControlEventEditingChanged]; |
| 205 |
| 206 // Destroy the model, in case it tries to call back into us when destroyed. |
| 207 popup_view_.reset(); |
| 208 } |
| 209 |
| 210 void OmniboxViewIOS::OpenMatch(const AutocompleteMatch& match, |
| 211 WindowOpenDisposition disposition, |
| 212 const GURL& alternate_nav_url, |
| 213 const base::string16& pasted_text, |
| 214 size_t selected_line) { |
| 215 // It may be unsafe to modify the contents of the field. |
| 216 if (ShouldIgnoreUserInputDueToPendingVoiceSearch()) { |
| 217 return; |
| 218 } |
| 219 |
| 220 OmniboxView::OpenMatch(match, disposition, alternate_nav_url, pasted_text, |
| 221 selected_line); |
| 222 } |
| 223 |
| 224 base::string16 OmniboxViewIOS::GetText() const { |
| 225 return [field_ displayedText]; |
| 226 } |
| 227 |
| 228 void OmniboxViewIOS::SetWindowTextAndCaretPos(const base::string16& text, |
| 229 size_t caret_pos, |
| 230 bool update_popup, |
| 231 bool notify_text_changed) { |
| 232 // Do not call SetUserText() here, as the user has not triggered this change. |
| 233 // Instead, set the field's text directly. |
| 234 // TODO(justincohen): b/5244062 Temporary fix to set the text_field value |
| 235 // before model()->CurrentTextIsUrl(), since that pulls from text_field.value. |
| 236 [field_ setText:base::SysUTF16ToNSString(text)]; |
| 237 |
| 238 NSAttributedString* as = ApplyTextAttributes(text); |
| 239 [field_ setText:as userTextLength:[as length]]; |
| 240 |
| 241 if (update_popup) |
| 242 UpdatePopup(); |
| 243 |
| 244 if (notify_text_changed) |
| 245 model()->OnChanged(); |
| 246 } |
| 247 |
| 248 void OmniboxViewIOS::RevertAll() { |
| 249 ignore_popup_updates_ = true; |
| 250 OmniboxView::RevertAll(); |
| 251 ignore_popup_updates_ = false; |
| 252 } |
| 253 |
| 254 void OmniboxViewIOS::UpdatePopup() { |
| 255 model()->SetInputInProgress(true); |
| 256 |
| 257 if (!model()->has_focus()) |
| 258 return; |
| 259 |
| 260 // Prevent inline-autocomplete if the IME is currently composing or if the |
| 261 // cursor is not at the end of the text. |
| 262 bool prevent_inline_autocomplete = |
| 263 IsImeComposing() || |
| 264 NSMaxRange(current_selection_) != [[field_ text] length]; |
| 265 model()->StartAutocomplete(current_selection_.length != 0, |
| 266 prevent_inline_autocomplete); |
| 267 popup_view_->SetTextAlignment([field_ bestTextAlignment]); |
| 268 } |
| 269 |
| 270 void OmniboxViewIOS::OnTemporaryTextMaybeChanged( |
| 271 const base::string16& display_text, |
| 272 bool save_original_selection, |
| 273 bool notify_text_changed) { |
| 274 SetWindowTextAndCaretPos(display_text, display_text.size(), false, false); |
| 275 model()->OnChanged(); |
| 276 } |
| 277 |
| 278 bool OmniboxViewIOS::OnInlineAutocompleteTextMaybeChanged( |
| 279 const base::string16& display_text, |
| 280 size_t user_text_length) { |
| 281 if (display_text == GetText()) |
| 282 return false; |
| 283 |
| 284 NSAttributedString* as = ApplyTextAttributes(display_text); |
| 285 [field_ setText:as userTextLength:user_text_length]; |
| 286 model()->OnChanged(); |
| 287 return true; |
| 288 } |
| 289 |
| 290 void OmniboxViewIOS::OnBeforePossibleChange() { |
| 291 GetState(&state_before_change_); |
| 292 marked_text_before_change_.reset([[field_ markedText] copy]); |
| 293 } |
| 294 |
| 295 bool OmniboxViewIOS::OnAfterPossibleChange(bool allow_keyword_ui_change) { |
| 296 State new_state; |
| 297 GetState(&new_state); |
| 298 // Manually update the selection state after calling GetState(). |
| 299 new_state.sel_start = current_selection_.location; |
| 300 new_state.sel_end = current_selection_.location + current_selection_.length; |
| 301 |
| 302 OmniboxView::StateChanges state_changes = |
| 303 GetStateChanges(state_before_change_, new_state); |
| 304 |
| 305 // iOS does not supports KeywordProvider, so never allow keyword UI changes. |
| 306 const bool something_changed = |
| 307 model()->OnAfterPossibleChange(state_changes, allow_keyword_ui_change); |
| 308 |
| 309 model()->OnChanged(); |
| 310 |
| 311 // TODO(justincohen): Find a different place to call this. Give the omnibox |
| 312 // a chance to update the alignment for a text direction change. |
| 313 [field_ updateTextDirection]; |
| 314 return something_changed; |
| 315 } |
| 316 |
| 317 bool OmniboxViewIOS::IsImeComposing() const { |
| 318 return [field_ markedTextRange] != nil; |
| 319 } |
| 320 |
| 321 bool OmniboxViewIOS::IsIndicatingQueryRefinement() const { |
| 322 return [field_ isShowingQueryRefinementChip]; |
| 323 } |
| 324 |
| 325 bool OmniboxViewIOS::IsSelectAll() const { |
| 326 return false; |
| 327 } |
| 328 |
| 329 bool OmniboxViewIOS::DeleteAtEndPressed() { |
| 330 return false; |
| 331 } |
| 332 |
| 333 void OmniboxViewIOS::GetSelectionBounds(base::string16::size_type* start, |
| 334 base::string16::size_type* end) const { |
| 335 *start = *end = 0; |
| 336 } |
| 337 |
| 338 gfx::NativeView OmniboxViewIOS::GetNativeView() const { |
| 339 return nullptr; |
| 340 } |
| 341 |
| 342 gfx::NativeView OmniboxViewIOS::GetRelativeWindowForPopup() const { |
| 343 return nullptr; |
| 344 } |
| 345 |
| 346 int OmniboxViewIOS::GetTextWidth() const { |
| 347 return 0; |
| 348 } |
| 349 |
| 350 // TODO(crbug.com/329527): [Merge r241107] implement OmniboxViewIOS::GetWidth(). |
| 351 int OmniboxViewIOS::GetWidth() const { |
| 352 return 0; |
| 353 } |
| 354 |
| 355 void OmniboxViewIOS::OnDidBeginEditing() { |
| 356 // If Open from Clipboard offers a suggestion, the popup may be opened when |
| 357 // |OnSetFocus| is called on the model. The state of the popup is saved early |
| 358 // to ignore that case. |
| 359 bool popup_was_open_before_editing_began = popup_view_->IsOpen(); |
| 360 |
| 361 // Text attributes (e.g. text color) should not be shown while editing, so |
| 362 // strip them out by calling setText (as opposed to setAttributedText). |
| 363 [field_ setText:[field_ text]]; |
| 364 [field_ enableLeftViewButton:NO]; |
| 365 OnBeforePossibleChange(); |
| 366 // In the case where the user taps the fakebox on the Google landing page, |
| 367 // the WebToolbarController invokes OnSetFocus before calling |
| 368 // becomeFirstResponder on OmniboxTextFieldIOS (which leads to this method |
| 369 // beting invoked) so there is no need to call OnSetFocus again. In fact, |
| 370 // calling OnSetFocus again here would reset the caret visibility to true and |
| 371 // it would be impossible to tell that the omnibox was focused by a tap in the |
| 372 // fakebox instead of the omnibox. |
| 373 if (!model()->has_focus()) { |
| 374 model()->OnSetFocus(false); |
| 375 } |
| 376 |
| 377 // If the omnibox is displaying a URL and the popup is not showing, set the |
| 378 // field into pre-editing state. If the omnibox is displaying search terms, |
| 379 // leave the default behavior of positioning the cursor at the end of the |
| 380 // text. If the popup is already open, that means that the omnibox is |
| 381 // regaining focus after a popup scroll took focus away, so the pre-edit |
| 382 // behavior should not be invoked. |
| 383 if (!popup_was_open_before_editing_began) |
| 384 [field_ enterPreEditState]; |
| 385 |
| 386 // The controller looks at the current pre-edit state, so the call to |
| 387 // OnSetFocus() must come after entering pre-edit. |
| 388 controller_->OnSetFocus(); |
| 389 } |
| 390 |
| 391 void OmniboxViewIOS::OnDidEndEditing() { |
| 392 CloseOmniboxPopup(); |
| 393 [field_ enableLeftViewButton:YES]; |
| 394 [field_ setChipText:@""]; |
| 395 model()->OnWillKillFocus(); |
| 396 model()->OnKillFocus(); |
| 397 if ([field_ isPreEditing]) |
| 398 [field_ exitPreEditState]; |
| 399 |
| 400 // The controller looks at the current pre-edit state, so the call to |
| 401 // OnKillFocus() must come after exiting pre-edit. |
| 402 controller_->OnKillFocus(); |
| 403 |
| 404 // Cancel any outstanding preload requests. |
| 405 [preloader_ cancelPrerender]; |
| 406 [preloader_ cancelPrefetch]; |
| 407 |
| 408 // Blow away any in-progress edits. |
| 409 RevertAll(); |
| 410 DCHECK(![field_ hasAutocompleteText]); |
| 411 } |
| 412 |
| 413 bool OmniboxViewIOS::OnWillChange(NSRange range, NSString* new_text) { |
| 414 bool ok_to_change = true; |
| 415 |
| 416 // It may be unsafe to modify the contents of the field. |
| 417 if (ShouldIgnoreUserInputDueToPendingVoiceSearch()) { |
| 418 return false; |
| 419 } |
| 420 |
| 421 if ([field_ isPreEditing]) { |
| 422 [field_ setClearingPreEditText:YES]; |
| 423 |
| 424 // Exit the pre-editing state in OnWillChange() instead of OnDidChange(), as |
| 425 // that allows IME to continue working. The following code clears the text |
| 426 // field but continues the normal text editing flow, so UIKit behaves as |
| 427 // though the user had typed into an empty field. |
| 428 [field_ exitPreEditState]; |
| 429 |
| 430 // Clearing the text field will trigger a call to OnDidChange(). This is |
| 431 // ok, because the autocomplete system will process it as if the user had |
| 432 // deleted all the omnibox text. |
| 433 [field_ setText:@""]; |
| 434 |
| 435 // Reset |range| to be of zero-length at location zero, as the field is now |
| 436 // cleared. |
| 437 range = NSMakeRange(0, 0); |
| 438 } |
| 439 |
| 440 // Figure out the old and current (new) selections. Assume the new selection |
| 441 // will be of zero-length, located at the end of |new_text|. |
| 442 NSRange old_range = range; |
| 443 NSRange new_range = NSMakeRange(range.location + [new_text length], 0); |
| 444 |
| 445 // We may need to fix up the old and new ranges in the case where autocomplete |
| 446 // text was showing. If there is autocomplete text, assume it was selected. |
| 447 // If the change is deleting one character from the end of the actual text, |
| 448 // disallow the change, but clear the autocomplete text and call OnDidChange |
| 449 // directly. If there is autocomplete text AND a text field selection, or if |
| 450 // the user entered multiple characters, clear the autocomplete text and |
| 451 // pretend it never existed. |
| 452 if ([field_ hasAutocompleteText]) { |
| 453 bool adding_text = (range.length < [new_text length]); |
| 454 bool deleting_text = (range.length > [new_text length]); |
| 455 |
| 456 if (adding_text) { |
| 457 // TODO(rohitrao): What about cases where [new_text length] > 1? This |
| 458 // could happen if an IME completion inserts multiple characters at once, |
| 459 // or if the user pastes some text in. Let's loosen this test to allow |
| 460 // multiple characters, as long as the "old range" ends at the end of the |
| 461 // permanent text. |
| 462 if ([new_text length] == 1 && range.location == [[field_ text] length]) { |
| 463 old_range = NSMakeRange([[field_ text] length], |
| 464 [field_ autocompleteText].length()); |
| 465 } |
| 466 } else if (deleting_text) { |
| 467 if ([new_text length] == 0 && |
| 468 range.location == [[field_ text] length] - 1) { |
| 469 ok_to_change = false; |
| 470 } |
| 471 } |
| 472 } |
| 473 |
| 474 // Update variables needed by OnDidChange() and GetState(). |
| 475 old_selection_ = old_range; |
| 476 current_selection_ = new_range; |
| 477 |
| 478 // Store the displayed text. Older version of Chrome used to clear the |
| 479 // autocomplete text here as well, but on iOS7 doing this causes the inline |
| 480 // autocomplete text to flicker, so the call was moved to the start on |
| 481 // OnDidChange(). |
| 482 GetState(&state_before_change_); |
| 483 // Manually update the selection state after calling GetState(). |
| 484 state_before_change_.sel_start = old_selection_.location; |
| 485 state_before_change_.sel_end = |
| 486 old_selection_.location + old_selection_.length; |
| 487 |
| 488 if (!ok_to_change) { |
| 489 // Force a change in the autocomplete system, since we won't get an |
| 490 // OnDidChange() message. |
| 491 OnDidChange(true); |
| 492 } |
| 493 |
| 494 return ok_to_change; |
| 495 } |
| 496 |
| 497 void OmniboxViewIOS::OnDidChange(bool processing_user_event) { |
| 498 // Sanitize pasted text. |
| 499 if (model()->is_pasting()) { |
| 500 base::string16 pastedText = base::SysNSStringToUTF16([field_ text]); |
| 501 base::string16 newText = OmniboxView::SanitizeTextForPaste(pastedText); |
| 502 if (pastedText != newText) { |
| 503 [field_ setText:SysUTF16ToNSString(newText)]; |
| 504 } |
| 505 } |
| 506 |
| 507 // Clear the autocomplete text, since the omnibox model does not expect to see |
| 508 // it in OnAfterPossibleChange(). Clearing the text here should not cause |
| 509 // flicker as the UI will not get a chance to redraw before the new |
| 510 // autocomplete text is set by the model. |
| 511 [field_ clearAutocompleteText]; |
| 512 [field_ setClearingPreEditText:NO]; |
| 513 |
| 514 // Generally do not notify the autocomplete system of a text change unless the |
| 515 // change was a direct result of a user event. One exception is if the marked |
| 516 // text changed, which could happen through a delayed IME recognition |
| 517 // callback. iOS4 does not provide API access to marked text, so use |
| 518 // |IsImeComposing()| as a proxy. |
| 519 bool proceed_without_user_event = false; |
| 520 |
| 521 // The IME exception does not work for Korean text, because Korean does not |
| 522 // seem to ever have marked text. It simply replaces or modifies previous |
| 523 // characters as you type. Always proceed without user input on iOS7 if the |
| 524 // Korean keyboard is currently active. (This Korean exception is not |
| 525 // possible on iOS6 due to crbug.com/285294.) |
| 526 NSString* current_language = [[field_ textInputMode] primaryLanguage]; |
| 527 |
| 528 if ([current_language hasPrefix:@"ko-"]) { |
| 529 proceed_without_user_event = true; |
| 530 } else { |
| 531 NSString* current_marked_text = [field_ markedText]; |
| 532 |
| 533 // The IME exception kicks in if the current marked text is not equal to the |
| 534 // previous marked text. Two nil strings should be considered equal, so |
| 535 // There is logic to avoid calling into isEqual: in that case. |
| 536 proceed_without_user_event = |
| 537 (marked_text_before_change_ || current_marked_text) && |
| 538 ![current_marked_text isEqual:marked_text_before_change_]; |
| 539 } |
| 540 |
| 541 if (!processing_user_event && !proceed_without_user_event) |
| 542 return; |
| 543 |
| 544 // TODO(crbug.com/564599): OnAfterPossibleChange() now takes an argument. It |
| 545 // use to not take an argument and was defaulting to false, so as it is |
| 546 // unclear what the correct value is, using what was that before seems |
| 547 // consistent. |
| 548 OnAfterPossibleChange(false); |
| 549 OnBeforePossibleChange(); |
| 550 } |
| 551 |
| 552 void OmniboxViewIOS::OnAccept() { |
| 553 // It may be unsafe to modify the contents of the field. |
| 554 // Note that by happy coincidence, the |textFieldDidReturn| delegate method |
| 555 // always returns NO, which is the desired behavior in this situation. |
| 556 if (ShouldIgnoreUserInputDueToPendingVoiceSearch()) { |
| 557 return; |
| 558 } |
| 559 |
| 560 base::RecordAction(UserMetricsAction("MobileOmniboxUse")); |
| 561 |
| 562 WindowOpenDisposition disposition = WindowOpenDisposition::CURRENT_TAB; |
| 563 model()->AcceptInput(disposition, false); |
| 564 RevertAll(); |
| 565 } |
| 566 |
| 567 void OmniboxViewIOS::OnClear() { |
| 568 [field_ clearAutocompleteText]; |
| 569 [field_ exitPreEditState]; |
| 570 } |
| 571 |
| 572 bool OmniboxViewIOS::OnCopy() { |
| 573 UIPasteboard* board = [UIPasteboard generalPasteboard]; |
| 574 UITextRange* selected_range = [field_ selectedTextRange]; |
| 575 base::string16 text = |
| 576 base::SysNSStringToUTF16([field_ textInRange:selected_range]); |
| 577 |
| 578 UITextPosition* start = [field_ beginningOfDocument]; |
| 579 UITextPosition* end = [field_ endOfDocument]; |
| 580 BOOL is_select_all = ([field_ comparePosition:[selected_range start] |
| 581 toPosition:start] == NSOrderedSame) && |
| 582 ([field_ comparePosition:[selected_range end] |
| 583 toPosition:end] == NSOrderedSame); |
| 584 |
| 585 // The following call to |-offsetFromPosition:toPosition:| gives the offset in |
| 586 // terms of the number of "visible characters." The documentation does not |
| 587 // specify whether this means glyphs or UTF16 chars. This does not matter for |
| 588 // the current implementation of AdjustTextForCopy(), but it may become an |
| 589 // issue at some point. |
| 590 NSInteger start_location = |
| 591 [field_ offsetFromPosition:start toPosition:[selected_range start]]; |
| 592 |
| 593 GURL url; |
| 594 bool write_url = false; |
| 595 // Don't adjust the text (e.g. add http://) if the omnibox is currently |
| 596 // showing non-URL text (e.g. search terms instead of the URL). |
| 597 if (!CanCopyURL()) { |
| 598 model()->AdjustTextForCopy(start_location, is_select_all, &text, &url, |
| 599 &write_url); |
| 600 } |
| 601 |
| 602 // Create the pasteboard item manually because the pasteboard expects a single |
| 603 // item with multiple representations. This is expressed as a single |
| 604 // NSDictionary with multiple keys, one for each representation. |
| 605 NSMutableDictionary* item = [NSMutableDictionary dictionaryWithCapacity:2]; |
| 606 [item setObject:base::SysUTF16ToNSString(text) |
| 607 forKey:(NSString*)kUTTypePlainText]; |
| 608 |
| 609 if (write_url) |
| 610 [item setObject:net::NSURLWithGURL(url) forKey:(NSString*)kUTTypeURL]; |
| 611 |
| 612 board.items = [NSArray arrayWithObject:item]; |
| 613 return true; |
| 614 } |
| 615 |
| 616 bool OmniboxViewIOS::OnCopyURL() { |
| 617 // Create the pasteboard item manually because the pasteboard expects a single |
| 618 // item with multiple representations. This is expressed as a single |
| 619 // NSDictionary with multiple keys, one for each representation. |
| 620 GURL url = controller_->GetToolbarModel()->GetURL(); |
| 621 NSDictionary* item = @{ |
| 622 (NSString*)kUTTypePlainText : base::SysUTF8ToNSString(url.spec()), |
| 623 (NSString*)kUTTypeURL : net::NSURLWithGURL(url) |
| 624 }; |
| 625 [UIPasteboard generalPasteboard].items = [NSArray arrayWithObject:item]; |
| 626 return true; |
| 627 } |
| 628 |
| 629 bool OmniboxViewIOS::CanCopyURL() { |
| 630 return false; |
| 631 } |
| 632 |
| 633 void OmniboxViewIOS::WillPaste() { |
| 634 model()->OnPaste(); |
| 635 } |
| 636 |
| 637 NSAttributedString* OmniboxViewIOS::ApplyTextAttributes( |
| 638 const base::string16& text) { |
| 639 NSMutableAttributedString* as = [[[NSMutableAttributedString alloc] |
| 640 initWithString:base::SysUTF16ToNSString(text)] autorelease]; |
| 641 url::Component scheme, host; |
| 642 AutocompleteInput::ParseForEmphasizeComponents( |
| 643 text, AutocompleteSchemeClassifierImpl(), &scheme, &host); |
| 644 |
| 645 const bool emphasize = model()->CurrentTextIsURL() && (host.len > 0); |
| 646 if (emphasize) { |
| 647 [as addAttribute:NSForegroundColorAttributeName |
| 648 value:BaseTextColor() |
| 649 range:NSMakeRange(0, [as length])]; |
| 650 |
| 651 [as addAttribute:NSForegroundColorAttributeName |
| 652 value:[field_ displayedTextColor] |
| 653 range:ComponentToNSRange(host)]; |
| 654 |
| 655 if (scheme.len > 0) { |
| 656 UIColor* color = nil; |
| 657 switch (controller_->GetToolbarModel()->GetSecurityLevel(false)) { |
| 658 case security_state::NONE: |
| 659 break; |
| 660 case security_state::SECURITY_WARNING: |
| 661 // Don't color strikethrough schemes. See https://crbug.com/635004#c6 |
| 662 if (![field_ incognito]) |
| 663 color = WarningTextColor(); |
| 664 [as addAttribute:NSStrikethroughStyleAttributeName |
| 665 value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] |
| 666 range:ComponentToNSRange(scheme)]; |
| 667 break; |
| 668 case security_state::SECURE: |
| 669 case security_state::EV_SECURE: |
| 670 color = [field_ incognito] ? IncognitoSecureTextColor() |
| 671 : SecureTextColor(); |
| 672 break; |
| 673 case security_state::DANGEROUS: |
| 674 // Don't color strikethrough schemes. See https://crbug.com/635004#c6 |
| 675 if (![field_ incognito]) |
| 676 color = ErrorTextColor(); |
| 677 [as addAttribute:NSStrikethroughStyleAttributeName |
| 678 value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] |
| 679 range:ComponentToNSRange(scheme)]; |
| 680 break; |
| 681 case security_state::HTTP_SHOW_WARNING: |
| 682 case security_state::SECURE_WITH_POLICY_INSTALLED_CERT: |
| 683 NOTREACHED(); |
| 684 } |
| 685 if (color) { |
| 686 [as addAttribute:NSForegroundColorAttributeName |
| 687 value:color |
| 688 range:ComponentToNSRange(scheme)]; |
| 689 } |
| 690 } |
| 691 } |
| 692 return as; |
| 693 } |
| 694 |
| 695 void OmniboxViewIOS::UpdateAppearance() { |
| 696 // If Siri is thinking, treat that as user input being in progress. It is |
| 697 // unsafe to modify the text field while voice entry is pending. |
| 698 if (model()->UpdatePermanentText()) { |
| 699 // Revert everything to the baseline look. |
| 700 RevertAll(); |
| 701 } else if (!model()->has_focus() && |
| 702 !ShouldIgnoreUserInputDueToPendingVoiceSearch()) { |
| 703 // Only update the chip text if the omnibox is not currently focused. |
| 704 [field_ setChipText:@""]; |
| 705 |
| 706 // Even if the change wasn't "user visible" to the model, it still may be |
| 707 // necessary to re-color to the URL string. Only do this if the omnibox is |
| 708 // not currently focused. |
| 709 base::string16 text = |
| 710 controller_->GetToolbarModel()->GetFormattedURL(nullptr); |
| 711 NSAttributedString* as = ApplyTextAttributes(text); |
| 712 [field_ setText:as userTextLength:[as length]]; |
| 713 } |
| 714 } |
| 715 |
| 716 void OmniboxViewIOS::OnDeleteBackward() { |
| 717 if ([field_ text].length == 0) { |
| 718 // If the user taps backspace while the pre-edit text is showing, |
| 719 // OnWillChange is invoked before this method and sets the text to an empty |
| 720 // string, so use the |clearingPreEditText| to determine if the chip should |
| 721 // be cleared or not. |
| 722 if ([field_ clearingPreEditText]) { |
| 723 // In the case where backspace is tapped while in pre-edit mode, |
| 724 // OnWillChange is called but OnDidChange is never called so ensure the |
| 725 // clearingPreEditText flag is set to false again. |
| 726 [field_ setClearingPreEditText:NO]; |
| 727 // Explicitly set the input-in-progress flag. Normally this is set via |
| 728 // in model()->OnAfterPossibleChange, but in this case the text has been |
| 729 // set to the empty string by OnWillChange so when OnAfterPossibleChange |
| 730 // checks if the text has changed it does not see any difference so it |
| 731 // never sets the input-in-progress flag. |
| 732 model()->SetInputInProgress(YES); |
| 733 } else { |
| 734 RemoveQueryRefinementChip(); |
| 735 } |
| 736 } |
| 737 } |
| 738 |
| 739 void OmniboxViewIOS::ClearText() { |
| 740 // It may be unsafe to modify the contents of the field. |
| 741 if (ShouldIgnoreUserInputDueToPendingVoiceSearch()) { |
| 742 return; |
| 743 } |
| 744 |
| 745 // Ensure omnibox is first responder. This will bring up the keyboard so the |
| 746 // user can start typing a new query. |
| 747 if (![field_ isFirstResponder]) |
| 748 [field_ becomeFirstResponder]; |
| 749 if ([field_ text].length == 0) { |
| 750 // If |field_| is empty, remove the query refinement chip. |
| 751 RemoveQueryRefinementChip(); |
| 752 } else { |
| 753 // Otherwise, just remove the text in the omnibox. |
| 754 // Since iOS 6, calling |setText| does not trigger |textDidChange| so it |
| 755 // must be called explicitly. |
| 756 OnClear(); |
| 757 [field_ setText:@""]; |
| 758 OnDidChange(YES); |
| 759 } |
| 760 // Calling OnDidChange() can trigger a scroll event, which removes focus from |
| 761 // the omnibox. |
| 762 [field_ becomeFirstResponder]; |
| 763 } |
| 764 |
| 765 void OmniboxViewIOS::RemoveQueryRefinementChip() { |
| 766 [field_ setChipText:nil]; |
| 767 controller_->OnChanged(); |
| 768 } |
| 769 |
| 770 bool OmniboxViewIOS::ShouldIgnoreUserInputDueToPendingVoiceSearch() { |
| 771 // When the response of the iOS voice entry is pending a spinning wheel is |
| 772 // visible. The spinner's location is marked in [self text] as a Unicode |
| 773 // "Object Replacement Character". |
| 774 // http://www.fileformat.info/info/unicode/char/fffc/index.htm |
| 775 NSString* objectReplacementChar = |
| 776 [NSString stringWithFormat:@"%C", (unichar)0xFFFC]; |
| 777 return [[field_ text] rangeOfString:objectReplacementChar].length > 0; |
| 778 } |
| 779 |
| 780 void OmniboxViewIOS::SetLeftImage(int imageId) { |
| 781 [field_ setPlaceholderImage:imageId]; |
| 782 } |
| 783 |
| 784 void OmniboxViewIOS::HideKeyboardAndEndEditing() { |
| 785 [field_ resignFirstResponder]; |
| 786 |
| 787 // Handle the case where a phone-format ombniox has already resigned first |
| 788 // responder because the popup was scrolled. If the model still has focus, |
| 789 // dismiss again. This should only happen on iPhone. |
| 790 if (model()->has_focus()) { |
| 791 DCHECK(!IsIPadIdiom()); |
| 792 this->OnDidEndEditing(); |
| 793 } |
| 794 } |
| 795 |
| 796 void OmniboxViewIOS::HideKeyboard() { |
| 797 [field_ resignFirstResponder]; |
| 798 } |
| 799 |
| 800 void OmniboxViewIOS::FocusOmnibox() { |
| 801 [field_ becomeFirstResponder]; |
| 802 } |
| 803 |
| 804 // Called whenever the popup results change. May trigger the URLs of |
| 805 // autocomplete results to be prerendered or prefetched. |
| 806 void OmniboxViewIOS::OnPopupResultsChanged(const AutocompleteResult& result) { |
| 807 if (!ignore_popup_updates_ && !result.empty()) { |
| 808 const AutocompleteMatch& match = result.match_at(0); |
| 809 bool is_inline_autocomplete = !match.inline_autocompletion.empty(); |
| 810 |
| 811 // TODO(rohitrao): When prerendering the result of a paste operation, we |
| 812 // should change the transition to LINK instead of TYPED. b/6143631. |
| 813 |
| 814 // Only prerender HISTORY_URL matches, which come from the history DB. Do |
| 815 // not prerender other types of matches, including matches from the search |
| 816 // provider. |
| 817 if (is_inline_autocomplete && |
| 818 match.type == AutocompleteMatchType::HISTORY_URL) { |
| 819 ui::PageTransition transition = ui::PageTransitionFromInt( |
| 820 match.transition | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR); |
| 821 [preloader_ prerenderURL:match.destination_url |
| 822 referrer:web::Referrer() |
| 823 transition:transition |
| 824 immediately:is_inline_autocomplete]; |
| 825 } else { |
| 826 [preloader_ cancelPrerender]; |
| 827 } |
| 828 |
| 829 // If the first autocomplete result is a search suggestion, prefetch the |
| 830 // corresponding search result page. |
| 831 if (match.type == AutocompleteMatchType::SEARCH_SUGGEST) { |
| 832 ui::PageTransition transition = ui::PageTransitionFromInt( |
| 833 match.transition | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR); |
| 834 [preloader_ prefetchURL:match.destination_url transition:transition]; |
| 835 } else { |
| 836 [preloader_ cancelPrefetch]; |
| 837 } |
| 838 } |
| 839 } |
| 840 |
| 841 BOOL OmniboxViewIOS::IsPopupOpen() { |
| 842 return popup_view_->IsOpen(); |
| 843 } |
| 844 |
| 845 int OmniboxViewIOS::GetIcon(bool offlinePage) const { |
| 846 if (!IsEditingOrEmpty()) { |
| 847 if (offlinePage) { |
| 848 return IDR_IOS_OMNIBOX_OFFLINE; |
| 849 } |
| 850 return GetIconForSecurityState( |
| 851 controller()->GetToolbarModel()->GetSecurityLevel(false)); |
| 852 } |
| 853 |
| 854 return GetIconForAutocompleteMatchType( |
| 855 model() ? model()->CurrentTextType() |
| 856 : AutocompleteMatchType::URL_WHAT_YOU_TYPED, |
| 857 /* is_starred */ false, /* is_incognito */ false); |
| 858 } |
| 859 |
| 860 int OmniboxViewIOS::GetOmniboxTextLength() const { |
| 861 return [field_ displayedText].length(); |
| 862 } |
| 863 |
| 864 void OmniboxViewIOS::EmphasizeURLComponents() { |
| 865 // TODO(rohitrao): Implement this function using code like below. This code |
| 866 // is being left out for now because it was not present before the OmniboxView |
| 867 // rewrite. |
| 868 #if 0 |
| 869 // When editing is in progress, the url text is not colored, so there is |
| 870 // nothing to emphasize. (Calling SetText() in that situation would also be |
| 871 // harmful, as it would reset the carat position to the end of the text.) |
| 872 if (!IsEditingOrEmpty()) |
| 873 SetText(GetText()); |
| 874 #endif |
| 875 } |
OLD | NEW |