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