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