omnibox_edit_model.cc revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
1// Copyright 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "chrome/browser/ui/omnibox/omnibox_edit_model.h" 6 7#include <string> 8 9#include "base/auto_reset.h" 10#include "base/format_macros.h" 11#include "base/metrics/histogram.h" 12#include "base/prefs/pref_service.h" 13#include "base/string_util.h" 14#include "base/stringprintf.h" 15#include "base/utf_string_conversions.h" 16#include "chrome/app/chrome_command_ids.h" 17#include "chrome/browser/autocomplete/autocomplete_classifier.h" 18#include "chrome/browser/autocomplete/autocomplete_classifier_factory.h" 19#include "chrome/browser/autocomplete/autocomplete_input.h" 20#include "chrome/browser/autocomplete/autocomplete_log.h" 21#include "chrome/browser/autocomplete/autocomplete_provider.h" 22#include "chrome/browser/autocomplete/extension_app_provider.h" 23#include "chrome/browser/autocomplete/history_url_provider.h" 24#include "chrome/browser/autocomplete/keyword_provider.h" 25#include "chrome/browser/autocomplete/search_provider.h" 26#include "chrome/browser/bookmarks/bookmark_utils.h" 27#include "chrome/browser/command_updater.h" 28#include "chrome/browser/extensions/api/omnibox/omnibox_api.h" 29#include "chrome/browser/google/google_url_tracker.h" 30#include "chrome/browser/net/predictor.h" 31#include "chrome/browser/net/url_fixer_upper.h" 32#include "chrome/browser/predictors/autocomplete_action_predictor.h" 33#include "chrome/browser/predictors/autocomplete_action_predictor_factory.h" 34#include "chrome/browser/prerender/prerender_field_trial.h" 35#include "chrome/browser/prerender/prerender_manager.h" 36#include "chrome/browser/prerender/prerender_manager_factory.h" 37#include "chrome/browser/profiles/profile.h" 38#include "chrome/browser/search/search.h" 39#include "chrome/browser/search_engines/template_url.h" 40#include "chrome/browser/search_engines/template_url_prepopulate_data.h" 41#include "chrome/browser/search_engines/template_url_service.h" 42#include "chrome/browser/search_engines/template_url_service_factory.h" 43#include "chrome/browser/sessions/session_tab_helper.h" 44#include "chrome/browser/ui/browser_list.h" 45#include "chrome/browser/ui/omnibox/omnibox_current_page_delegate_impl.h" 46#include "chrome/browser/ui/omnibox/omnibox_edit_controller.h" 47#include "chrome/browser/ui/omnibox/omnibox_popup_model.h" 48#include "chrome/browser/ui/omnibox/omnibox_popup_view.h" 49#include "chrome/browser/ui/omnibox/omnibox_view.h" 50#include "chrome/browser/ui/search/instant_controller.h" 51#include "chrome/browser/ui/search/search_tab_helper.h" 52#include "chrome/common/chrome_notification_types.h" 53#include "chrome/common/chrome_switches.h" 54#include "chrome/common/pref_names.h" 55#include "chrome/common/url_constants.h" 56#include "content/public/browser/notification_service.h" 57#include "content/public/browser/render_view_host.h" 58#include "content/public/browser/user_metrics.h" 59#include "extensions/common/constants.h" 60#include "googleurl/src/url_util.h" 61#include "ui/gfx/image/image.h" 62 63using content::UserMetricsAction; 64using predictors::AutocompleteActionPredictor; 65using predictors::AutocompleteActionPredictorFactory; 66 67namespace { 68 69// Histogram name which counts the number of times that the user text is 70// cleared. IME users are sometimes in the situation that IME was 71// unintentionally turned on and failed to input latin alphabets (ASCII 72// characters) or the opposite case. In that case, users may delete all 73// the text and the user text gets cleared. We'd like to measure how often 74// this scenario happens. 75// 76// Note that since we don't currently correlate "text cleared" events with 77// IME usage, this also captures many other cases where users clear the text; 78// though it explicitly doesn't log deleting all the permanent text as 79// the first action of an editing sequence (see comments in 80// OnAfterPossibleChange()). 81const char kOmniboxUserTextClearedHistogram[] = "Omnibox.UserTextCleared"; 82 83enum UserTextClearedType { 84 OMNIBOX_USER_TEXT_CLEARED_BY_EDITING = 0, 85 OMNIBOX_USER_TEXT_CLEARED_WITH_ESCAPE = 1, 86 OMNIBOX_USER_TEXT_CLEARED_NUM_OF_ITEMS, 87}; 88 89} // namespace 90 91/////////////////////////////////////////////////////////////////////////////// 92// OmniboxEditModel::State 93 94OmniboxEditModel::State::State(bool user_input_in_progress, 95 const string16& user_text, 96 const string16& keyword, 97 bool is_keyword_hint, 98 OmniboxFocusState focus_state) 99 : user_input_in_progress(user_input_in_progress), 100 user_text(user_text), 101 keyword(keyword), 102 is_keyword_hint(is_keyword_hint), 103 focus_state(focus_state) { 104} 105 106OmniboxEditModel::State::~State() { 107} 108 109/////////////////////////////////////////////////////////////////////////////// 110// OmniboxEditModel 111 112OmniboxEditModel::OmniboxEditModel(OmniboxView* view, 113 OmniboxEditController* controller, 114 Profile* profile) 115 : view_(view), 116 popup_(NULL), 117 controller_(controller), 118 focus_state_(OMNIBOX_FOCUS_NONE), 119 user_input_in_progress_(false), 120 just_deleted_text_(false), 121 has_temporary_text_(false), 122 is_temporary_text_set_by_instant_(false), 123 paste_state_(NONE), 124 control_key_state_(UP), 125 is_keyword_hint_(false), 126 profile_(profile), 127 in_revert_(false), 128 in_escape_handler_(false), 129 allow_exact_keyword_match_(false) { 130 // Use a restricted subset of the autocomplete providers if we're using the 131 // Instant Extended API, as it doesn't support them all. 132 autocomplete_controller_.reset(new AutocompleteController(profile, this, 133 chrome::search::IsInstantExtendedAPIEnabled() ? 134 AutocompleteClassifier::kInstantExtendedOmniboxProviders : 135 AutocompleteClassifier::kDefaultOmniboxProviders)); 136 delegate_.reset(new OmniboxCurrentPageDelegateImpl(controller, profile)); 137} 138 139OmniboxEditModel::~OmniboxEditModel() { 140} 141 142const OmniboxEditModel::State OmniboxEditModel::GetStateForTabSwitch() { 143 // Like typing, switching tabs "accepts" the temporary text as the user 144 // text, because it makes little sense to have temporary text when the 145 // popup is closed. 146 if (user_input_in_progress_) { 147 // Weird edge case to match other browsers: if the edit is empty, revert to 148 // the permanent text (so the user can get it back easily) but select it (so 149 // on switching back, typing will "just work"). 150 const string16 user_text(UserTextFromDisplayText(view_->GetText())); 151 if (user_text.empty()) { 152 view_->RevertAll(); 153 view_->SelectAll(true); 154 } else { 155 InternalSetUserText(user_text); 156 } 157 } 158 159 return State(user_input_in_progress_, user_text_, keyword_, is_keyword_hint_, 160 focus_state_); 161} 162 163void OmniboxEditModel::RestoreState(const State& state) { 164 SetFocusState(state.focus_state, OMNIBOX_FOCUS_CHANGE_TAB_SWITCH); 165 // Restore any user editing. 166 if (state.user_input_in_progress) { 167 // NOTE: Be sure and set keyword-related state BEFORE invoking 168 // DisplayTextFromUserText(), as its result depends upon this state. 169 keyword_ = state.keyword; 170 is_keyword_hint_ = state.is_keyword_hint; 171 view_->SetUserText(state.user_text, 172 DisplayTextFromUserText(state.user_text), false); 173 } 174} 175 176AutocompleteMatch OmniboxEditModel::CurrentMatch() { 177 AutocompleteMatch match; 178 GetInfoForCurrentText(&match, NULL); 179 return match; 180} 181 182bool OmniboxEditModel::UpdatePermanentText(const string16& new_permanent_text) { 183 // When there's a new URL, and the user is not editing anything or the edit 184 // doesn't have focus, we want to revert the edit to show the new URL. (The 185 // common case where the edit doesn't have focus is when the user has started 186 // an edit and then abandoned it and clicked a link on the page.) 187 const bool visibly_changed_permanent_text = 188 (permanent_text_ != new_permanent_text) && 189 (!user_input_in_progress_ || !has_focus()); 190 191 permanent_text_ = new_permanent_text; 192 return visibly_changed_permanent_text; 193} 194 195GURL OmniboxEditModel::PermanentURL() { 196 return URLFixerUpper::FixupURL(UTF16ToUTF8(permanent_text_), std::string()); 197} 198 199void OmniboxEditModel::SetUserText(const string16& text) { 200 SetInputInProgress(true); 201 InternalSetUserText(text); 202 paste_state_ = NONE; 203 has_temporary_text_ = false; 204 is_temporary_text_set_by_instant_ = false; 205} 206 207void OmniboxEditModel::FinalizeInstantQuery(const string16& input_text, 208 const InstantSuggestion& suggestion, 209 bool skip_inline_autocomplete) { 210 if (skip_inline_autocomplete) { 211 const string16 final_text = input_text + suggestion.text; 212 view_->OnBeforePossibleChange(); 213 view_->SetWindowTextAndCaretPos(final_text, final_text.length(), false, 214 false); 215 view_->OnAfterPossibleChange(); 216 } else if (popup_->IsOpen()) { 217 SearchProvider* search_provider = 218 autocomplete_controller_->search_provider(); 219 // There may be no providers during testing; guard against that. 220 if (search_provider) 221 search_provider->FinalizeInstantQuery(input_text, suggestion); 222 } 223} 224 225void OmniboxEditModel::SetInstantSuggestion( 226 const InstantSuggestion& suggestion) { 227 switch (suggestion.behavior) { 228 case INSTANT_COMPLETE_NOW: 229 view_->SetInstantSuggestion(string16()); 230 if (!suggestion.text.empty()) 231 FinalizeInstantQuery(view_->GetText(), suggestion, false); 232 break; 233 234 case INSTANT_COMPLETE_NEVER: 235 DCHECK_EQ(INSTANT_SUGGESTION_SEARCH, suggestion.type); 236 view_->SetInstantSuggestion(suggestion.text); 237 break; 238 239 case INSTANT_COMPLETE_REPLACE: { 240 const bool save_original_selection = !has_temporary_text_; 241 view_->SetInstantSuggestion(string16()); 242 has_temporary_text_ = true; 243 is_temporary_text_set_by_instant_ = true; 244 // Instant suggestions are never a keyword. 245 keyword_ = string16(); 246 is_keyword_hint_ = false; 247 view_->OnTemporaryTextMaybeChanged(suggestion.text, 248 save_original_selection, true); 249 break; 250 } 251 } 252} 253 254bool OmniboxEditModel::CommitSuggestedText(bool skip_inline_autocomplete) { 255 if (!controller_->GetInstant()) 256 return false; 257 258 const string16 suggestion = view_->GetInstantSuggestion(); 259 if (suggestion.empty()) 260 return false; 261 262 // Assume that the gray text we are committing is a search suggestion. 263 FinalizeInstantQuery(view_->GetText(), 264 InstantSuggestion(suggestion, 265 INSTANT_COMPLETE_NOW, 266 INSTANT_SUGGESTION_SEARCH, 267 string16()), 268 skip_inline_autocomplete); 269 return true; 270} 271 272void OmniboxEditModel::OnChanged() { 273 // Don't call CurrentMatch() when there's no editing, as in this case we'll 274 // never actually use it. This avoids running the autocomplete providers (and 275 // any systems they then spin up) during startup. 276 const AutocompleteMatch& current_match = user_input_in_progress_ ? 277 CurrentMatch() : AutocompleteMatch(); 278 279 AutocompleteActionPredictor::Action recommended_action = 280 AutocompleteActionPredictor::ACTION_NONE; 281 AutocompleteActionPredictor* action_predictor = 282 user_input_in_progress_ ? 283 AutocompleteActionPredictorFactory::GetForProfile(profile_) : NULL; 284 if (action_predictor) { 285 action_predictor->RegisterTransitionalMatches(user_text_, result()); 286 // Confer with the AutocompleteActionPredictor to determine what action, if 287 // any, we should take. Get the recommended action here even if we don't 288 // need it so we can get stats for anyone who is opted in to UMA, but only 289 // get it if the user has actually typed something to avoid constructing it 290 // before it's needed. Note: This event is triggered as part of startup when 291 // the initial tab transitions to the start page. 292 recommended_action = 293 action_predictor->RecommendAction(user_text_, current_match); 294 } 295 296 UMA_HISTOGRAM_ENUMERATION("AutocompleteActionPredictor.Action", 297 recommended_action, 298 AutocompleteActionPredictor::LAST_PREDICT_ACTION); 299 300 if (!DoInstant(current_match)) { 301 // Hide any suggestions we might be showing. 302 view_->SetInstantSuggestion(string16()); 303 304 // No need to wait any longer for Instant. 305 FinalizeInstantQuery(string16(), InstantSuggestion(), false); 306 } 307 308 switch (recommended_action) { 309 case AutocompleteActionPredictor::ACTION_PRERENDER: 310 // It's possible that there is no current page, for instance if the tab 311 // has been closed or on return from a sleep state. 312 // (http://crbug.com/105689) 313 if (!delegate_->CurrentPageExists()) 314 break; 315 // Ask for prerendering if the destination URL is different than the 316 // current URL. 317 if (current_match.destination_url != PermanentURL()) 318 delegate_->DoPrerender(current_match); 319 break; 320 case AutocompleteActionPredictor::ACTION_PRECONNECT: 321 DoPreconnect(current_match); 322 break; 323 case AutocompleteActionPredictor::ACTION_NONE: 324 break; 325 } 326 327 controller_->OnChanged(); 328} 329 330void OmniboxEditModel::GetDataForURLExport(GURL* url, 331 string16* title, 332 gfx::Image* favicon) { 333 AutocompleteMatch match; 334 GetInfoForCurrentText(&match, NULL); 335 *url = match.destination_url; 336 if (*url == URLFixerUpper::FixupURL(UTF16ToUTF8(permanent_text_), 337 std::string())) { 338 *title = controller_->GetTitle(); 339 *favicon = controller_->GetFavicon(); 340 } 341} 342 343bool OmniboxEditModel::UseVerbatimInstant() { 344#if defined(OS_MACOSX) 345 // TODO(suzhe): Fix Mac port to display Instant suggest in a separated NSView, 346 // so that we can display Instant suggest along with composition text. 347 const AutocompleteInput& input = autocomplete_controller_->input(); 348 if (input.prevent_inline_autocomplete()) 349 return true; 350#endif 351 352 // The value of input.prevent_inline_autocomplete() is determined by the 353 // following conditions: 354 // 1. If the caret is at the end of the text. 355 // 2. If it's in IME composition mode. 356 // We send the caret position to Instant (so it can determine #1 itself), and 357 // we use a separated widget for displaying the Instant suggest (so it doesn't 358 // interfere with #2). So, we don't need to care about the value of 359 // input.prevent_inline_autocomplete() here. 360 return view_->DeleteAtEndPressed() || popup_->selected_line() != 0 || 361 just_deleted_text_; 362} 363 364bool OmniboxEditModel::CurrentTextIsURL() const { 365 if (view_->toolbar_model()->WouldReplaceSearchURLWithSearchTerms()) 366 return false; 367 368 // If current text is not composed of replaced search terms and 369 // !user_input_in_progress_, then permanent text is showing and should be a 370 // URL, so no further checking is needed. By avoiding checking in this case, 371 // we avoid calling into the autocomplete providers, and thus initializing the 372 // history system, as long as possible, which speeds startup. 373 if (!user_input_in_progress_) 374 return true; 375 376 AutocompleteMatch match; 377 GetInfoForCurrentText(&match, NULL); 378 return !AutocompleteMatch::IsSearchType(match.type); 379} 380 381AutocompleteMatch::Type OmniboxEditModel::CurrentTextType() const { 382 AutocompleteMatch match; 383 GetInfoForCurrentText(&match, NULL); 384 return match.type; 385} 386 387void OmniboxEditModel::AdjustTextForCopy(int sel_min, 388 bool is_all_selected, 389 string16* text, 390 GURL* url, 391 bool* write_url) { 392 *write_url = false; 393 394 // Do not adjust if selection did not start at the beginning of the field, or 395 // if the URL was replaced by search terms. 396 if (sel_min != 0 || 397 view_->toolbar_model()->WouldReplaceSearchURLWithSearchTerms()) 398 return; 399 400 if (!user_input_in_progress_ && is_all_selected) { 401 // The user selected all the text and has not edited it. Use the url as the 402 // text so that if the scheme was stripped it's added back, and the url 403 // is unescaped (we escape parts of the url for display). 404 *url = PermanentURL(); 405 *text = UTF8ToUTF16(url->spec()); 406 *write_url = true; 407 return; 408 } 409 410 // We can't use CurrentTextIsURL() or GetDataForURLExport() because right now 411 // the user is probably holding down control to cause the copy, which will 412 // screw up our calculation of the desired_tld. 413 AutocompleteMatch match; 414 AutocompleteClassifierFactory::GetForProfile(profile_)->Classify(*text, 415 KeywordIsSelected(), true, &match, NULL); 416 if (AutocompleteMatch::IsSearchType(match.type)) 417 return; 418 *url = match.destination_url; 419 420 // Prefix the text with 'http://' if the text doesn't start with 'http://', 421 // the text parses as a url with a scheme of http, the user selected the 422 // entire host, and the user hasn't edited the host or manually removed the 423 // scheme. 424 GURL perm_url(PermanentURL()); 425 if (perm_url.SchemeIs(chrome::kHttpScheme) && 426 url->SchemeIs(chrome::kHttpScheme) && perm_url.host() == url->host()) { 427 *write_url = true; 428 string16 http = ASCIIToUTF16(chrome::kHttpScheme) + 429 ASCIIToUTF16(content::kStandardSchemeSeparator); 430 if (text->compare(0, http.length(), http) != 0) 431 *text = http + *text; 432 } 433} 434 435void OmniboxEditModel::SetInputInProgress(bool in_progress) { 436 if (user_input_in_progress_ == in_progress) 437 return; 438 439 user_input_in_progress_ = in_progress; 440 if (user_input_in_progress_) { 441 time_user_first_modified_omnibox_ = base::TimeTicks::Now(); 442 content::RecordAction(content::UserMetricsAction("OmniboxInputInProgress")); 443 autocomplete_controller_->ResetSession(); 444 } 445 controller_->OnInputInProgress(in_progress); 446 447 delegate_->NotifySearchTabHelper(user_input_in_progress_, !in_revert_); 448} 449 450void OmniboxEditModel::Revert() { 451 SetInputInProgress(false); 452 paste_state_ = NONE; 453 InternalSetUserText(string16()); 454 keyword_.clear(); 455 is_keyword_hint_ = false; 456 has_temporary_text_ = false; 457 is_temporary_text_set_by_instant_ = false; 458 view_->SetWindowTextAndCaretPos(permanent_text_, 459 has_focus() ? permanent_text_.length() : 0, 460 false, true); 461 AutocompleteActionPredictor* action_predictor = 462 AutocompleteActionPredictorFactory::GetForProfile(profile_); 463 if (action_predictor) 464 action_predictor->ClearTransitionalMatches(); 465} 466 467void OmniboxEditModel::StartAutocomplete( 468 bool has_selected_text, 469 bool prevent_inline_autocomplete) const { 470 ClearPopupKeywordMode(); 471 472 bool keyword_is_selected = KeywordIsSelected(); 473 popup_->SetHoveredLine(OmniboxPopupModel::kNoMatch); 474 475 size_t cursor_position; 476 if (inline_autocomplete_text_.empty()) { 477 // Cursor position is equivalent to the current selection's end. 478 size_t start; 479 view_->GetSelectionBounds(&start, &cursor_position); 480 // Adjust cursor position taking into account possible keyword in the user 481 // text. We rely on DisplayTextFromUserText() method which is consistent 482 // with keyword extraction done in KeywordProvider/SearchProvider. 483 const size_t cursor_offset = 484 user_text_.length() - DisplayTextFromUserText(user_text_).length(); 485 cursor_position += cursor_offset; 486 } else { 487 // There are some cases where StartAutocomplete() may be called 488 // with non-empty |inline_autocomplete_text_|. In such cases, we cannot 489 // use the current selection, because it could result with the cursor 490 // position past the last character from the user text. Instead, 491 // we assume that the cursor is simply at the end of input. 492 // One example is when user presses Ctrl key while having a highlighted 493 // inline autocomplete text. 494 // TODO: Rethink how we are going to handle this case to avoid 495 // inconsistent behavior when user presses Ctrl key. 496 // See http://crbug.com/165961 and http://crbug.com/165968 for more details. 497 cursor_position = user_text_.length(); 498 } 499 500 // We don't explicitly clear OmniboxPopupModel::manually_selected_match, as 501 // Start ends up invoking OmniboxPopupModel::OnResultChanged which clears it. 502 autocomplete_controller_->Start(AutocompleteInput( 503 user_text_, cursor_position, string16(), GURL(), 504 prevent_inline_autocomplete || just_deleted_text_ || 505 (has_selected_text && inline_autocomplete_text_.empty()) || 506 (paste_state_ != NONE), keyword_is_selected, 507 keyword_is_selected || allow_exact_keyword_match_, 508 AutocompleteInput::ALL_MATCHES)); 509} 510 511void OmniboxEditModel::StopAutocomplete() { 512 autocomplete_controller_->Stop(true); 513} 514 515bool OmniboxEditModel::CanPasteAndGo(const string16& text) const { 516 if (!view_->command_updater()->IsCommandEnabled(IDC_OPEN_CURRENT_URL)) 517 return false; 518 519 AutocompleteMatch match; 520 ClassifyStringForPasteAndGo(text, &match, NULL); 521 return match.destination_url.is_valid(); 522} 523 524void OmniboxEditModel::PasteAndGo(const string16& text) { 525 DCHECK(CanPasteAndGo(text)); 526 view_->RevertAll(); 527 AutocompleteMatch match; 528 GURL alternate_nav_url; 529 ClassifyStringForPasteAndGo(text, &match, &alternate_nav_url); 530 view_->OpenMatch(match, CURRENT_TAB, alternate_nav_url, 531 OmniboxPopupModel::kNoMatch); 532} 533 534bool OmniboxEditModel::IsPasteAndSearch(const string16& text) const { 535 AutocompleteMatch match; 536 ClassifyStringForPasteAndGo(text, &match, NULL); 537 return AutocompleteMatch::IsSearchType(match.type); 538} 539 540void OmniboxEditModel::AcceptInput(WindowOpenDisposition disposition, 541 bool for_drop) { 542 // Get the URL and transition type for the selected entry. 543 AutocompleteMatch match; 544 GURL alternate_nav_url; 545 GetInfoForCurrentText(&match, &alternate_nav_url); 546 547 // If CTRL is down it means the user wants to append ".com" to the text he 548 // typed. If we can successfully generate a URL_WHAT_YOU_TYPED match doing 549 // that, then we use this. 550 if (control_key_state_ == DOWN_WITHOUT_CHANGE && !KeywordIsSelected()) { 551 // Generate a new AutocompleteInput, copying the latest one but using "com" 552 // as the desired TLD. Then use this autocomplete input to generate a 553 // URL_WHAT_YOU_TYPED AutocompleteMatch. Note that using the most recent 554 // input instead of the currently visible text means we'll ignore any 555 // visible inline autocompletion: if a user types "foo" and is autocompleted 556 // to "foodnetwork.com", ctrl-enter will navigate to "foo.com", not 557 // "foodnetwork.com". At the time of writing, this behavior matches 558 // Internet Explorer, but not Firefox. 559 const AutocompleteInput& old_input = autocomplete_controller_->input(); 560 AutocompleteInput input( 561 old_input.text(), old_input.cursor_position(), ASCIIToUTF16("com"), 562 GURL(), old_input.prevent_inline_autocomplete(), 563 old_input.prefer_keyword(), old_input.allow_exact_keyword_match(), 564 old_input.matches_requested()); 565 AutocompleteMatch url_match = 566 HistoryURLProvider::SuggestExactInput(match.provider, input, true); 567 568 if (url_match.destination_url.is_valid()) { 569 // We have a valid URL, we use this newly generated AutocompleteMatch. 570 match = url_match; 571 alternate_nav_url = GURL(); 572 } 573 } 574 575 if (!match.destination_url.is_valid()) 576 return; 577 578 if ((match.transition == content::PAGE_TRANSITION_TYPED) && 579 (match.destination_url == 580 URLFixerUpper::FixupURL(UTF16ToUTF8(permanent_text_), std::string()))) { 581 // When the user hit enter on the existing permanent URL, treat it like a 582 // reload for scoring purposes. We could detect this by just checking 583 // user_input_in_progress_, but it seems better to treat "edits" that end 584 // up leaving the URL unchanged (e.g. deleting the last character and then 585 // retyping it) as reloads too. We exclude non-TYPED transitions because if 586 // the transition is GENERATED, the user input something that looked 587 // different from the current URL, even if it wound up at the same place 588 // (e.g. manually retyping the same search query), and it seems wrong to 589 // treat this as a reload. 590 match.transition = content::PAGE_TRANSITION_RELOAD; 591 } else if (for_drop || ((paste_state_ != NONE) && 592 match.is_history_what_you_typed_match)) { 593 // When the user pasted in a URL and hit enter, score it like a link click 594 // rather than a normal typed URL, so it doesn't get inline autocompleted 595 // as aggressively later. 596 match.transition = content::PAGE_TRANSITION_LINK; 597 } 598 599 const TemplateURL* template_url = match.GetTemplateURL(profile_, false); 600 if (template_url && template_url->url_ref().HasGoogleBaseURLs()) 601 GoogleURLTracker::GoogleURLSearchCommitted(profile_); 602 603 view_->OpenMatch(match, disposition, alternate_nav_url, 604 OmniboxPopupModel::kNoMatch); 605} 606 607void OmniboxEditModel::OpenMatch(const AutocompleteMatch& match, 608 WindowOpenDisposition disposition, 609 const GURL& alternate_nav_url, 610 size_t index) { 611 // We only care about cases where there is a selection (i.e. the popup is 612 // open). 613 if (popup_->IsOpen()) { 614 const base::TimeTicks& now(base::TimeTicks::Now()); 615 // TODO(sreeram): Handle is_temporary_text_set_by_instant_ correctly. 616 AutocompleteLog log( 617 autocomplete_controller_->input().text(), 618 just_deleted_text_, 619 autocomplete_controller_->input().type(), 620 popup_->selected_line(), 621 -1, // don't yet know tab ID; set later if appropriate 622 delegate_->CurrentPageExists() ? ClassifyPage(delegate_->GetURL()) : 623 metrics::OmniboxEventProto_PageClassification_OTHER, 624 now - time_user_first_modified_omnibox_, 625 string16::npos, // completed_length; possibly set later 626 now - autocomplete_controller_->last_time_default_match_changed(), 627 result()); 628 DCHECK(user_input_in_progress_ || 629 match.provider->type() == AutocompleteProvider::TYPE_ZERO_SUGGEST) 630 << "We didn't get here through the expected series of calls. " 631 << "time_user_first_modified_omnibox_ is not set correctly and other " 632 << "things may be wrong. Match provider: " << match.provider->GetName(); 633 DCHECK(log.elapsed_time_since_user_first_modified_omnibox >= 634 log.elapsed_time_since_last_change_to_default_match) 635 << "We should've got the notification that the user modified the " 636 << "omnibox text at same time or before the most recent time the " 637 << "default match changed."; 638 if (index != OmniboxPopupModel::kNoMatch) 639 log.selected_index = index; 640 if (match.inline_autocomplete_offset != string16::npos) { 641 DCHECK_GE(match.fill_into_edit.length(), 642 match.inline_autocomplete_offset); 643 log.completed_length = 644 match.fill_into_edit.length() - match.inline_autocomplete_offset; 645 } 646 647 if ((disposition == CURRENT_TAB) && delegate_->CurrentPageExists()) { 648 // If we know the destination is being opened in the current tab, 649 // we can easily get the tab ID. (If it's being opened in a new 650 // tab, we don't know the tab ID yet.) 651 log.tab_id = delegate_->GetSessionID().id(); 652 } 653 autocomplete_controller_->AddProvidersInfo(&log.providers_info); 654 content::NotificationService::current()->Notify( 655 chrome::NOTIFICATION_OMNIBOX_OPENED_URL, 656 content::Source<Profile>(profile_), 657 content::Details<AutocompleteLog>(&log)); 658 HISTOGRAM_ENUMERATION("Omnibox.EventCount", 1, 2); 659 } 660 661 TemplateURL* template_url = match.GetTemplateURL(profile_, false); 662 if (template_url) { 663 if (match.transition == content::PAGE_TRANSITION_KEYWORD) { 664 // The user is using a non-substituting keyword or is explicitly in 665 // keyword mode. 666 667 AutocompleteMatch current_match; 668 GetInfoForCurrentText(¤t_match, NULL); 669 const AutocompleteMatch& match = (index == OmniboxPopupModel::kNoMatch) ? 670 current_match : result().match_at(index); 671 672 // Don't increment usage count for extension keywords. 673 if (delegate_->ProcessExtensionKeyword(template_url, match)) { 674 view_->RevertAll(); 675 return; 676 } 677 678 content::RecordAction(UserMetricsAction("AcceptedKeyword")); 679 TemplateURLServiceFactory::GetForProfile(profile_)->IncrementUsageCount( 680 template_url); 681 } else { 682 DCHECK_EQ(content::PAGE_TRANSITION_GENERATED, match.transition); 683 // NOTE: We purposefully don't increment the usage count of the default 684 // search engine here like we do for explicit keywords above; see comments 685 // in template_url.h. 686 } 687 688 // NOTE: Non-prepopulated engines will all have ID 0, which is fine as 689 // the prepopulate IDs start at 1. Distribution-specific engines will 690 // all have IDs above the maximum, and will be automatically lumped 691 // together in an "overflow" bucket in the histogram. 692 UMA_HISTOGRAM_ENUMERATION("Omnibox.SearchEngine", 693 template_url->prepopulate_id(), 694 TemplateURLPrepopulateData::kMaxPrepopulatedEngineID); 695 } 696 697 if (disposition != NEW_BACKGROUND_TAB) { 698 base::AutoReset<bool> tmp(&in_revert_, true); 699 view_->RevertAll(); // Revert the box to its unedited state 700 } 701 702 if (match.type == AutocompleteMatch::EXTENSION_APP) { 703 ExtensionAppProvider::LaunchAppFromOmnibox(match, profile_, disposition); 704 } else { 705 base::TimeDelta query_formulation_time = 706 base::TimeTicks::Now() - time_user_first_modified_omnibox_; 707 const GURL destination_url = autocomplete_controller_-> 708 GetDestinationURL(match, query_formulation_time); 709 // This calls RevertAll again. 710 base::AutoReset<bool> tmp(&in_revert_, true); 711 controller_->OnAutocompleteAccept(destination_url, disposition, 712 match.transition, alternate_nav_url); 713 } 714 715 if (match.starred) 716 bookmark_utils::RecordBookmarkLaunch(bookmark_utils::LAUNCH_OMNIBOX); 717} 718 719bool OmniboxEditModel::AcceptKeyword() { 720 DCHECK(is_keyword_hint_ && !keyword_.empty()); 721 722 autocomplete_controller_->Stop(false); 723 is_keyword_hint_ = false; 724 725 if (popup_->IsOpen()) 726 popup_->SetSelectedLineState(OmniboxPopupModel::KEYWORD); 727 else 728 StartAutocomplete(false, true); 729 730 // Ensure the current selection is saved before showing keyword mode 731 // so that moving to another line and then reverting the text will restore 732 // the current state properly. 733 bool save_original_selection = !has_temporary_text_; 734 has_temporary_text_ = true; 735 is_temporary_text_set_by_instant_ = false; 736 view_->OnTemporaryTextMaybeChanged( 737 DisplayTextFromUserText(CurrentMatch().fill_into_edit), 738 save_original_selection, true); 739 740 content::RecordAction(UserMetricsAction("AcceptedKeywordHint")); 741 return true; 742} 743 744void OmniboxEditModel::ClearKeyword(const string16& visible_text) { 745 autocomplete_controller_->Stop(false); 746 ClearPopupKeywordMode(); 747 748 const string16 window_text(keyword_ + visible_text); 749 750 // Only reset the result if the edit text has changed since the 751 // keyword was accepted, or if the popup is closed. 752 if (just_deleted_text_ || !visible_text.empty() || !popup_->IsOpen()) { 753 view_->OnBeforePossibleChange(); 754 view_->SetWindowTextAndCaretPos(window_text.c_str(), keyword_.length(), 755 false, false); 756 keyword_.clear(); 757 is_keyword_hint_ = false; 758 view_->OnAfterPossibleChange(); 759 just_deleted_text_ = true; // OnAfterPossibleChange() fails to clear this 760 // since the edit contents have actually grown 761 // longer. 762 } else { 763 is_keyword_hint_ = true; 764 view_->SetWindowTextAndCaretPos(window_text.c_str(), keyword_.length(), 765 false, true); 766 } 767} 768 769const AutocompleteResult& OmniboxEditModel::result() const { 770 return autocomplete_controller_->result(); 771} 772 773void OmniboxEditModel::OnSetFocus(bool control_down) { 774 // If the omnibox lost focus while the caret was hidden and then regained 775 // focus, OnSetFocus() is called and should restore visibility. Note that 776 // focus can be regained without an accompanying call to 777 // OmniboxView::SetFocus(), e.g. by tabbing in. 778 SetFocusState(OMNIBOX_FOCUS_VISIBLE, OMNIBOX_FOCUS_CHANGE_EXPLICIT); 779 control_key_state_ = control_down ? DOWN_WITHOUT_CHANGE : UP; 780 781 if (delegate_->CurrentPageExists()) { 782 // TODO(jered): We may want to merge this into Start() and just call that 783 // here rather than having a special entry point for zero-suggest. Note 784 // that we avoid PermanentURL() here because it's not guaranteed to give us 785 // the actual underlying current URL, e.g. if we're on the NTP and the 786 // |permanent_text_| is empty. 787 autocomplete_controller_->StartZeroSuggest(delegate_->GetURL(), 788 user_text_); 789 } 790 791 delegate_->NotifySearchTabHelper(user_input_in_progress_, !in_revert_); 792} 793 794void OmniboxEditModel::SetCaretVisibility(bool visible) { 795 // Caret visibility only matters if the omnibox has focus. 796 if (focus_state_ != OMNIBOX_FOCUS_NONE) { 797 SetFocusState(visible ? OMNIBOX_FOCUS_VISIBLE : OMNIBOX_FOCUS_INVISIBLE, 798 OMNIBOX_FOCUS_CHANGE_EXPLICIT); 799 } 800} 801 802void OmniboxEditModel::OnWillKillFocus(gfx::NativeView view_gaining_focus) { 803 InstantController* instant = controller_->GetInstant(); 804 if (instant) { 805 instant->OmniboxFocusChanged(OMNIBOX_FOCUS_NONE, 806 OMNIBOX_FOCUS_CHANGE_EXPLICIT, 807 view_gaining_focus); 808 } 809 810 SetInstantSuggestion(InstantSuggestion()); 811 812 // TODO(jered): Rip this out along with StartZeroSuggest. 813 autocomplete_controller_->StopZeroSuggest(); 814 delegate_->NotifySearchTabHelper(user_input_in_progress_, !in_revert_); 815} 816 817void OmniboxEditModel::OnKillFocus() { 818 // TODO(samarth): determine if it is safe to move the call to 819 // OmniboxFocusChanged() from OnWillKillFocus() to here, which would let us 820 // just call SetFocusState() to handle the state change. 821 focus_state_ = OMNIBOX_FOCUS_NONE; 822 control_key_state_ = UP; 823 paste_state_ = NONE; 824} 825 826bool OmniboxEditModel::OnEscapeKeyPressed() { 827 if (has_temporary_text_) { 828 AutocompleteMatch match; 829 InfoForCurrentSelection(&match, NULL); 830 if (match.destination_url != original_url_) { 831 RevertTemporaryText(true); 832 return true; 833 } 834 } 835 836 // We do not clear the pending entry from the omnibox when a load is first 837 // stopped. If the user presses Escape while stopped, we clear it. 838 if (delegate_->CurrentPageExists() && !delegate_->IsLoading()) { 839 delegate_->GetNavigationController().DiscardNonCommittedEntries(); 840 view_->Update(NULL); 841 } 842 843 // If the user wasn't editing, but merely had focus in the edit, allow <esc> 844 // to be processed as an accelerator, so it can still be used to stop a load. 845 // When the permanent text isn't all selected we still fall through to the 846 // SelectAll() call below so users can arrow around in the text and then hit 847 // <esc> to quickly replace all the text; this matches IE. 848 if (!user_input_in_progress_ && view_->IsSelectAll()) 849 return false; 850 851 in_escape_handler_ = true; 852 if (!user_text_.empty()) { 853 UMA_HISTOGRAM_ENUMERATION(kOmniboxUserTextClearedHistogram, 854 OMNIBOX_USER_TEXT_CLEARED_WITH_ESCAPE, 855 OMNIBOX_USER_TEXT_CLEARED_NUM_OF_ITEMS); 856 } 857 view_->RevertAll(); 858 in_escape_handler_ = false; 859 view_->SelectAll(true); 860 return true; 861} 862 863void OmniboxEditModel::OnControlKeyChanged(bool pressed) { 864 // Don't change anything unless the key state is actually toggling. 865 if (pressed == (control_key_state_ == UP)) { 866 ControlKeyState old_state = control_key_state_; 867 control_key_state_ = pressed ? DOWN_WITHOUT_CHANGE : UP; 868 if ((control_key_state_ == DOWN_WITHOUT_CHANGE) && has_temporary_text_) { 869 // Arrowing down and then hitting control accepts the temporary text as 870 // the input text. 871 InternalSetUserText(UserTextFromDisplayText(view_->GetText())); 872 has_temporary_text_ = false; 873 is_temporary_text_set_by_instant_ = false; 874 } 875 if ((old_state != DOWN_WITH_CHANGE) && popup_->IsOpen()) { 876 // Autocomplete history provider results may change, so refresh the 877 // popup. This will force user_input_in_progress_ to true, but if the 878 // popup is open, that should have already been the case. 879 view_->UpdatePopup(); 880 } 881 } 882} 883 884void OmniboxEditModel::OnUpOrDownKeyPressed(int count) { 885 // NOTE: This purposefully doesn't trigger any code that resets paste_state_. 886 if (!popup_->IsOpen()) { 887 if (!query_in_progress()) { 888 // The popup is neither open nor working on a query already. So, start an 889 // autocomplete query for the current text. This also sets 890 // user_input_in_progress_ to true, which we want: if the user has started 891 // to interact with the popup, changing the permanent_text_ shouldn't 892 // change the displayed text. 893 // Note: This does not force the popup to open immediately. 894 // TODO(pkasting): We should, in fact, force this particular query to open 895 // the popup immediately. 896 if (!user_input_in_progress_) 897 InternalSetUserText(permanent_text_); 898 view_->UpdatePopup(); 899 } else { 900 // TODO(pkasting): The popup is working on a query but is not open. We 901 // should force it to open immediately. 902 } 903 } else { 904 InstantController* instant = controller_->GetInstant(); 905 if (instant && instant->OnUpOrDownKeyPressed(count)) { 906 // If Instant handles the key press, it's showing a list of suggestions 907 // that it's stepping through. In that case, our popup model is 908 // irrelevant, so don't process the key press ourselves. However, do stop 909 // the autocomplete system from changing the results. 910 autocomplete_controller_->Stop(false); 911 } else { 912 // The popup is open, so the user should be able to interact with it 913 // normally. 914 popup_->Move(count); 915 } 916 } 917} 918 919void OmniboxEditModel::OnPopupDataChanged( 920 const string16& text, 921 GURL* destination_for_temporary_text_change, 922 const string16& keyword, 923 bool is_keyword_hint) { 924 // Update keyword/hint-related local state. 925 bool keyword_state_changed = (keyword_ != keyword) || 926 ((is_keyword_hint_ != is_keyword_hint) && !keyword.empty()); 927 if (keyword_state_changed) { 928 keyword_ = keyword; 929 is_keyword_hint_ = is_keyword_hint; 930 931 // |is_keyword_hint_| should always be false if |keyword_| is empty. 932 DCHECK(!keyword_.empty() || !is_keyword_hint_); 933 } 934 935 // Handle changes to temporary text. 936 if (destination_for_temporary_text_change != NULL) { 937 const bool save_original_selection = !has_temporary_text_; 938 if (save_original_selection) { 939 // Save the original selection and URL so it can be reverted later. 940 has_temporary_text_ = true; 941 is_temporary_text_set_by_instant_ = false; 942 original_url_ = *destination_for_temporary_text_change; 943 inline_autocomplete_text_.clear(); 944 } 945 if (control_key_state_ == DOWN_WITHOUT_CHANGE) { 946 // Arrowing around the popup cancels control-enter. 947 control_key_state_ = DOWN_WITH_CHANGE; 948 // Now things are a bit screwy: the desired_tld has changed, but if we 949 // update the popup, the new order of entries won't match the old, so the 950 // user's selection gets screwy; and if we don't update the popup, and the 951 // user reverts, then the selected item will be as if control is still 952 // pressed, even though maybe it isn't any more. There is no obvious 953 // right answer here :( 954 } 955 view_->OnTemporaryTextMaybeChanged(DisplayTextFromUserText(text), 956 save_original_selection, true); 957 return; 958 } 959 960 bool call_controller_onchanged = true; 961 inline_autocomplete_text_ = text; 962 963 if (keyword_state_changed && KeywordIsSelected()) { 964 // If we reach here, the user most likely entered keyword mode by inserting 965 // a space between a keyword name and a search string (as pressing space or 966 // tab after the keyword name alone would have been be handled in 967 // MaybeAcceptKeywordBySpace() by calling AcceptKeyword(), which won't reach 968 // here). In this case, we don't want to call 969 // OnInlineAutocompleteTextMaybeChanged() as normal, because that will 970 // correctly change the text (to the search string alone) but move the caret 971 // to the end of the string; instead we want the caret at the start of the 972 // search string since that's where it was in the original input. So we set 973 // the text and caret position directly. 974 // 975 // It may also be possible to reach here if we're reverting from having 976 // temporary text back to a default match that's a keyword search, but in 977 // that case the RevertTemporaryText() call below will reset the caret or 978 // selection correctly so the caret positioning we do here won't matter. 979 view_->SetWindowTextAndCaretPos(DisplayTextFromUserText(user_text_), 0, 980 false, false); 981 } else if (view_->OnInlineAutocompleteTextMaybeChanged( 982 DisplayTextFromUserText(user_text_ + inline_autocomplete_text_), 983 DisplayTextFromUserText(user_text_).length())) { 984 call_controller_onchanged = false; 985 } 986 987 // If |has_temporary_text_| is true, then we previously had a manual selection 988 // but now don't (or |destination_for_temporary_text_change| would have been 989 // non-NULL). This can happen when deleting the selected item in the popup. 990 // In this case, we've already reverted the popup to the default match, so we 991 // need to revert ourselves as well. 992 if (has_temporary_text_) { 993 RevertTemporaryText(false); 994 call_controller_onchanged = false; 995 } 996 997 // We need to invoke OnChanged in case the destination url changed (as could 998 // happen when control is toggled). 999 if (call_controller_onchanged) 1000 OnChanged(); 1001} 1002 1003bool OmniboxEditModel::OnAfterPossibleChange(const string16& old_text, 1004 const string16& new_text, 1005 size_t selection_start, 1006 size_t selection_end, 1007 bool selection_differs, 1008 bool text_differs, 1009 bool just_deleted_text, 1010 bool allow_keyword_ui_change) { 1011 // Update the paste state as appropriate: if we're just finishing a paste 1012 // that replaced all the text, preserve that information; otherwise, if we've 1013 // made some other edit, clear paste tracking. 1014 if (paste_state_ == PASTING) 1015 paste_state_ = PASTED; 1016 else if (text_differs) 1017 paste_state_ = NONE; 1018 1019 // Restore caret visibility whenever the user changes text or selection in the 1020 // omnibox. 1021 if (text_differs || selection_differs) 1022 SetFocusState(OMNIBOX_FOCUS_VISIBLE, OMNIBOX_FOCUS_CHANGE_TYPING); 1023 1024 // Modifying the selection counts as accepting the autocompleted text. 1025 const bool user_text_changed = 1026 text_differs || (selection_differs && !inline_autocomplete_text_.empty()); 1027 1028 // If something has changed while the control key is down, prevent 1029 // "ctrl-enter" until the control key is released. When we do this, we need 1030 // to update the popup if it's open, since the desired_tld will have changed. 1031 if ((text_differs || selection_differs) && 1032 (control_key_state_ == DOWN_WITHOUT_CHANGE)) { 1033 control_key_state_ = DOWN_WITH_CHANGE; 1034 if (!text_differs && !popup_->IsOpen()) 1035 return false; // Don't open the popup for no reason. 1036 } else if (!user_text_changed) { 1037 return false; 1038 } 1039 1040 // If the user text has not changed, we do not want to change the model's 1041 // state associated with the text. Otherwise, we can get surprising behavior 1042 // where the autocompleted text unexpectedly reappears, e.g. crbug.com/55983 1043 if (user_text_changed) { 1044 InternalSetUserText(UserTextFromDisplayText(new_text)); 1045 has_temporary_text_ = false; 1046 is_temporary_text_set_by_instant_ = false; 1047 1048 // Track when the user has deleted text so we won't allow inline 1049 // autocomplete. 1050 just_deleted_text_ = just_deleted_text; 1051 1052 if (user_input_in_progress_ && user_text_.empty()) { 1053 // Log cases where the user started editing and then subsequently cleared 1054 // all the text. Note that this explicitly doesn't catch cases like 1055 // "hit ctrl-l to select whole edit contents, then hit backspace", because 1056 // in such cases, |user_input_in_progress| won't be true here. 1057 UMA_HISTOGRAM_ENUMERATION(kOmniboxUserTextClearedHistogram, 1058 OMNIBOX_USER_TEXT_CLEARED_BY_EDITING, 1059 OMNIBOX_USER_TEXT_CLEARED_NUM_OF_ITEMS); 1060 } 1061 } 1062 1063 const bool no_selection = selection_start == selection_end; 1064 1065 // Update the popup for the change, in the process changing to keyword mode 1066 // if the user hit space in mid-string after a keyword. 1067 // |allow_exact_keyword_match_| will be used by StartAutocomplete() method, 1068 // which will be called by |view_->UpdatePopup()|; so after that returns we 1069 // can safely reset this flag. 1070 allow_exact_keyword_match_ = text_differs && allow_keyword_ui_change && 1071 !just_deleted_text && no_selection && 1072 CreatedKeywordSearchByInsertingSpaceInMiddle(old_text, user_text_, 1073 selection_start); 1074 view_->UpdatePopup(); 1075 allow_exact_keyword_match_ = false; 1076 1077 // Change to keyword mode if the user is now pressing space after a keyword 1078 // name. Note that if this is the case, then even if there was no keyword 1079 // hint when we entered this function (e.g. if the user has used space to 1080 // replace some selected text that was adjoined to this keyword), there will 1081 // be one now because of the call to UpdatePopup() above; so it's safe for 1082 // MaybeAcceptKeywordBySpace() to look at |keyword_| and |is_keyword_hint_| to 1083 // determine what keyword, if any, is applicable. 1084 // 1085 // If MaybeAcceptKeywordBySpace() accepts the keyword and returns true, that 1086 // will have updated our state already, so in that case we don't also return 1087 // true from this function. 1088 return !(text_differs && allow_keyword_ui_change && !just_deleted_text && 1089 no_selection && (selection_start == user_text_.length()) && 1090 MaybeAcceptKeywordBySpace(user_text_)); 1091} 1092 1093void OmniboxEditModel::OnPopupBoundsChanged(const gfx::Rect& bounds) { 1094 InstantController* instant = controller_->GetInstant(); 1095 if (instant) 1096 instant->SetPopupBounds(bounds); 1097} 1098 1099void OmniboxEditModel::OnResultChanged(bool default_match_changed) { 1100 const bool was_open = popup_->IsOpen(); 1101 if (default_match_changed) { 1102 string16 inline_autocomplete_text; 1103 string16 keyword; 1104 bool is_keyword_hint = false; 1105 const AutocompleteResult& result = this->result(); 1106 const AutocompleteResult::const_iterator match(result.default_match()); 1107 if (match != result.end()) { 1108 if ((match->inline_autocomplete_offset != string16::npos) && 1109 (match->inline_autocomplete_offset < 1110 match->fill_into_edit.length())) { 1111 inline_autocomplete_text = 1112 match->fill_into_edit.substr(match->inline_autocomplete_offset); 1113 } 1114 1115 if (!prerender::IsOmniboxEnabled(profile_)) 1116 DoPreconnect(*match); 1117 1118 // We could prefetch the alternate nav URL, if any, but because there 1119 // can be many of these as a user types an initial series of characters, 1120 // the OS DNS cache could suffer eviction problems for minimal gain. 1121 1122 match->GetKeywordUIState(profile_, &keyword, &is_keyword_hint); 1123 } 1124 1125 popup_->OnResultChanged(); 1126 OnPopupDataChanged(inline_autocomplete_text, NULL, keyword, 1127 is_keyword_hint); 1128 } else { 1129 popup_->OnResultChanged(); 1130 } 1131 1132 if (popup_->IsOpen()) { 1133 OnPopupBoundsChanged(popup_->view()->GetTargetBounds()); 1134 } else if (was_open) { 1135 // Accepts the temporary text as the user text, because it makes little 1136 // sense to have temporary text when the popup is closed. 1137 InternalSetUserText(UserTextFromDisplayText(view_->GetText())); 1138 has_temporary_text_ = false; 1139 is_temporary_text_set_by_instant_ = false; 1140 OnPopupBoundsChanged(gfx::Rect()); 1141 delegate_->NotifySearchTabHelper(user_input_in_progress_, !in_revert_); 1142 } 1143 1144 InstantController* instant = controller_->GetInstant(); 1145 if (instant && !in_revert_) 1146 instant->HandleAutocompleteResults(*autocomplete_controller_->providers()); 1147} 1148 1149bool OmniboxEditModel::query_in_progress() const { 1150 return !autocomplete_controller_->done(); 1151} 1152 1153void OmniboxEditModel::InternalSetUserText(const string16& text) { 1154 user_text_ = text; 1155 just_deleted_text_ = false; 1156 inline_autocomplete_text_.clear(); 1157} 1158 1159bool OmniboxEditModel::KeywordIsSelected() const { 1160 return !is_keyword_hint_ && !keyword_.empty(); 1161} 1162 1163void OmniboxEditModel::ClearPopupKeywordMode() const { 1164 if (popup_->IsOpen() && 1165 popup_->selected_line_state() == OmniboxPopupModel::KEYWORD) 1166 popup_->SetSelectedLineState(OmniboxPopupModel::NORMAL); 1167} 1168 1169string16 OmniboxEditModel::DisplayTextFromUserText(const string16& text) const { 1170 return KeywordIsSelected() ? 1171 KeywordProvider::SplitReplacementStringFromInput(text, false) : text; 1172} 1173 1174string16 OmniboxEditModel::UserTextFromDisplayText(const string16& text) const { 1175 return KeywordIsSelected() ? (keyword_ + char16(' ') + text) : text; 1176} 1177 1178void OmniboxEditModel::InfoForCurrentSelection(AutocompleteMatch* match, 1179 GURL* alternate_nav_url) const { 1180 DCHECK(match != NULL); 1181 const AutocompleteResult& result = this->result(); 1182 if (!autocomplete_controller_->done()) { 1183 // It's technically possible for |result| to be empty if no provider returns 1184 // a synchronous result but the query has not completed synchronously; 1185 // pratically, however, that should never actually happen. 1186 if (result.empty()) 1187 return; 1188 // The user cannot have manually selected a match, or the query would have 1189 // stopped. So the default match must be the desired selection. 1190 *match = *result.default_match(); 1191 } else { 1192 CHECK(popup_->IsOpen()); 1193 // If there are no results, the popup should be closed (so we should have 1194 // failed the CHECK above), and URLsForDefaultMatch() should have been 1195 // called instead. 1196 CHECK(!result.empty()); 1197 CHECK(popup_->selected_line() < result.size()); 1198 *match = result.match_at(popup_->selected_line()); 1199 } 1200 if (alternate_nav_url && popup_->manually_selected_match().empty()) 1201 *alternate_nav_url = result.alternate_nav_url(); 1202} 1203 1204void OmniboxEditModel::GetInfoForCurrentText(AutocompleteMatch* match, 1205 GURL* alternate_nav_url) const { 1206 // If there's temporary text and it has been set by Instant, we won't find it 1207 // in the popup model, so classify the text anew. 1208 if ((popup_->IsOpen() || query_in_progress()) && 1209 !is_temporary_text_set_by_instant_) { 1210 InfoForCurrentSelection(match, alternate_nav_url); 1211 } else { 1212 AutocompleteClassifierFactory::GetForProfile(profile_)->Classify( 1213 UserTextFromDisplayText(view_->GetText()), KeywordIsSelected(), true, 1214 match, alternate_nav_url); 1215 } 1216} 1217 1218void OmniboxEditModel::RevertTemporaryText(bool revert_popup) { 1219 // The user typed something, then selected a different item. Restore the 1220 // text they typed and change back to the default item. 1221 // NOTE: This purposefully does not reset paste_state_. 1222 bool notify_instant = is_temporary_text_set_by_instant_; 1223 just_deleted_text_ = false; 1224 has_temporary_text_ = false; 1225 is_temporary_text_set_by_instant_ = false; 1226 1227 InstantController* instant = controller_->GetInstant(); 1228 if (instant && notify_instant) { 1229 // Normally, popup_->ResetToDefaultMatch() will cause the view text to be 1230 // updated. In Instant Extended mode however, the popup_ is not used, so it 1231 // won't do anything. So, update the view ourselves. Even if Instant is not 1232 // in extended mode (i.e., it's enabled in non-extended mode, or disabled 1233 // altogether), this is okay to do, since the call to 1234 // popup_->ResetToDefaultMatch() will just override whatever we do here. 1235 // 1236 // The two "false" arguments make sure that our shenanigans don't cause any 1237 // previously saved selection to be erased nor OnChanged() to be called. 1238 view_->OnTemporaryTextMaybeChanged(user_text_ + inline_autocomplete_text_, 1239 false, false); 1240 AutocompleteResult::const_iterator match(result().default_match()); 1241 instant->OnCancel(match != result().end() ? *match : AutocompleteMatch(), 1242 user_text_, 1243 user_text_ + inline_autocomplete_text_); 1244 } 1245 if (revert_popup) 1246 popup_->ResetToDefaultMatch(); 1247 view_->OnRevertTemporaryText(); 1248} 1249 1250bool OmniboxEditModel::MaybeAcceptKeywordBySpace(const string16& new_text) { 1251 size_t keyword_length = new_text.length() - 1; 1252 return (paste_state_ == NONE) && is_keyword_hint_ && !keyword_.empty() && 1253 inline_autocomplete_text_.empty() && 1254 (keyword_.length() == keyword_length) && 1255 IsSpaceCharForAcceptingKeyword(new_text[keyword_length]) && 1256 !new_text.compare(0, keyword_length, keyword_, 0, keyword_length) && 1257 AcceptKeyword(); 1258} 1259 1260bool OmniboxEditModel::CreatedKeywordSearchByInsertingSpaceInMiddle( 1261 const string16& old_text, 1262 const string16& new_text, 1263 size_t caret_position) const { 1264 DCHECK_GE(new_text.length(), caret_position); 1265 1266 // Check simple conditions first. 1267 if ((paste_state_ != NONE) || (caret_position < 2) || 1268 (old_text.length() < caret_position) || 1269 (new_text.length() == caret_position)) 1270 return false; 1271 size_t space_position = caret_position - 1; 1272 if (!IsSpaceCharForAcceptingKeyword(new_text[space_position]) || 1273 IsWhitespace(new_text[space_position - 1]) || 1274 new_text.compare(0, space_position, old_text, 0, space_position) || 1275 !new_text.compare(space_position, new_text.length() - space_position, 1276 old_text, space_position, 1277 old_text.length() - space_position)) { 1278 return false; 1279 } 1280 1281 // Then check if the text before the inserted space matches a keyword. 1282 string16 keyword; 1283 TrimWhitespace(new_text.substr(0, space_position), TRIM_LEADING, &keyword); 1284 // TODO(sreeram): Once the Instant extended API supports keywords properly, 1285 // keyword_provider() should never be NULL. Remove that clause. 1286 return !keyword.empty() && autocomplete_controller_->keyword_provider() && 1287 !autocomplete_controller_->keyword_provider()-> 1288 GetKeywordForText(keyword).empty(); 1289} 1290 1291bool OmniboxEditModel::DoInstant(const AutocompleteMatch& match) { 1292 InstantController* instant = controller_->GetInstant(); 1293 if (!instant || in_revert_) 1294 return false; 1295 1296 // Don't call Update() if the change is a result of a 1297 // INSTANT_COMPLETE_REPLACE instant suggestion. 1298 if (has_temporary_text_ && is_temporary_text_set_by_instant_) 1299 return false; 1300 1301 // The two pieces of text we want to send Instant, viz., what the user has 1302 // typed, and the full omnibox text including any inline autocompletion. 1303 string16 user_text = has_temporary_text_ ? 1304 match.fill_into_edit : DisplayTextFromUserText(user_text_); 1305 string16 full_text = view_->GetText(); 1306 1307 // Remove "?" if we're in forced query mode. 1308 AutocompleteInput::RemoveForcedQueryStringIfNecessary( 1309 autocomplete_controller_->input().type(), &user_text); 1310 AutocompleteInput::RemoveForcedQueryStringIfNecessary( 1311 autocomplete_controller_->input().type(), &full_text); 1312 1313 size_t start, end; 1314 view_->GetSelectionBounds(&start, &end); 1315 1316 return instant->Update(match, user_text, full_text, start, end, 1317 UseVerbatimInstant(), user_input_in_progress_, popup_->IsOpen(), 1318 in_escape_handler_, KeywordIsSelected()); 1319} 1320 1321void OmniboxEditModel::DoPreconnect(const AutocompleteMatch& match) { 1322 if (!match.destination_url.SchemeIs(extensions::kExtensionScheme)) { 1323 // Warm up DNS Prefetch cache, or preconnect to a search service. 1324 UMA_HISTOGRAM_ENUMERATION("Autocomplete.MatchType", match.type, 1325 AutocompleteMatch::NUM_TYPES); 1326 if (profile_->GetNetworkPredictor()) { 1327 profile_->GetNetworkPredictor()->AnticipateOmniboxUrl( 1328 match.destination_url, 1329 AutocompleteActionPredictor::IsPreconnectable(match)); 1330 } 1331 // We could prefetch the alternate nav URL, if any, but because there 1332 // can be many of these as a user types an initial series of characters, 1333 // the OS DNS cache could suffer eviction problems for minimal gain. 1334 } 1335} 1336 1337// static 1338bool OmniboxEditModel::IsSpaceCharForAcceptingKeyword(wchar_t c) { 1339 switch (c) { 1340 case 0x0020: // Space 1341 case 0x3000: // Ideographic Space 1342 return true; 1343 default: 1344 return false; 1345 } 1346} 1347 1348metrics::OmniboxEventProto::PageClassification 1349 OmniboxEditModel::ClassifyPage(const GURL& gurl) const { 1350 if (!gurl.is_valid()) 1351 return metrics::OmniboxEventProto_PageClassification_INVALID_SPEC; 1352 const std::string& url = gurl.spec(); 1353 if (url == chrome::kChromeUINewTabURL) 1354 return metrics::OmniboxEventProto_PageClassification_NEW_TAB_PAGE; 1355 if (url == chrome::kAboutBlankURL) 1356 return metrics::OmniboxEventProto_PageClassification_BLANK; 1357 if (url == profile()->GetPrefs()->GetString(prefs::kHomePage)) 1358 return metrics::OmniboxEventProto_PageClassification_HOMEPAGE; 1359 return metrics::OmniboxEventProto_PageClassification_OTHER; 1360} 1361 1362void OmniboxEditModel::ClassifyStringForPasteAndGo( 1363 const string16& text, 1364 AutocompleteMatch* match, 1365 GURL* alternate_nav_url) const { 1366 DCHECK(match); 1367 AutocompleteClassifierFactory::GetForProfile(profile_)->Classify(text, 1368 false, false, match, alternate_nav_url); 1369} 1370 1371void OmniboxEditModel::SetFocusState(OmniboxFocusState state, 1372 OmniboxFocusChangeReason reason) { 1373 if (state == focus_state_) 1374 return; 1375 1376 InstantController* instant = controller_->GetInstant(); 1377 if (instant) 1378 instant->OmniboxFocusChanged(state, reason, NULL); 1379 1380 // Update state and notify view if the omnibox has focus and the caret 1381 // visibility changed. 1382 const bool was_caret_visible = is_caret_visible(); 1383 focus_state_ = state; 1384 if (focus_state_ != OMNIBOX_FOCUS_NONE && 1385 is_caret_visible() != was_caret_visible) 1386 view_->ApplyCaretVisibility(); 1387} 1388