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