blob: 7456ccd8eef152f91ff7b745ac82451f0734ffe5 [file] [log] [blame]
// Copyright (c) 2010 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"
#include "unicode/ubidi.h"
#include "base/string_util.h"
#include "chrome/browser/autocomplete/autocomplete_edit.h"
#include "chrome/browser/autocomplete/autocomplete_popup_view.h"
#include "chrome/browser/profile.h"
#include "chrome/browser/extensions/extensions_service.h"
#include "chrome/browser/search_engines/template_url.h"
#include "chrome/browser/search_engines/template_url_model.h"
#include "chrome/common/notification_service.h"
#include "gfx/rect.h"
///////////////////////////////////////////////////////////////////////////////
// AutocompletePopupModel
AutocompletePopupModel::AutocompletePopupModel(
AutocompletePopupView* popup_view,
AutocompleteEditModel* edit_model,
Profile* profile)
: view_(popup_view),
edit_model_(edit_model),
controller_(new AutocompleteController(profile)),
profile_(profile),
hovered_line_(kNoMatch),
selected_line_(kNoMatch) {
registrar_.Add(this, NotificationType::AUTOCOMPLETE_CONTROLLER_RESULT_UPDATED,
Source<AutocompleteController>(controller_.get()));
}
AutocompletePopupModel::~AutocompletePopupModel() {
}
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);
}
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.
if ((hovered_line_ != kNoMatch) && (hovered_line_ != selected_line_))
view_->InvalidateLine(hovered_line_);
// Change the hover to the new line.
hovered_line_ = line;
if (!is_disabling && (hovered_line_ != selected_line_))
view_->InvalidateLine(hovered_line_);
}
void AutocompletePopupModel::SetSelectedLine(size_t line,
bool reset_to_default) {
// We should at least be dealing with the results of the current query. Note
// that even if |line| was valid on entry, this may make it invalid. We clamp
// it below.
controller_->CommitIfQueryHasNeverBeenCommitted();
const AutocompleteResult& result = controller_->result();
if (result.empty())
return;
// Cancel the query so the matches don't change on the user.
controller_->Stop(false);
line = std::min(line, result.size() - 1);
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.
// We need to update |selected_line_| before calling OnPopupDataChanged(), so
// that when the edit notifies its controller that something has changed, the
// controller can get the correct updated data.
//
// NOTE: We should never reach here with no selected line; the same code that
// opened the popup and made it possible to get here should have also set a
// selected line.
CHECK(selected_line_ != kNoMatch);
GURL current_destination(result.match_at(selected_line_).destination_url);
view_->InvalidateLine(selected_line_);
selected_line_ = line;
view_->InvalidateLine(selected_line_);
// Update the edit with the new data for this match.
// TODO(pkasting): If |selected_line_| moves to the controller, this can be
// eliminated and just become a call to the observer on the edit.
std::wstring keyword;
const bool is_keyword_hint = GetKeywordForMatch(match, &keyword);
if (reset_to_default) {
std::wstring inline_autocomplete_text;
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);
}
edit_model_->OnPopupDataChanged(inline_autocomplete_text, NULL,
keyword, is_keyword_hint);
} else {
edit_model_->OnPopupDataChanged(match.fill_into_edit, &current_destination,
keyword, is_keyword_hint);
}
// Repaint old and new selected lines immediately, so that the edit doesn't
// appear to update [much] faster than the popup.
view_->PaintUpdatesNow();
}
void AutocompletePopupModel::ResetToDefaultMatch() {
const AutocompleteResult& result = controller_->result();
CHECK(!result.empty());
SetSelectedLine(result.default_match() - result.begin(), true);
view_->OnDragCanceled();
}
void AutocompletePopupModel::InfoForCurrentSelection(
AutocompleteMatch* match,
GURL* alternate_nav_url) const {
DCHECK(match != NULL);
const AutocompleteResult* result;
if (!controller_->done()) {
// NOTE: Using latest_result() is important here since not only could it
// contain newer results than result() for the current query, it could even
// refer to an entirely different query (e.g. if the user is typing rapidly
// and the controller is purposefully delaying updates to avoid flicker).
result = &controller_->latest_result();
// It's technically possible for |result| to be empty if no provider returns
// a synchronous result but the query has not completed synchronously;
// pratically, however, that should never actually happen.
if (result->empty())
return;
// 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 {
CHECK(IsOpen());
// The query isn't running, so the standard result set can't possibly be out
// of date.
//
// NOTE: In practice, it should actually be safe to use
// controller_->latest_result() here too, since the controller keeps that
// up-to-date. However we generally try to avoid referring to that.
result = &controller_->result();
// If there are no results, the popup should be closed (so we should have
// failed the CHECK above), and URLsForDefaultMatch() should have been
// called instead.
CHECK(!result->empty());
CHECK(selected_line_ < result->size());
*match = result->match_at(selected_line_);
}
if (alternate_nav_url && manually_selected_match_.empty())
*alternate_nav_url = result->alternate_nav_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 (TemplateURL::SupportsReplacement(match.template_url)) {
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 (!TemplateURL::SupportsReplacement(template_url))
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) {
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 : new_line,
false);
}
void AutocompletePopupModel::TryDeletingCurrentItem() {
// We could use InfoForCurrentSelection() 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;
// Cancel the query so the matches don't change on the user.
controller_->Stop(false);
const AutocompleteMatch& match =
controller_->result().match_at(selected_line_);
if (match.deletable) {
const size_t selected_line = selected_line_;
controller_->DeleteMatch(match); // This may 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.
// SetSelectedLine() will clamp to take care of the case where we deleted
// the last item.
// 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(selected_line, false);
}
}
}
void AutocompletePopupModel::Observe(NotificationType type,
const NotificationSource& source,
const NotificationDetails& details) {
DCHECK_EQ(NotificationType::AUTOCOMPLETE_CONTROLLER_RESULT_UPDATED,
type.value);
const AutocompleteResult* result =
Details<const AutocompleteResult>(details).ptr();
selected_line_ = result->default_match() == result->end() ?
kNoMatch : static_cast<size_t>(result->default_match() - result->begin());
// There had better not be a nonempty result set with no default match.
CHECK((selected_line_ != kNoMatch) || result->empty());
// 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();
#if defined(TOOLKIT_VIEWS)
edit_model_->PopupBoundsChangedTo(view_->GetTargetBounds());
#else
// TODO: port
#endif
}
const SkBitmap* AutocompletePopupModel::GetSpecialIconForMatch(
const AutocompleteMatch& match) const {
if (!match.template_url || !match.template_url->IsExtensionKeyword())
return NULL;
return &profile_->GetExtensionsService()->GetOmniboxPopupIcon(
match.template_url->GetExtensionId());
}