1 // Copyright (c) 2011 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 "chrome/browser/autocomplete/autocomplete_edit.h" 6 7 #include <string> 8 9 #include "base/basictypes.h" 10 #include "base/metrics/histogram.h" 11 #include "base/string_util.h" 12 #include "base/utf_string_conversions.h" 13 #include "chrome/app/chrome_command_ids.h" 14 #include "chrome/browser/autocomplete/autocomplete_classifier.h" 15 #include "chrome/browser/autocomplete/autocomplete_edit_view.h" 16 #include "chrome/browser/autocomplete/autocomplete_match.h" 17 #include "chrome/browser/autocomplete/autocomplete_popup_model.h" 18 #include "chrome/browser/autocomplete/autocomplete_popup_view.h" 19 #include "chrome/browser/autocomplete/keyword_provider.h" 20 #include "chrome/browser/autocomplete/search_provider.h" 21 #include "chrome/browser/command_updater.h" 22 #include "chrome/browser/extensions/extension_omnibox_api.h" 23 #include "chrome/browser/google/google_url_tracker.h" 24 #include "chrome/browser/instant/instant_controller.h" 25 #include "chrome/browser/metrics/user_metrics.h" 26 #include "chrome/browser/net/predictor_api.h" 27 #include "chrome/browser/net/url_fixer_upper.h" 28 #include "chrome/browser/profiles/profile.h" 29 #include "chrome/browser/search_engines/template_url.h" 30 #include "chrome/browser/search_engines/template_url_model.h" 31 #include "chrome/browser/ui/browser_list.h" 32 #include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h" 33 #include "chrome/common/url_constants.h" 34 #include "content/common/notification_service.h" 35 #include "googleurl/src/gurl.h" 36 #include "googleurl/src/url_util.h" 37 #include "third_party/skia/include/core/SkBitmap.h" 38 39 /////////////////////////////////////////////////////////////////////////////// 40 // AutocompleteEditController 41 42 AutocompleteEditController::~AutocompleteEditController() { 43 } 44 45 /////////////////////////////////////////////////////////////////////////////// 46 // AutocompleteEditModel::State 47 48 AutocompleteEditModel::State::State(bool user_input_in_progress, 49 const string16& user_text, 50 const string16& keyword, 51 bool is_keyword_hint) 52 : user_input_in_progress(user_input_in_progress), 53 user_text(user_text), 54 keyword(keyword), 55 is_keyword_hint(is_keyword_hint) { 56 } 57 58 AutocompleteEditModel::State::~State() { 59 } 60 61 /////////////////////////////////////////////////////////////////////////////// 62 // AutocompleteEditModel 63 64 AutocompleteEditModel::AutocompleteEditModel( 65 AutocompleteEditView* view, 66 AutocompleteEditController* controller, 67 Profile* profile) 68 : ALLOW_THIS_IN_INITIALIZER_LIST( 69 autocomplete_controller_(new AutocompleteController(profile, this))), 70 view_(view), 71 popup_(NULL), 72 controller_(controller), 73 has_focus_(false), 74 user_input_in_progress_(false), 75 just_deleted_text_(false), 76 has_temporary_text_(false), 77 paste_state_(NONE), 78 control_key_state_(UP), 79 is_keyword_hint_(false), 80 paste_and_go_transition_(PageTransition::TYPED), 81 profile_(profile), 82 update_instant_(true), 83 allow_exact_keyword_match_(false), 84 instant_complete_behavior_(INSTANT_COMPLETE_DELAYED) { 85 } 86 87 AutocompleteEditModel::~AutocompleteEditModel() { 88 } 89 90 void AutocompleteEditModel::SetProfile(Profile* profile) { 91 DCHECK(profile); 92 profile_ = profile; 93 autocomplete_controller_->SetProfile(profile); 94 popup_->set_profile(profile); 95 } 96 97 const AutocompleteEditModel::State 98 AutocompleteEditModel::GetStateForTabSwitch() { 99 // Like typing, switching tabs "accepts" the temporary text as the user 100 // text, because it makes little sense to have temporary text when the 101 // popup is closed. 102 if (user_input_in_progress_) { 103 // Weird edge case to match other browsers: if the edit is empty, revert to 104 // the permanent text (so the user can get it back easily) but select it (so 105 // on switching back, typing will "just work"). 106 const string16 user_text(UserTextFromDisplayText(view_->GetText())); 107 if (user_text.empty()) { 108 view_->RevertAll(); 109 view_->SelectAll(true); 110 } else { 111 InternalSetUserText(user_text); 112 } 113 } 114 115 return State(user_input_in_progress_, user_text_, keyword_, is_keyword_hint_); 116 } 117 118 void AutocompleteEditModel::RestoreState(const State& state) { 119 // Restore any user editing. 120 if (state.user_input_in_progress) { 121 // NOTE: Be sure and set keyword-related state BEFORE invoking 122 // DisplayTextFromUserText(), as its result depends upon this state. 123 keyword_ = state.keyword; 124 is_keyword_hint_ = state.is_keyword_hint; 125 view_->SetUserText(state.user_text, 126 DisplayTextFromUserText(state.user_text), false); 127 } 128 } 129 130 AutocompleteMatch AutocompleteEditModel::CurrentMatch() { 131 AutocompleteMatch match; 132 GetInfoForCurrentText(&match, NULL); 133 return match; 134 } 135 136 bool AutocompleteEditModel::UpdatePermanentText( 137 const string16& new_permanent_text) { 138 // When there's a new URL, and the user is not editing anything or the edit 139 // doesn't have focus, we want to revert the edit to show the new URL. (The 140 // common case where the edit doesn't have focus is when the user has started 141 // an edit and then abandoned it and clicked a link on the page.) 142 const bool visibly_changed_permanent_text = 143 (permanent_text_ != new_permanent_text) && 144 (!user_input_in_progress_ || !has_focus_); 145 146 permanent_text_ = new_permanent_text; 147 return visibly_changed_permanent_text; 148 } 149 150 GURL AutocompleteEditModel::PermanentURL() { 151 return URLFixerUpper::FixupURL(UTF16ToUTF8(permanent_text_), std::string()); 152 } 153 154 void AutocompleteEditModel::SetUserText(const string16& text) { 155 SetInputInProgress(true); 156 InternalSetUserText(text); 157 paste_state_ = NONE; 158 has_temporary_text_ = false; 159 } 160 161 void AutocompleteEditModel::FinalizeInstantQuery( 162 const string16& input_text, 163 const string16& suggest_text, 164 bool skip_inline_autocomplete) { 165 if (skip_inline_autocomplete) { 166 const string16 final_text = input_text + suggest_text; 167 view_->OnBeforePossibleChange(); 168 view_->SetWindowTextAndCaretPos(final_text, final_text.length()); 169 view_->OnAfterPossibleChange(); 170 } else if (popup_->IsOpen()) { 171 SearchProvider* search_provider = 172 autocomplete_controller_->search_provider(); 173 search_provider->FinalizeInstantQuery(input_text, suggest_text); 174 } 175 } 176 177 void AutocompleteEditModel::SetSuggestedText( 178 const string16& text, 179 InstantCompleteBehavior behavior) { 180 instant_complete_behavior_ = behavior; 181 if (instant_complete_behavior_ == INSTANT_COMPLETE_NOW) { 182 if (!text.empty()) 183 FinalizeInstantQuery(view_->GetText(), text, false); 184 else 185 view_->SetInstantSuggestion(text, false); 186 } else { 187 DCHECK((behavior == INSTANT_COMPLETE_DELAYED) || 188 (behavior == INSTANT_COMPLETE_NEVER)); 189 view_->SetInstantSuggestion(text, behavior == INSTANT_COMPLETE_DELAYED); 190 } 191 } 192 193 bool AutocompleteEditModel::CommitSuggestedText(bool skip_inline_autocomplete) { 194 if (!controller_->GetInstant()) 195 return false; 196 197 const string16 suggestion = view_->GetInstantSuggestion(); 198 if (suggestion.empty()) 199 return false; 200 201 FinalizeInstantQuery(view_->GetText(), suggestion, skip_inline_autocomplete); 202 return true; 203 } 204 205 bool AutocompleteEditModel::AcceptCurrentInstantPreview() { 206 return InstantController::CommitIfCurrent(controller_->GetInstant()); 207 } 208 209 void AutocompleteEditModel::OnChanged() { 210 InstantController* instant = controller_->GetInstant(); 211 string16 suggested_text; 212 TabContentsWrapper* tab = controller_->GetTabContentsWrapper(); 213 bool might_support_instant = false; 214 if (update_instant_ && instant && tab) { 215 if (user_input_in_progress() && popup_->IsOpen()) { 216 AutocompleteMatch current_match = CurrentMatch(); 217 if (current_match.destination_url == PermanentURL()) { 218 // The destination is the same as the current url. This typically 219 // happens if the user presses the down error in the omnibox, in which 220 // case we don't want to load a preview. 221 instant->DestroyPreviewContentsAndLeaveActive(); 222 } else { 223 instant->Update(tab, CurrentMatch(), view_->GetText(), 224 UseVerbatimInstant(), &suggested_text); 225 } 226 } else { 227 instant->DestroyPreviewContents(); 228 } 229 might_support_instant = instant->MightSupportInstant(); 230 } 231 232 if (!might_support_instant) { 233 // Hide any suggestions we might be showing. 234 view_->SetInstantSuggestion(string16(), false); 235 236 // No need to wait any longer for instant. 237 FinalizeInstantQuery(string16(), string16(), false); 238 } else { 239 SetSuggestedText(suggested_text, instant_complete_behavior_); 240 } 241 242 controller_->OnChanged(); 243 } 244 245 void AutocompleteEditModel::GetDataForURLExport(GURL* url, 246 string16* title, 247 SkBitmap* favicon) { 248 AutocompleteMatch match; 249 GetInfoForCurrentText(&match, NULL); 250 *url = match.destination_url; 251 if (*url == URLFixerUpper::FixupURL(UTF16ToUTF8(permanent_text_), 252 std::string())) { 253 *title = controller_->GetTitle(); 254 *favicon = controller_->GetFavicon(); 255 } 256 } 257 258 bool AutocompleteEditModel::UseVerbatimInstant() { 259 #if defined(OS_MACOSX) 260 // TODO(suzhe): Fix Mac port to display Instant suggest in a separated NSView, 261 // so that we can display instant suggest along with composition text. 262 const AutocompleteInput& input = autocomplete_controller_->input(); 263 if (input.initial_prevent_inline_autocomplete()) 264 return true; 265 #endif 266 267 // The value of input.initial_prevent_inline_autocomplete() is determined by 268 // following conditions: 269 // 1. If the caret is at the end of the text (checked below). 270 // 2. If it's in IME composition mode. 271 // As we use a separated widget for displaying the instant suggest, it won't 272 // interfere with IME composition, so we don't need to care about the value of 273 // input.initial_prevent_inline_autocomplete() here. 274 if (view_->DeleteAtEndPressed() || (popup_->selected_line() != 0) || 275 just_deleted_text_) 276 return true; 277 278 string16::size_type start, end; 279 view_->GetSelectionBounds(&start, &end); 280 return (start != end) || (start != view_->GetText().size()); 281 } 282 283 string16 AutocompleteEditModel::GetDesiredTLD() const { 284 // Tricky corner case: The user has typed "foo" and currently sees an inline 285 // autocomplete suggestion of "foo.net". He now presses ctrl-a (e.g. to 286 // select all, on Windows). If we treat the ctrl press as potentially for the 287 // sake of ctrl-enter, then we risk "www.foo.com" being promoted as the best 288 // match. This would make the autocompleted text disappear, leaving our user 289 // feeling very confused when the wrong text gets highlighted. 290 // 291 // Thus, we only treat the user as pressing ctrl-enter when the user presses 292 // ctrl without any fragile state built up in the omnibox: 293 // * the contents of the omnibox have not changed since the keypress, 294 // * there is no autocompleted text visible, and 295 // * the user is not typing a keyword query. 296 return (control_key_state_ == DOWN_WITHOUT_CHANGE && 297 inline_autocomplete_text_.empty() && !KeywordIsSelected())? 298 ASCIIToUTF16("com") : string16(); 299 } 300 301 bool AutocompleteEditModel::CurrentTextIsURL() const { 302 // If !user_input_in_progress_, the permanent text is showing, which should 303 // always be a URL, so no further checking is needed. By avoiding checking in 304 // this case, we avoid calling into the autocomplete providers, and thus 305 // initializing the history system, as long as possible, which speeds startup. 306 if (!user_input_in_progress_) 307 return true; 308 309 AutocompleteMatch match; 310 GetInfoForCurrentText(&match, NULL); 311 return match.transition == PageTransition::TYPED; 312 } 313 314 AutocompleteMatch::Type AutocompleteEditModel::CurrentTextType() const { 315 AutocompleteMatch match; 316 GetInfoForCurrentText(&match, NULL); 317 return match.type; 318 } 319 320 void AutocompleteEditModel::AdjustTextForCopy(int sel_min, 321 bool is_all_selected, 322 string16* text, 323 GURL* url, 324 bool* write_url) { 325 *write_url = false; 326 327 if (sel_min != 0) 328 return; 329 330 // We can't use CurrentTextIsURL() or GetDataForURLExport() because right now 331 // the user is probably holding down control to cause the copy, which will 332 // screw up our calculation of the desired_tld. 333 if (!GetURLForText(*text, url)) 334 return; // Can't be parsed as a url, no need to adjust text. 335 336 if (!user_input_in_progress() && is_all_selected) { 337 // The user selected all the text and has not edited it. Use the url as the 338 // text so that if the scheme was stripped it's added back, and the url 339 // is unescaped (we escape parts of the url for display). 340 *text = UTF8ToUTF16(url->spec()); 341 *write_url = true; 342 return; 343 } 344 345 // Prefix the text with 'http://' if the text doesn't start with 'http://', 346 // the text parses as a url with a scheme of http, the user selected the 347 // entire host, and the user hasn't edited the host or manually removed the 348 // scheme. 349 GURL perm_url; 350 if (GetURLForText(permanent_text_, &perm_url) && 351 perm_url.SchemeIs(chrome::kHttpScheme) && 352 url->SchemeIs(chrome::kHttpScheme) && 353 perm_url.host() == url->host()) { 354 *write_url = true; 355 356 string16 http = ASCIIToUTF16(chrome::kHttpScheme) + 357 ASCIIToUTF16(chrome::kStandardSchemeSeparator); 358 if (text->compare(0, http.length(), http) != 0) 359 *text = http + *text; 360 } 361 } 362 363 void AutocompleteEditModel::SetInputInProgress(bool in_progress) { 364 if (user_input_in_progress_ == in_progress) 365 return; 366 367 user_input_in_progress_ = in_progress; 368 controller_->OnInputInProgress(in_progress); 369 } 370 371 void AutocompleteEditModel::Revert() { 372 SetInputInProgress(false); 373 paste_state_ = NONE; 374 InternalSetUserText(string16()); 375 keyword_.clear(); 376 is_keyword_hint_ = false; 377 has_temporary_text_ = false; 378 view_->SetWindowTextAndCaretPos(permanent_text_, 379 has_focus_ ? permanent_text_.length() : 0); 380 } 381 382 void AutocompleteEditModel::StartAutocomplete( 383 bool has_selected_text, 384 bool prevent_inline_autocomplete) const { 385 bool keyword_is_selected = KeywordIsSelected(); 386 popup_->SetHoveredLine(AutocompletePopupModel::kNoMatch); 387 // We don't explicitly clear AutocompletePopupModel::manually_selected_match, 388 // as Start ends up invoking AutocompletePopupModel::OnResultChanged which 389 // clears it. 390 autocomplete_controller_->Start( 391 user_text_, GetDesiredTLD(), 392 prevent_inline_autocomplete || just_deleted_text_ || 393 (has_selected_text && inline_autocomplete_text_.empty()) || 394 (paste_state_ != NONE), keyword_is_selected, 395 keyword_is_selected || allow_exact_keyword_match_, 396 AutocompleteInput::ALL_MATCHES); 397 } 398 399 void AutocompleteEditModel::StopAutocomplete() { 400 if (popup_->IsOpen() && update_instant_) { 401 InstantController* instant = controller_->GetInstant(); 402 if (instant && !instant->commit_on_mouse_up()) 403 instant->DestroyPreviewContents(); 404 } 405 406 autocomplete_controller_->Stop(true); 407 } 408 409 bool AutocompleteEditModel::CanPasteAndGo(const string16& text) const { 410 if (!view_->GetCommandUpdater()->IsCommandEnabled(IDC_OPEN_CURRENT_URL)) 411 return false; 412 413 AutocompleteMatch match; 414 profile_->GetAutocompleteClassifier()->Classify(text, string16(), false, 415 &match, &paste_and_go_alternate_nav_url_); 416 paste_and_go_url_ = match.destination_url; 417 paste_and_go_transition_ = match.transition; 418 return paste_and_go_url_.is_valid(); 419 } 420 421 void AutocompleteEditModel::PasteAndGo() { 422 // The final parameter to OpenURL, keyword, is not quite correct here: it's 423 // possible to "paste and go" a string that contains a keyword. This is 424 // enough of an edge case that we ignore this possibility. 425 view_->RevertAll(); 426 view_->OpenURL(paste_and_go_url_, CURRENT_TAB, paste_and_go_transition_, 427 paste_and_go_alternate_nav_url_, AutocompletePopupModel::kNoMatch, 428 string16()); 429 } 430 431 void AutocompleteEditModel::AcceptInput(WindowOpenDisposition disposition, 432 bool for_drop) { 433 // Get the URL and transition type for the selected entry. 434 AutocompleteMatch match; 435 GURL alternate_nav_url; 436 GetInfoForCurrentText(&match, &alternate_nav_url); 437 438 if (!match.destination_url.is_valid()) 439 return; 440 441 if ((match.transition == PageTransition::TYPED) && (match.destination_url == 442 URLFixerUpper::FixupURL(UTF16ToUTF8(permanent_text_), std::string()))) { 443 // When the user hit enter on the existing permanent URL, treat it like a 444 // reload for scoring purposes. We could detect this by just checking 445 // user_input_in_progress_, but it seems better to treat "edits" that end 446 // up leaving the URL unchanged (e.g. deleting the last character and then 447 // retyping it) as reloads too. We exclude non-TYPED transitions because if 448 // the transition is GENERATED, the user input something that looked 449 // different from the current URL, even if it wound up at the same place 450 // (e.g. manually retyping the same search query), and it seems wrong to 451 // treat this as a reload. 452 match.transition = PageTransition::RELOAD; 453 } else if (for_drop || ((paste_state_ != NONE) && 454 match.is_history_what_you_typed_match)) { 455 // When the user pasted in a URL and hit enter, score it like a link click 456 // rather than a normal typed URL, so it doesn't get inline autocompleted 457 // as aggressively later. 458 match.transition = PageTransition::LINK; 459 } 460 461 if (match.type == AutocompleteMatch::SEARCH_WHAT_YOU_TYPED || 462 match.type == AutocompleteMatch::SEARCH_HISTORY || 463 match.type == AutocompleteMatch::SEARCH_SUGGEST) { 464 const TemplateURL* default_provider = 465 profile_->GetTemplateURLModel()->GetDefaultSearchProvider(); 466 if (default_provider && default_provider->url() && 467 default_provider->url()->HasGoogleBaseURLs()) { 468 GoogleURLTracker::GoogleURLSearchCommitted(); 469 #if defined(OS_WIN) && defined(GOOGLE_CHROME_BUILD) 470 // TODO(pastarmovj): Remove these metrics once we have proven that (close 471 // to) none searches that should have RLZ are sent out without one. 472 default_provider->url()->CollectRLZMetrics(); 473 #endif 474 } 475 } 476 view_->OpenURL(match.destination_url, disposition, match.transition, 477 alternate_nav_url, AutocompletePopupModel::kNoMatch, 478 is_keyword_hint_ ? string16() : keyword_); 479 } 480 481 void AutocompleteEditModel::OpenURL(const GURL& url, 482 WindowOpenDisposition disposition, 483 PageTransition::Type transition, 484 const GURL& alternate_nav_url, 485 size_t index, 486 const string16& keyword) { 487 // We only care about cases where there is a selection (i.e. the popup is 488 // open). 489 if (popup_->IsOpen()) { 490 AutocompleteLog log(autocomplete_controller_->input().text(), 491 autocomplete_controller_->input().type(), 492 popup_->selected_line(), 0, result()); 493 if (index != AutocompletePopupModel::kNoMatch) 494 log.selected_index = index; 495 else if (!has_temporary_text_) 496 log.inline_autocompleted_length = inline_autocomplete_text_.length(); 497 NotificationService::current()->Notify( 498 NotificationType::OMNIBOX_OPENED_URL, Source<Profile>(profile_), 499 Details<AutocompleteLog>(&log)); 500 } 501 502 TemplateURLModel* template_url_model = profile_->GetTemplateURLModel(); 503 if (template_url_model && !keyword.empty()) { 504 const TemplateURL* const template_url = 505 template_url_model->GetTemplateURLForKeyword(keyword); 506 507 // Special case for extension keywords. Don't increment usage count for 508 // these. 509 if (template_url && template_url->IsExtensionKeyword()) { 510 AutocompleteMatch current_match; 511 GetInfoForCurrentText(¤t_match, NULL); 512 513 const AutocompleteMatch& match = 514 index == AutocompletePopupModel::kNoMatch ? 515 current_match : result().match_at(index); 516 517 // Strip the keyword + leading space off the input. 518 size_t prefix_length = match.template_url->keyword().size() + 1; 519 ExtensionOmniboxEventRouter::OnInputEntered( 520 profile_, match.template_url->GetExtensionId(), 521 UTF16ToUTF8(match.fill_into_edit.substr(prefix_length))); 522 view_->RevertAll(); 523 return; 524 } 525 526 if (template_url) { 527 UserMetrics::RecordAction(UserMetricsAction("AcceptedKeyword"), profile_); 528 template_url_model->IncrementUsageCount(template_url); 529 } 530 531 // NOTE: We purposefully don't increment the usage count of the default 532 // search engine, if applicable; see comments in template_url.h. 533 } 534 535 if (disposition != NEW_BACKGROUND_TAB) { 536 update_instant_ = false; 537 view_->RevertAll(); // Revert the box to its unedited state 538 } 539 controller_->OnAutocompleteAccept(url, disposition, transition, 540 alternate_nav_url); 541 542 InstantController* instant = controller_->GetInstant(); 543 if (instant && !popup_->IsOpen()) 544 instant->DestroyPreviewContents(); 545 update_instant_ = true; 546 } 547 548 bool AutocompleteEditModel::AcceptKeyword() { 549 DCHECK(is_keyword_hint_ && !keyword_.empty()); 550 551 view_->OnBeforePossibleChange(); 552 view_->SetWindowTextAndCaretPos(string16(), 0); 553 is_keyword_hint_ = false; 554 view_->OnAfterPossibleChange(); 555 just_deleted_text_ = false; // OnAfterPossibleChange() erroneously sets this 556 // since the edit contents have disappeared. It 557 // doesn't really matter, but we clear it to be 558 // consistent. 559 UserMetrics::RecordAction(UserMetricsAction("AcceptedKeywordHint"), profile_); 560 return true; 561 } 562 563 void AutocompleteEditModel::ClearKeyword(const string16& visible_text) { 564 view_->OnBeforePossibleChange(); 565 const string16 window_text(keyword_ + visible_text); 566 view_->SetWindowTextAndCaretPos(window_text.c_str(), keyword_.length()); 567 keyword_.clear(); 568 is_keyword_hint_ = false; 569 view_->OnAfterPossibleChange(); 570 just_deleted_text_ = true; // OnAfterPossibleChange() fails to clear this 571 // since the edit contents have actually grown 572 // longer. 573 } 574 575 const AutocompleteResult& AutocompleteEditModel::result() const { 576 return autocomplete_controller_->result(); 577 } 578 579 void AutocompleteEditModel::OnSetFocus(bool control_down) { 580 has_focus_ = true; 581 control_key_state_ = control_down ? DOWN_WITHOUT_CHANGE : UP; 582 NotificationService::current()->Notify( 583 NotificationType::AUTOCOMPLETE_EDIT_FOCUSED, 584 Source<AutocompleteEditModel>(this), 585 NotificationService::NoDetails()); 586 } 587 588 void AutocompleteEditModel::OnWillKillFocus( 589 gfx::NativeView view_gaining_focus) { 590 SetSuggestedText(string16(), INSTANT_COMPLETE_NOW); 591 592 InstantController* instant = controller_->GetInstant(); 593 if (instant) 594 instant->OnAutocompleteLostFocus(view_gaining_focus); 595 } 596 597 void AutocompleteEditModel::OnKillFocus() { 598 has_focus_ = false; 599 control_key_state_ = UP; 600 paste_state_ = NONE; 601 } 602 603 bool AutocompleteEditModel::OnEscapeKeyPressed() { 604 if (has_temporary_text_) { 605 AutocompleteMatch match; 606 InfoForCurrentSelection(&match, NULL); 607 if (match.destination_url != original_url_) { 608 RevertTemporaryText(true); 609 return true; 610 } 611 } 612 613 // If the user wasn't editing, but merely had focus in the edit, allow <esc> 614 // to be processed as an accelerator, so it can still be used to stop a load. 615 // When the permanent text isn't all selected we still fall through to the 616 // SelectAll() call below so users can arrow around in the text and then hit 617 // <esc> to quickly replace all the text; this matches IE. 618 if (!user_input_in_progress_ && view_->IsSelectAll()) 619 return false; 620 621 view_->RevertAll(); 622 view_->SelectAll(true); 623 return true; 624 } 625 626 void AutocompleteEditModel::OnControlKeyChanged(bool pressed) { 627 // Don't change anything unless the key state is actually toggling. 628 if (pressed == (control_key_state_ == UP)) { 629 ControlKeyState old_state = control_key_state_; 630 control_key_state_ = pressed ? DOWN_WITHOUT_CHANGE : UP; 631 if ((control_key_state_ == DOWN_WITHOUT_CHANGE) && has_temporary_text_) { 632 // Arrowing down and then hitting control accepts the temporary text as 633 // the input text. 634 InternalSetUserText(UserTextFromDisplayText(view_->GetText())); 635 has_temporary_text_ = false; 636 if (KeywordIsSelected()) 637 AcceptKeyword(); 638 } 639 if ((old_state != DOWN_WITH_CHANGE) && popup_->IsOpen()) { 640 // Autocomplete history provider results may change, so refresh the 641 // popup. This will force user_input_in_progress_ to true, but if the 642 // popup is open, that should have already been the case. 643 view_->UpdatePopup(); 644 } 645 } 646 } 647 648 void AutocompleteEditModel::OnUpOrDownKeyPressed(int count) { 649 // NOTE: This purposefully don't trigger any code that resets paste_state_. 650 651 if (!popup_->IsOpen()) { 652 if (!query_in_progress()) { 653 // The popup is neither open nor working on a query already. So, start an 654 // autocomplete query for the current text. This also sets 655 // user_input_in_progress_ to true, which we want: if the user has started 656 // to interact with the popup, changing the permanent_text_ shouldn't 657 // change the displayed text. 658 // Note: This does not force the popup to open immediately. 659 // TODO(pkasting): We should, in fact, force this particular query to open 660 // the popup immediately. 661 if (!user_input_in_progress_) 662 InternalSetUserText(permanent_text_); 663 view_->UpdatePopup(); 664 } else { 665 // TODO(pkasting): The popup is working on a query but is not open. We 666 // should force it to open immediately. 667 } 668 } else { 669 // The popup is open, so the user should be able to interact with it 670 // normally. 671 popup_->Move(count); 672 } 673 } 674 675 void AutocompleteEditModel::OnPopupDataChanged( 676 const string16& text, 677 GURL* destination_for_temporary_text_change, 678 const string16& keyword, 679 bool is_keyword_hint) { 680 // Update keyword/hint-related local state. 681 bool keyword_state_changed = (keyword_ != keyword) || 682 ((is_keyword_hint_ != is_keyword_hint) && !keyword.empty()); 683 if (keyword_state_changed) { 684 keyword_ = keyword; 685 is_keyword_hint_ = is_keyword_hint; 686 687 // |is_keyword_hint_| should always be false if |keyword_| is empty. 688 DCHECK(!keyword_.empty() || !is_keyword_hint_); 689 } 690 691 // Handle changes to temporary text. 692 if (destination_for_temporary_text_change != NULL) { 693 const bool save_original_selection = !has_temporary_text_; 694 if (save_original_selection) { 695 // Save the original selection and URL so it can be reverted later. 696 has_temporary_text_ = true; 697 original_url_ = *destination_for_temporary_text_change; 698 inline_autocomplete_text_.clear(); 699 } 700 if (control_key_state_ == DOWN_WITHOUT_CHANGE) { 701 // Arrowing around the popup cancels control-enter. 702 control_key_state_ = DOWN_WITH_CHANGE; 703 // Now things are a bit screwy: the desired_tld has changed, but if we 704 // update the popup, the new order of entries won't match the old, so the 705 // user's selection gets screwy; and if we don't update the popup, and the 706 // user reverts, then the selected item will be as if control is still 707 // pressed, even though maybe it isn't any more. There is no obvious 708 // right answer here :( 709 } 710 view_->OnTemporaryTextMaybeChanged(DisplayTextFromUserText(text), 711 save_original_selection); 712 return; 713 } 714 715 bool call_controller_onchanged = true; 716 inline_autocomplete_text_ = text; 717 if (view_->OnInlineAutocompleteTextMaybeChanged( 718 DisplayTextFromUserText(user_text_ + inline_autocomplete_text_), 719 DisplayTextFromUserText(user_text_).length())) 720 call_controller_onchanged = false; 721 722 // If |has_temporary_text_| is true, then we previously had a manual selection 723 // but now don't (or |destination_for_temporary_text_change| would have been 724 // non-NULL). This can happen when deleting the selected item in the popup. 725 // In this case, we've already reverted the popup to the default match, so we 726 // need to revert ourselves as well. 727 if (has_temporary_text_) { 728 RevertTemporaryText(false); 729 call_controller_onchanged = false; 730 } 731 732 // We need to invoke OnChanged in case the destination url changed (as could 733 // happen when control is toggled). 734 if (call_controller_onchanged) 735 OnChanged(); 736 } 737 738 bool AutocompleteEditModel::OnAfterPossibleChange( 739 const string16& new_text, 740 size_t selection_start, 741 size_t selection_end, 742 bool selection_differs, 743 bool text_differs, 744 bool just_deleted_text, 745 bool allow_keyword_ui_change) { 746 // Update the paste state as appropriate: if we're just finishing a paste 747 // that replaced all the text, preserve that information; otherwise, if we've 748 // made some other edit, clear paste tracking. 749 if (paste_state_ == PASTING) 750 paste_state_ = PASTED; 751 else if (text_differs) 752 paste_state_ = NONE; 753 754 // Modifying the selection counts as accepting the autocompleted text. 755 const bool user_text_changed = 756 text_differs || (selection_differs && !inline_autocomplete_text_.empty()); 757 758 // If something has changed while the control key is down, prevent 759 // "ctrl-enter" until the control key is released. When we do this, we need 760 // to update the popup if it's open, since the desired_tld will have changed. 761 if ((text_differs || selection_differs) && 762 (control_key_state_ == DOWN_WITHOUT_CHANGE)) { 763 control_key_state_ = DOWN_WITH_CHANGE; 764 if (!text_differs && !popup_->IsOpen()) 765 return false; // Don't open the popup for no reason. 766 } else if (!user_text_changed) { 767 return false; 768 } 769 770 const string16 old_user_text = user_text_; 771 // If the user text has not changed, we do not want to change the model's 772 // state associated with the text. Otherwise, we can get surprising behavior 773 // where the autocompleted text unexpectedly reappears, e.g. crbug.com/55983 774 if (user_text_changed) { 775 InternalSetUserText(UserTextFromDisplayText(new_text)); 776 has_temporary_text_ = false; 777 778 // Track when the user has deleted text so we won't allow inline 779 // autocomplete. 780 just_deleted_text_ = just_deleted_text; 781 } 782 783 const bool no_selection = selection_start == selection_end; 784 785 // Update the popup for the change, in the process changing to keyword mode 786 // if the user hit space in mid-string after a keyword. 787 // |allow_exact_keyword_match_| will be used by StartAutocomplete() method, 788 // which will be called by |view_->UpdatePopup()|. So we can safely clear 789 // this flag afterwards. 790 allow_exact_keyword_match_ = 791 text_differs && allow_keyword_ui_change && 792 !just_deleted_text && no_selection && 793 ShouldAllowExactKeywordMatch(old_user_text, user_text_, selection_start); 794 view_->UpdatePopup(); 795 allow_exact_keyword_match_ = false; 796 797 // Change to keyword mode if the user has typed a keyword name and is now 798 // pressing space after the name. Accepting the keyword will update our 799 // state, so in that case there's no need to also return true here. 800 return !(text_differs && allow_keyword_ui_change && !just_deleted_text && 801 no_selection && selection_start == user_text_.length() && 802 MaybeAcceptKeywordBySpace(old_user_text, user_text_)); 803 } 804 805 void AutocompleteEditModel::PopupBoundsChangedTo(const gfx::Rect& bounds) { 806 InstantController* instant = controller_->GetInstant(); 807 if (instant) 808 instant->SetOmniboxBounds(bounds); 809 } 810 811 // Return true if the suggestion type warrants a TCP/IP preconnection. 812 // i.e., it is now highly likely that the user will select the related domain. 813 static bool IsPreconnectable(AutocompleteMatch::Type type) { 814 UMA_HISTOGRAM_ENUMERATION("Autocomplete.MatchType", type, 815 AutocompleteMatch::NUM_TYPES); 816 switch (type) { 817 // Matches using the user's default search engine. 818 case AutocompleteMatch::SEARCH_WHAT_YOU_TYPED: 819 case AutocompleteMatch::SEARCH_HISTORY: 820 case AutocompleteMatch::SEARCH_SUGGEST: 821 // A match that uses a non-default search engine (e.g. for tab-to-search). 822 case AutocompleteMatch::SEARCH_OTHER_ENGINE: 823 return true; 824 825 default: 826 return false; 827 } 828 } 829 830 void AutocompleteEditModel::OnResultChanged(bool default_match_changed) { 831 const bool was_open = popup_->IsOpen(); 832 if (default_match_changed) { 833 string16 inline_autocomplete_text; 834 string16 keyword; 835 bool is_keyword_hint = false; 836 const AutocompleteResult& result = this->result(); 837 const AutocompleteResult::const_iterator match(result.default_match()); 838 if (match != result.end()) { 839 if ((match->inline_autocomplete_offset != string16::npos) && 840 (match->inline_autocomplete_offset < 841 match->fill_into_edit.length())) { 842 inline_autocomplete_text = 843 match->fill_into_edit.substr(match->inline_autocomplete_offset); 844 } 845 846 if (!match->destination_url.SchemeIs(chrome::kExtensionScheme)) { 847 // Warm up DNS Prefetch cache, or preconnect to a search service. 848 chrome_browser_net::AnticipateOmniboxUrl(match->destination_url, 849 IsPreconnectable(match->type)); 850 } 851 852 // We could prefetch the alternate nav URL, if any, but because there 853 // can be many of these as a user types an initial series of characters, 854 // the OS DNS cache could suffer eviction problems for minimal gain. 855 856 is_keyword_hint = popup_->GetKeywordForMatch(*match, &keyword); 857 } 858 popup_->OnResultChanged(); 859 OnPopupDataChanged(inline_autocomplete_text, NULL, keyword, 860 is_keyword_hint); 861 } else { 862 popup_->OnResultChanged(); 863 } 864 865 if (popup_->IsOpen()) { 866 PopupBoundsChangedTo(popup_->view()->GetTargetBounds()); 867 } else if (was_open) { 868 // Accepts the temporary text as the user text, because it makes little 869 // sense to have temporary text when the popup is closed. 870 InternalSetUserText(UserTextFromDisplayText(view_->GetText())); 871 has_temporary_text_ = false; 872 PopupBoundsChangedTo(gfx::Rect()); 873 } 874 } 875 876 bool AutocompleteEditModel::query_in_progress() const { 877 return !autocomplete_controller_->done(); 878 } 879 880 void AutocompleteEditModel::InternalSetUserText(const string16& text) { 881 user_text_ = text; 882 just_deleted_text_ = false; 883 inline_autocomplete_text_.clear(); 884 } 885 886 bool AutocompleteEditModel::KeywordIsSelected() const { 887 return !is_keyword_hint_ && !keyword_.empty(); 888 } 889 890 string16 AutocompleteEditModel::DisplayTextFromUserText( 891 const string16& text) const { 892 return KeywordIsSelected() ? 893 KeywordProvider::SplitReplacementStringFromInput(text, false) : text; 894 } 895 896 string16 AutocompleteEditModel::UserTextFromDisplayText( 897 const string16& text) const { 898 return KeywordIsSelected() ? (keyword_ + char16(' ') + text) : text; 899 } 900 901 void AutocompleteEditModel::InfoForCurrentSelection( 902 AutocompleteMatch* match, 903 GURL* alternate_nav_url) const { 904 DCHECK(match != NULL); 905 const AutocompleteResult& result = this->result(); 906 if (!autocomplete_controller_->done()) { 907 // It's technically possible for |result| to be empty if no provider returns 908 // a synchronous result but the query has not completed synchronously; 909 // pratically, however, that should never actually happen. 910 if (result.empty()) 911 return; 912 // The user cannot have manually selected a match, or the query would have 913 // stopped. So the default match must be the desired selection. 914 *match = *result.default_match(); 915 } else { 916 CHECK(popup_->IsOpen()); 917 // If there are no results, the popup should be closed (so we should have 918 // failed the CHECK above), and URLsForDefaultMatch() should have been 919 // called instead. 920 CHECK(!result.empty()); 921 CHECK(popup_->selected_line() < result.size()); 922 *match = result.match_at(popup_->selected_line()); 923 } 924 if (alternate_nav_url && popup_->manually_selected_match().empty()) 925 *alternate_nav_url = result.alternate_nav_url(); 926 } 927 928 void AutocompleteEditModel::GetInfoForCurrentText( 929 AutocompleteMatch* match, 930 GURL* alternate_nav_url) const { 931 if (popup_->IsOpen() || query_in_progress()) { 932 InfoForCurrentSelection(match, alternate_nav_url); 933 } else { 934 profile_->GetAutocompleteClassifier()->Classify( 935 UserTextFromDisplayText(view_->GetText()), GetDesiredTLD(), true, 936 match, alternate_nav_url); 937 } 938 } 939 940 bool AutocompleteEditModel::GetURLForText(const string16& text, 941 GURL* url) const { 942 GURL parsed_url; 943 const AutocompleteInput::Type type = AutocompleteInput::Parse( 944 UserTextFromDisplayText(text), string16(), NULL, NULL, &parsed_url); 945 if (type != AutocompleteInput::URL) 946 return false; 947 948 *url = parsed_url; 949 return true; 950 } 951 952 void AutocompleteEditModel::RevertTemporaryText(bool revert_popup) { 953 // The user typed something, then selected a different item. Restore the 954 // text they typed and change back to the default item. 955 // NOTE: This purposefully does not reset paste_state_. 956 just_deleted_text_ = false; 957 has_temporary_text_ = false; 958 if (revert_popup) 959 popup_->ResetToDefaultMatch(); 960 view_->OnRevertTemporaryText(); 961 } 962 963 bool AutocompleteEditModel::MaybeAcceptKeywordBySpace( 964 const string16& old_user_text, 965 const string16& new_user_text) { 966 return (paste_state_ == NONE) && is_keyword_hint_ && !keyword_.empty() && 967 inline_autocomplete_text_.empty() && new_user_text.length() >= 2 && 968 IsSpaceCharForAcceptingKeyword(*new_user_text.rbegin()) && 969 !IsWhitespace(*(new_user_text.rbegin() + 1)) && 970 (old_user_text.length() + 1 >= new_user_text.length()) && 971 !new_user_text.compare(0, new_user_text.length() - 1, old_user_text, 972 0, new_user_text.length() - 1) && 973 AcceptKeyword(); 974 } 975 976 bool AutocompleteEditModel::ShouldAllowExactKeywordMatch( 977 const string16& old_user_text, 978 const string16& new_user_text, 979 size_t caret_position) { 980 // Check simple conditions first. 981 if (paste_state_ != NONE || caret_position < 2 || 982 new_user_text.length() <= caret_position || 983 old_user_text.length() < caret_position || 984 !IsSpaceCharForAcceptingKeyword(new_user_text[caret_position - 1]) || 985 IsSpaceCharForAcceptingKeyword(new_user_text[caret_position - 2]) || 986 new_user_text.compare(0, caret_position - 1, old_user_text, 987 0, caret_position - 1) || 988 !new_user_text.compare(caret_position - 1, 989 new_user_text.length() - caret_position + 1, 990 old_user_text, caret_position - 1, 991 old_user_text.length() - caret_position + 1)) { 992 return false; 993 } 994 995 // Then check if the text before the inserted space matches a keyword. 996 string16 keyword; 997 TrimWhitespace(new_user_text.substr(0, caret_position - 1), 998 TRIM_LEADING, &keyword); 999 1000 // Only allow exact keyword match if |keyword| represents a keyword hint. 1001 return keyword.length() && popup_->GetKeywordForText(keyword, &keyword); 1002 } 1003 1004 // static 1005 bool AutocompleteEditModel::IsSpaceCharForAcceptingKeyword(wchar_t c) { 1006 switch (c) { 1007 case 0x0020: // Space 1008 case 0x3000: // Ideographic Space 1009 return true; 1010 default: 1011 return false; 1012 } 1013 } 1014