summaryrefslogtreecommitdiffstats
path: root/chrome/browser/autocomplete/autocomplete_popup_model.cc
diff options
context:
space:
mode:
Diffstat (limited to 'chrome/browser/autocomplete/autocomplete_popup_model.cc')
-rw-r--r--chrome/browser/autocomplete/autocomplete_popup_model.cc361
1 files changed, 361 insertions, 0 deletions
diff --git a/chrome/browser/autocomplete/autocomplete_popup_model.cc b/chrome/browser/autocomplete/autocomplete_popup_model.cc
new file mode 100644
index 0000000..1e2f21f
--- /dev/null
+++ b/chrome/browser/autocomplete/autocomplete_popup_model.cc
@@ -0,0 +1,361 @@
+// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/autocomplete/autocomplete_popup_model.h"
+
+// TODO(deanm): Clean up these includes, not going to fight it now.
+#include <cmath>
+
+#include "base/scoped_ptr.h"
+#include "base/string_util.h"
+#include "chrome/browser/autocomplete/autocomplete_edit.h"
+#include "chrome/browser/autocomplete/autocomplete_popup_view.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/net/dns_global.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/search_engines/template_url.h"
+#include "chrome/browser/search_engines/template_url_model.h"
+#include "chrome/browser/views/location_bar_view.h"
+#include "chrome/common/gfx/chrome_canvas.h"
+#include "chrome/common/l10n_util.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/common/resource_bundle.h"
+#include "grit/theme_resources.h"
+#include "third_party/icu38/public/common/unicode/ubidi.h"
+
+AutocompletePopupModel::AutocompletePopupModel(
+ const ChromeFont& font,
+ AutocompleteEditView* edit_view,
+ AutocompleteEditModel* edit_model,
+ Profile* profile)
+ : view_(new AutocompletePopupView(this, font, edit_view)),
+ edit_model_(edit_model),
+ controller_(new AutocompleteController(profile)),
+ profile_(profile),
+ hovered_line_(kNoMatch),
+ selected_line_(kNoMatch),
+ inside_synchronous_query_(false) {
+ registrar_.Add(
+ this,
+ NotificationType::AUTOCOMPLETE_CONTROLLER_RESULT_UPDATED,
+ Source<AutocompleteController>(controller_.get()));
+ registrar_.Add(
+ this,
+ NotificationType::AUTOCOMPLETE_CONTROLLER_SYNCHRONOUS_MATCHES_AVAILABLE,
+ Source<AutocompleteController>(controller_.get()));
+}
+
+AutocompletePopupModel::~AutocompletePopupModel() {
+ StopAutocomplete();
+}
+
+void AutocompletePopupModel::SetProfile(Profile* profile) {
+ DCHECK(profile);
+ profile_ = profile;
+ controller_->SetProfile(profile);
+}
+
+void AutocompletePopupModel::StartAutocomplete(
+ const std::wstring& text,
+ const std::wstring& desired_tld,
+ bool prevent_inline_autocomplete,
+ bool prefer_keyword) {
+ // The user is interacting with the edit, so stop tracking hover.
+ SetHoveredLine(kNoMatch);
+
+ manually_selected_match_.Clear();
+
+ controller_->Start(text, desired_tld, prevent_inline_autocomplete,
+ prefer_keyword, false);
+}
+
+void AutocompletePopupModel::StopAutocomplete() {
+ controller_->Stop(true);
+ SetHoveredLine(kNoMatch);
+ selected_line_ = kNoMatch;
+ view_->UpdatePopupAppearance();
+}
+
+bool AutocompletePopupModel::IsOpen() const {
+ return view_->IsOpen();
+}
+
+void AutocompletePopupModel::SetHoveredLine(size_t line) {
+ const bool is_disabling = (line == kNoMatch);
+ DCHECK(is_disabling || (line < controller_->result().size()));
+
+ if (line == hovered_line_)
+ return; // Nothing to do
+
+ // Make sure the old hovered line is redrawn. No need to redraw the selected
+ // line since selection overrides hover so the appearance won't change.
+ const bool is_enabling = (hovered_line_ == kNoMatch);
+ if (!is_enabling && (hovered_line_ != selected_line_))
+ view_->InvalidateLine(hovered_line_);
+
+ // Change the hover to the new line and make sure it's redrawn.
+ hovered_line_ = line;
+ if (!is_disabling && (hovered_line_ != selected_line_))
+ view_->InvalidateLine(hovered_line_);
+
+ if (is_enabling || is_disabling)
+ view_->OnHoverEnabledOrDisabled(is_disabling);
+}
+
+void AutocompletePopupModel::SetSelectedLine(size_t line,
+ bool reset_to_default) {
+ const AutocompleteResult& result = controller_->result();
+ DCHECK(line < result.size());
+ if (result.empty())
+ return;
+
+ // Cancel the query so the matches don't change on the user.
+ controller_->Stop(false);
+
+ const AutocompleteMatch& match = result.match_at(line);
+ if (reset_to_default) {
+ manually_selected_match_.Clear();
+ } else {
+ // Track the user's selection until they cancel it.
+ manually_selected_match_.destination_url = match.destination_url;
+ manually_selected_match_.provider_affinity = match.provider;
+ manually_selected_match_.is_history_what_you_typed_match =
+ match.is_history_what_you_typed_match;
+ }
+
+ if (line == selected_line_)
+ return; // Nothing else to do.
+
+ // Update the edit with the new data for this match.
+ std::wstring keyword;
+ const bool is_keyword_hint = GetKeywordForMatch(match, &keyword);
+ edit_model_->OnPopupDataChanged(
+ reset_to_default ? std::wstring() : match.fill_into_edit,
+ !reset_to_default, keyword, is_keyword_hint, match.type);
+
+ // Repaint old and new selected lines immediately, so that the edit doesn't
+ // appear to update [much] faster than the popup. We must not update
+ // |selected_line_| before calling OnPopupDataChanged() (since the edit may
+ // call us back to get data about the old selection), and we must not call
+ // UpdateWindow() before updating |selected_line_| (since the paint routine
+ // relies on knowing the correct selected line).
+ view_->InvalidateLine(selected_line_);
+ selected_line_ = line;
+ view_->InvalidateLine(selected_line_);
+ view_->UpdateWindow();
+}
+
+void AutocompletePopupModel::ResetToDefaultMatch() {
+ const AutocompleteResult& result = controller_->result();
+ DCHECK(!result.empty());
+ SetSelectedLine(result.default_match() - result.begin(), true);
+}
+
+GURL AutocompletePopupModel::URLsForCurrentSelection(
+ PageTransition::Type* transition,
+ bool* is_history_what_you_typed_match,
+ GURL* alternate_nav_url) const {
+ // We need to use the result on the controller, because if the popup is open,
+ // the user changes the contents of the edit, and then presses enter before
+ // any results have been displayed, results_ will be nonempty but wrong. (In
+ // most other cases, the controller's results will match the popup's.)
+ // TODO(pkasting): If manually_selected_match_ moves to the controller, this
+ // can move to the edit.
+ if (controller_->result().empty())
+ return GURL();
+
+ const AutocompleteResult& result = controller_->result();
+ AutocompleteResult::const_iterator match;
+ if (!controller_->done()) {
+ // The user cannot have manually selected a match, or the query would have
+ // stopped. So the default match must be the desired selection.
+ match = result.default_match();
+ } else {
+ // The query isn't running, so the popup can't possibly be out of date.
+ DCHECK(selected_line_ < result.size());
+ match = result.begin() + selected_line_;
+ }
+ if (transition)
+ *transition = match->transition;
+ if (is_history_what_you_typed_match)
+ *is_history_what_you_typed_match = match->is_history_what_you_typed_match;
+ if (alternate_nav_url && manually_selected_match_.empty())
+ *alternate_nav_url = result.GetAlternateNavURL(controller_->input(), match);
+ return match->destination_url;
+}
+
+GURL AutocompletePopupModel::URLsForDefaultMatch(
+ const std::wstring& text,
+ const std::wstring& desired_tld,
+ PageTransition::Type* transition,
+ bool* is_history_what_you_typed_match,
+ GURL* alternate_nav_url) {
+ // We had better not already be doing anything, or this call will blow it
+ // away.
+ DCHECK(!IsOpen());
+ DCHECK(controller_->done());
+
+ // Run the new query and get only the synchronously available matches.
+ inside_synchronous_query_ = true; // Tell Observe() not to notify the edit or
+ // update our appearance.
+ controller_->Start(text, desired_tld, true, false, true);
+ inside_synchronous_query_ = false;
+ DCHECK(controller_->done());
+ const AutocompleteResult& result = controller_->result();
+ if (result.empty())
+ return GURL();
+
+ // Get the URLs for the default match.
+ const AutocompleteResult::const_iterator match = result.default_match();
+ if (transition)
+ *transition = match->transition;
+ if (is_history_what_you_typed_match)
+ *is_history_what_you_typed_match = match->is_history_what_you_typed_match;
+ if (alternate_nav_url)
+ *alternate_nav_url = result.GetAlternateNavURL(controller_->input(), match);
+ return match->destination_url;
+}
+
+bool AutocompletePopupModel::GetKeywordForMatch(const AutocompleteMatch& match,
+ std::wstring* keyword) const {
+ // Assume we have no keyword until we find otherwise.
+ keyword->clear();
+
+ // If the current match is a keyword, return that as the selected keyword.
+ if (match.template_url && match.template_url->url() &&
+ match.template_url->url()->SupportsReplacement()) {
+ keyword->assign(match.template_url->keyword());
+ return false;
+ }
+
+ // See if the current match's fill_into_edit corresponds to a keyword.
+ if (!profile_->GetTemplateURLModel())
+ return false;
+ profile_->GetTemplateURLModel()->Load();
+ const std::wstring keyword_hint(
+ TemplateURLModel::CleanUserInputKeyword(match.fill_into_edit));
+ if (keyword_hint.empty())
+ return false;
+
+ // Don't provide a hint if this keyword doesn't support replacement.
+ const TemplateURL* const template_url =
+ profile_->GetTemplateURLModel()->GetTemplateURLForKeyword(keyword_hint);
+ if (!template_url || !template_url->url() ||
+ !template_url->url()->SupportsReplacement())
+ return false;
+
+ keyword->assign(keyword_hint);
+ return true;
+}
+
+AutocompleteLog* AutocompletePopupModel::GetAutocompleteLog() {
+ return new AutocompleteLog(controller_->input().text(),
+ controller_->input().type(), selected_line_, 0, controller_->result());
+}
+
+void AutocompletePopupModel::Move(int count) {
+ // TODO(pkasting): Temporary hack. If the query is running while the popup is
+ // open, we might be showing the results of the previous query still. Force
+ // the popup to display the latest results so the popup and the controller
+ // aren't out of sync. The better fix here is to roll the controller back to
+ // be in sync with what the popup is showing.
+ if (IsOpen() && !controller_->done()) {
+ Observe(NotificationType::AUTOCOMPLETE_CONTROLLER_RESULT_UPDATED,
+ Source<AutocompleteController>(controller_.get()),
+ NotificationService::NoDetails());
+ }
+
+ const AutocompleteResult& result = controller_->result();
+ if (result.empty())
+ return;
+
+ // The user is using the keyboard to change the selection, so stop tracking
+ // hover.
+ SetHoveredLine(kNoMatch);
+
+ // Clamp the new line to [0, result_.count() - 1].
+ const size_t new_line = selected_line_ + count;
+ SetSelectedLine((((count < 0) && (new_line >= selected_line_)) ?
+ 0 : std::min(new_line, result.size() - 1)), false);
+}
+
+void AutocompletePopupModel::TryDeletingCurrentItem() {
+ // We could use URLsForCurrentSelection() here, but it seems better to try
+ // and shift-delete the actual selection, rather than any "in progress, not
+ // yet visible" one.
+ if (selected_line_ == kNoMatch)
+ return;
+ const AutocompleteMatch& match =
+ controller_->result().match_at(selected_line_);
+ if (match.deletable) {
+ const size_t selected_line = selected_line_;
+ controller_->DeleteMatch(match); // This will synchronously notify us that
+ // the results have changed.
+ const AutocompleteResult& result = controller_->result();
+ if (!result.empty()) {
+ // Move the selection to the next choice after the deleted one.
+ // TODO(pkasting): Eventually the controller should take care of this
+ // before notifying us, reducing flicker. At that point the check for
+ // deletability can move there too.
+ SetSelectedLine(std::min(result.size() - 1, selected_line), false);
+ }
+ }
+}
+
+void AutocompletePopupModel::Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ if (inside_synchronous_query_)
+ return;
+
+ const AutocompleteResult& result = controller_->result();
+ switch (type.value) {
+ case NotificationType::AUTOCOMPLETE_CONTROLLER_RESULT_UPDATED: {
+ selected_line_ = (result.default_match() == result.end()) ?
+ kNoMatch : (result.default_match() - result.begin());
+ // If we're going to trim the window size to no longer include the hovered
+ // line, turn hover off. Practically, this shouldn't happen, but it
+ // doesn't hurt to be defensive.
+ if ((hovered_line_ != kNoMatch) && (result.size() <= hovered_line_))
+ SetHoveredLine(kNoMatch);
+
+ view_->UpdatePopupAppearance();
+ }
+ // FALL THROUGH
+
+ case NotificationType::AUTOCOMPLETE_CONTROLLER_SYNCHRONOUS_MATCHES_AVAILABLE: {
+ // Update the edit with the possibly new data for this match.
+ // NOTE: This must be done after the code above, so that our internal
+ // state will be consistent when the edit calls back to
+ // URLsForCurrentSelection().
+ std::wstring inline_autocomplete_text;
+ std::wstring keyword;
+ bool is_keyword_hint = false;
+ AutocompleteMatch::Type type = AutocompleteMatch::SEARCH_WHAT_YOU_TYPED;
+ const AutocompleteResult::const_iterator match(result.default_match());
+ if (match != result.end()) {
+ if ((match->inline_autocomplete_offset != std::wstring::npos) &&
+ (match->inline_autocomplete_offset <
+ match->fill_into_edit.length())) {
+ inline_autocomplete_text =
+ match->fill_into_edit.substr(match->inline_autocomplete_offset);
+ }
+ // Warm up DNS Prefetch Cache.
+ chrome_browser_net::DnsPrefetchUrl(match->destination_url);
+ // We could prefetch the alternate nav URL, if any, but because there
+ // can be many of these as a user types an initial series of characters,
+ // the OS DNS cache could suffer eviction problems for minimal gain.
+
+ is_keyword_hint = GetKeywordForMatch(*match, &keyword);
+ type = match->type;
+ }
+ edit_model_->OnPopupDataChanged(inline_autocomplete_text, false, keyword,
+ is_keyword_hint, type);
+ return;
+ }
+
+ default:
+ NOTREACHED();
+ }
+}