diff options
author | initial.commit <initial.commit@0039d316-1c4b-4281-b951-d872f2087c98> | 2008-07-26 23:55:29 +0000 |
---|---|---|
committer | initial.commit <initial.commit@0039d316-1c4b-4281-b951-d872f2087c98> | 2008-07-26 23:55:29 +0000 |
commit | 09911bf300f1a419907a9412154760efd0b7abc3 (patch) | |
tree | f131325fb4e2ad12c6d3504ab75b16dd92facfed /chrome/browser/autocomplete | |
parent | 586acc5fe142f498261f52c66862fa417c3d52d2 (diff) | |
download | chromium_src-09911bf300f1a419907a9412154760efd0b7abc3.zip chromium_src-09911bf300f1a419907a9412154760efd0b7abc3.tar.gz chromium_src-09911bf300f1a419907a9412154760efd0b7abc3.tar.bz2 |
Add chrome to the repository.
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@15 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/autocomplete')
20 files changed, 10677 insertions, 0 deletions
diff --git a/chrome/browser/autocomplete/autocomplete.cc b/chrome/browser/autocomplete/autocomplete.cc new file mode 100644 index 0000000..ecc3bbd --- /dev/null +++ b/chrome/browser/autocomplete/autocomplete.cc @@ -0,0 +1,754 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include <algorithm> + +#include "chrome/browser/autocomplete/autocomplete.h" + +#include "base/string_util.h" +#include "chrome/browser/autocomplete/history_url_provider.h" +#include "chrome/browser/autocomplete/history_contents_provider.h" +#include "chrome/browser/autocomplete/keyword_provider.h" +#include "chrome/browser/autocomplete/search_provider.h" +#include "chrome/browser/external_protocol_handler.h" +#include "chrome/browser/history_tab_ui.h" +#include "chrome/browser/url_fixer_upper.h" +#include "chrome/common/gfx/url_elider.h" +#include "chrome/common/l10n_util.h" +#include "chrome/common/pref_names.h" +#include "chrome/common/pref_service.h" +#include "googleurl/src/url_canon_ip.h" +#include "net/base/net_util.h" +#include "net/base/registry_controlled_domain.h" + +#include "generated_resources.h" + + +// AutocompleteInput ---------------------------------------------------------- + +AutocompleteInput::AutocompleteInput(const std::wstring& text, + const std::wstring& desired_tld, + bool prevent_inline_autocomplete) + : desired_tld_(desired_tld), + prevent_inline_autocomplete_(prevent_inline_autocomplete) { + // Trim whitespace from edges of input; don't inline autocomplete if there + // was trailing whitespace. + if (TrimWhitespace(text, TRIM_ALL, &text_) & TRIM_TRAILING) + prevent_inline_autocomplete_ = true; + + url_parse::Parsed parts; + type_ = Parse(text_, desired_tld, &parts, &scheme_); + + if (type_ == INVALID) + return; + + if (type_ == FORCED_QUERY && text_[0] == L'?') + text_.erase(0, 1); +} + +//static +AutocompleteInput::Type AutocompleteInput::Parse(const std::wstring& text, + const std::wstring& desired_tld, + url_parse::Parsed* parts, + std::wstring* scheme) { + DCHECK(parts); + + const size_t first_non_white = text.find_first_not_of(kWhitespaceWide, 0); + if (first_non_white == std::wstring::npos) + return INVALID; // All whitespace. + + if (text.at(first_non_white) == L'?') { + // If the first non-whitespace character is a '?', we magically treat this + // as a query. + return FORCED_QUERY; + } + + // Ask our parsing back-end to help us understand what the user typed. We + // use the URLFixerUpper here because we want to be smart about what we + // consider a scheme. For example, we shouldn't consider www.google.com:80 + // to have a scheme. + const std::wstring parsed_scheme(URLFixerUpper::SegmentURL(text, parts)); + if (scheme) + *scheme = parsed_scheme; + + if (parsed_scheme == L"file") { + // A user might or might not type a scheme when entering a file URL. + return URL; + } + + // If the user typed a scheme, determine our available actions based on that. + if (parts->scheme.is_valid()) { + // See if we know how to handle the URL internally. + if (URLRequest::IsHandledProtocol(WideToASCII(parsed_scheme))) + return URL; + + // There are also some schemes that we convert to other things before they + // reach the renderer or else the renderer handles internally without + // reaching the URLRequest logic. We thus won't catch these above, but we + // should still claim to handle them. + if ((parsed_scheme == L"view-source") || (parsed_scheme == L"javascript") || + (parsed_scheme == L"data")) + return URL; + + // Finally, check and see if the user has explicitly opened this scheme as + // a URL before. We need to do this last because some schemes may be in + // here as "blocked" (e.g. "javascript") because we don't want pages to open + // them, but users still can. + switch (ExternalProtocolHandler::GetBlockState(parsed_scheme)) { + case ExternalProtocolHandler::DONT_BLOCK: + return URL; + + case ExternalProtocolHandler::BLOCK: + // If we don't want the user to open the URL, don't let it be navigated + // to at all. + return QUERY; + + default: + // We don't know about this scheme. It's likely to be a search operator + // like "site:" or "link:". We classify it as UNKNOWN so the user has + // the option of treating it as a URL if we're wrong. + // Note that SegmentURL() is smart so we aren't tricked by "c:\foo" or + // "www.example.com:81" in this case. + return UNKNOWN; + } + } + + // The user didn't type a scheme. Assume that this is either an HTTP URL or + // not a URL at all; try to determine which. + + // It's not clear that we can reach here with an empty "host" (maybe on some + // kinds of garbage input?), but if we did, it couldn't be a URL. + if (!parts->host.is_nonempty()) + return QUERY; + // (We use the registry length later below but ask for it here so we can check + // the host's validity at this point.) + const std::wstring host(text.substr(parts->host.begin, parts->host.len)); + const size_t registry_length = + RegistryControlledDomainService::GetRegistryLength(host, false); + if (registry_length == std::wstring::npos) + return QUERY; // It's not clear to me that we can reach this... + + // A space in the "host" means this is a query. (Technically, IE and GURL + // allow hostnames with spaces for wierd intranet machines, but it's supposed + // to be illegal and I'm not worried about users trying to type these in.) + if (host.find(' ') != std::wstring::npos) + return QUERY; + + // Presence of a password/port mean this is almost certainly a URL. We don't + // treat usernames (without passwords) as indicating a URL, because this could + // be an email address like "user@mail.com" which is more likely a search than + // an HTTP auth login attempt. + if (parts->password.is_nonempty() || parts->port.is_nonempty()) + return URL; + + // See if the host is an IP address. + bool is_ip_address; + net_util::CanonicalizeHost(host, &is_ip_address); + if (is_ip_address) { + // If the user originally typed a host that looks like an IP address (a + // dotted quad), they probably want to open it. If the original input was + // something else (like a single number), they probably wanted to search for + // it. This is true even if the URL appears to have a path: "1.2/45" is + // more likely a search (for the answer to a math problem) than a URL. + url_parse::Component components[4]; + const bool found_ipv4 = + url_canon::FindIPv4Components(text.c_str(), parts->host, components); + DCHECK(found_ipv4); + for (size_t i = 0; i < arraysize(components); ++i) { + if (!components[i].is_nonempty()) + return UNKNOWN; + } + return URL; + } + + // The host doesn't look like a number, so see if the user's given us a path. + if (parts->path.is_nonempty()) { + // Most inputs with paths are URLs, even ones without known registries (e.g. + // intranet URLs). However, if there's no known registry, and the path has + // a space, this is more likely a query with a slash in the first term (e.g. + // "ps/2 games") than a URL. We can still open URLs with spaces in the path + // by escaping the space, and we will still inline autocomplete them if + // users have typed them in the past, but we default to searching since + // that's the common case. + return ((registry_length == 0) && + (text.substr(parts->path.begin, parts->path.len).find(' ') != + std::wstring::npos)) ? UNKNOWN : URL; + } + + // If we reach here with a username, our input looks like "user@host"; this is + // the case mentioned above, where we think this is more likely an email + // address than an HTTP auth attempt, so search for it. + if (parts->username.is_nonempty()) + return UNKNOWN; + + // We have a bare host string. See if it has a known TLD. If so, it's + // probably a URL. + if (registry_length != 0) + return URL; + + // No TLD that we know about. This could be: + // * A string that the user wishes to add a desired_tld to to get a URL. If + // we reach this point, we know there's no known TLD on the string, so the + // fixup code will be willing to add one; thus this is a URL. + // * A single word "foo"; possibly an intranet site, but more likely a search. + // This is ideally an UNKNOWN, and we can let the Alternate Nav URL code + // catch our mistakes. + // * A URL with a valid TLD we don't know about yet. If e.g. a registrar adds + // "xxx" as a TLD, then until we add it to our data file, Chrome won't know + // "foo.xxx" is a real URL. So ideally this is a URL, but we can't really + // distinguish this case from: + // * A "URL-like" string that's not really a URL (like + // "browser.tabs.closeButtons" or "java.awt.event.*"). This is ideally a + // QUERY. Since the above case and this one are indistinguishable, and this + // case is likely to be much more common, just say these are both UNKNOWN, + // which should default to the right thing and let users correct us on a + // case-by-case basis. + return desired_tld.empty() ? UNKNOWN : REQUESTED_URL; +} + +bool AutocompleteInput::Equals(const AutocompleteInput& other) const { + return (text_ == other.text_) && + (type_ == other.type_) && + (desired_tld_ == other.desired_tld_) && + (scheme_ == other.scheme_) && + (prevent_inline_autocomplete_ == other.prevent_inline_autocomplete_); +} + +void AutocompleteInput::Clear() { + text_.clear(); + type_ = INVALID; + scheme_.clear(); + desired_tld_.clear(); + prevent_inline_autocomplete_ = false; +} + +// AutocompleteMatch ---------------------------------------------------------- + +AutocompleteMatch::AutocompleteMatch() + : provider(NULL), + relevance(0), + deletable(false), + inline_autocomplete_offset(std::wstring::npos), + transition(PageTransition::TYPED), + is_history_what_you_typed_match(false), + type(URL), + template_url(NULL), + starred(false) { +} + +AutocompleteMatch::AutocompleteMatch(AutocompleteProvider* provider, + int relevance, + bool deletable) + : provider(provider), + relevance(relevance), + deletable(deletable), + inline_autocomplete_offset(std::wstring::npos), + transition(PageTransition::TYPED), + is_history_what_you_typed_match(false), + type(URL), + template_url(NULL), + starred(false) { +} + +// static +bool AutocompleteMatch::MoreRelevant(const AutocompleteMatch& elem1, + const AutocompleteMatch& elem2) { + // For equal-relevance matches, we sort alphabetically, so that providers + // who return multiple elements at the same priority get a "stable" sort + // across multiple updates. + if (elem1.relevance == elem2.relevance) + return elem1.contents > elem2.contents; + + // A negative relevance indicates the real relevance can be determined by + // negating the value. If both relevances are negative, negate the result + // so that we end up with positive relevances, then negative relevances with + // the negative relevances sorted by absolute values. + const bool result = elem1.relevance > elem2.relevance; + return (elem1.relevance < 0 && elem2.relevance < 0) ? !result : result; +} + +// static +bool AutocompleteMatch::DestinationSortFunc(const AutocompleteMatch& elem1, + const AutocompleteMatch& elem2) { + // Sort identical destination_urls together. Place the most relevant matches + // first, so that when we call std::unique(), these are the ones that get + // preserved. + return (elem1.destination_url != elem2.destination_url) ? + (elem1.destination_url < elem2.destination_url) : + MoreRelevant(elem1, elem2); +} + +// static +bool AutocompleteMatch::DestinationsEqual(const AutocompleteMatch& elem1, + const AutocompleteMatch& elem2) { + return elem1.destination_url == elem2.destination_url; +} + +// static +void AutocompleteMatch::ClassifyMatchInString( + const std::wstring& find_text, + const std::wstring& text, + int style, + ACMatchClassifications* classification) { + ClassifyLocationInString(text.find(find_text), find_text.length(), + text.length(), style, classification); +} + +void AutocompleteMatch::ClassifyLocationInString( + size_t match_location, + size_t match_length, + size_t overall_length, + int style, + ACMatchClassifications* classification) { + // Classifying an empty match makes no sense and will lead to validation + // errors later. + DCHECK(match_length > 0); + + classification->clear(); + + // Don't classify anything about an empty string + // (AutocompleteMatch::Validate() checks this). + if (overall_length == 0) + return; + + // Mark pre-match portion of string (if any). + if (match_location != 0) { + classification->push_back(ACMatchClassification(0, style)); + } + + // Mark matching portion of string. + if (match_location == std::wstring::npos) { + // No match, above classification will suffice for whole string. + return; + } + classification->push_back(ACMatchClassification(match_location, + (style | ACMatchClassification::MATCH) & ~ACMatchClassification::DIM)); + + // Mark post-match portion of string (if any). + const size_t after_match(match_location + match_length); + if (after_match < overall_length) { + classification->push_back(ACMatchClassification(after_match, style)); + } +} + +#ifndef NDEBUG +void AutocompleteMatch::Validate() const { + ValidateClassifications(contents, contents_class); + ValidateClassifications(description, description_class); +} + +void AutocompleteMatch::ValidateClassifications( + const std::wstring& text, + const ACMatchClassifications& classifications) const { + if (text.empty()) { + DCHECK(classifications.size() == 0); + return; + } + + // The classifications should always cover the whole string. + DCHECK(classifications.size() > 0) << "No classification for text"; + DCHECK(classifications[0].offset == 0) << "Classification misses beginning"; + if (classifications.size() == 1) + return; + + // The classifications should always be sorted. + size_t last_offset = classifications[0].offset; + for (ACMatchClassifications::const_iterator i(classifications.begin() + 1); + i != classifications.end(); ++i) { + DCHECK(i->offset > last_offset) << "Classification unsorted"; + DCHECK(i->offset < text.length()) << "Classification out of bounds"; + last_offset = i->offset; + } +} +#endif + +// AutocompleteProvider ------------------------------------------------------- + +// static +size_t AutocompleteProvider::max_matches_ = 3; + +AutocompleteProvider::~AutocompleteProvider() { + Stop(); +} + +void AutocompleteProvider::SetProfile(Profile* profile) { + DCHECK(profile); + Stop(); // It makes no sense to continue running a query from an old profile. + profile_ = profile; +} + +std::wstring AutocompleteProvider::StringForURLDisplay( + const GURL& url, + bool check_accept_lang) { + return gfx::ElideUrl(url, ChromeFont(), 0, check_accept_lang && profile_ ? + profile_->GetPrefs()->GetString(prefs::kAcceptLanguages) : + std::wstring()); +} + +// AutocompleteResult --------------------------------------------------------- + +// static +size_t AutocompleteResult::max_matches_ = 6; + +void AutocompleteResult::Selection::Clear() { + destination_url.clear(); + provider_affinity = NULL; + is_history_what_you_typed_match = false; +} + +AutocompleteResult::AutocompleteResult() { + // Reserve space for the max number of matches we'll show. The +1 accounts + // for the history shortcut match as it isn't included in max_matches. + matches_.reserve(max_matches() + 1); + + // It's probably safe to do this in the initializer list, but there's little + // penalty to doing it here and it ensures our object is fully constructed + // before calling member functions. + default_match_ = end(); +} + +void AutocompleteResult::CopyFrom(const AutocompleteResult& rhs) { + if (this == &rhs) + return; + + matches_ = rhs.matches_; + // Careful! You can't just copy iterators from another container, you have to + // reconstruct them. + default_match_ = (rhs.default_match_ == rhs.end()) ? + end() : (begin() + (rhs.default_match_ - rhs.begin())); +} + +void AutocompleteResult::AppendMatches(const ACMatches& matches) { + default_match_ = end(); + std::copy(matches.begin(), matches.end(), std::back_inserter(matches_)); +} + +void AutocompleteResult::AddMatch(const AutocompleteMatch& match) { + default_match_ = end(); + matches_.insert(std::upper_bound(matches_.begin(), matches_.end(), match, + &AutocompleteMatch::MoreRelevant), match); +} + +void AutocompleteResult::SortAndCull() { + default_match_ = end(); + + // Remove duplicates. + std::sort(matches_.begin(), matches_.end(), + &AutocompleteMatch::DestinationSortFunc); + matches_.erase(std::unique(matches_.begin(), matches_.end(), + &AutocompleteMatch::DestinationsEqual), + matches_.end()); + + // Find the top max_matches. + if (matches_.size() > max_matches()) { + std::partial_sort(matches_.begin(), matches_.begin() + max_matches(), + matches_.end(), &AutocompleteMatch::MoreRelevant); + matches_.resize(max_matches()); + } + + // HistoryContentsProvider use a negative relevance as a way to avoid + // starving out other provider results, yet we may end up using the result. To + // make sure such results are sorted correctly we search for all + // relevances < 0 and negate them. If we change our relevance algorithm to + // properly mix different providers results, this can go away. + for (ACMatches::iterator i = matches_.begin(); i != matches_.end(); ++i) { + if (i->relevance < 0) + i->relevance = -i->relevance; + } + + // Now put the final result set in order. + std::sort(matches_.begin(), matches_.end(), &AutocompleteMatch::MoreRelevant); +} + +bool AutocompleteResult::SetDefaultMatch(const Selection& selection) { + default_match_ = end(); + + // Look for the best match. + for (const_iterator i(begin()); i != end(); ++i) { + // If we have an exact match, return immediately. + if (selection.is_history_what_you_typed_match ? + i->is_history_what_you_typed_match : + (!selection.destination_url.empty() && + (i->destination_url == selection.destination_url))) { + default_match_ = i; + return true; + } + + // Otherwise, see if this match is closer to the desired selection than the + // existing one. + if (default_match_ == end()) { + // No match at all yet, pick the first one we see. + default_match_ = i; + } else if (selection.provider_affinity == NULL) { + // No provider desired, choose solely based on relevance. + if (AutocompleteMatch::MoreRelevant(*i, *default_match_)) + default_match_ = i; + } else { + // Desired provider trumps any undesired provider; otherwise choose based + // on relevance. + const bool providers_match = + (i->provider == selection.provider_affinity); + const bool default_provider_doesnt_match = + (default_match_->provider != selection.provider_affinity); + if ((providers_match && default_provider_doesnt_match) || + ((providers_match || default_provider_doesnt_match) && + AutocompleteMatch::MoreRelevant(*i, *default_match_))) + default_match_ = i; + } + } + return false; +} + +std::wstring AutocompleteResult::GetAlternateNavURL( + const AutocompleteInput& input, + const_iterator match) const { + if (((input.type() == AutocompleteInput::UNKNOWN) || + (input.type() == AutocompleteInput::REQUESTED_URL)) && + (match->transition != PageTransition::TYPED)) { + for (const_iterator i(begin()); i != end(); ++i) { + if (i->is_history_what_you_typed_match) { + return (i->destination_url == match->destination_url) ? + std::wstring() : i->destination_url; + } + } + } + return std::wstring(); +} + +#ifndef NDEBUG +void AutocompleteResult::Validate() const { + for (const_iterator i(begin()); i != end(); ++i) + i->Validate(); +} +#endif + +// AutocompleteController ----------------------------------------------------- + +const int AutocompleteController::kNoItemSelected = -1; + +AutocompleteController::AutocompleteController(ACControllerListener* listener, + Profile* profile) + : listener_(listener) { + providers_.push_back(new SearchProvider(this, profile)); + providers_.push_back(new HistoryURLProvider(this, profile)); + keyword_provider_ = new KeywordProvider(this, profile); + providers_.push_back(keyword_provider_); + if (listener) { + // These providers are async-only, so there's no need to create them when + // we'll only be doing synchronous queries. + history_contents_provider_ = new HistoryContentsProvider(this, profile); + providers_.push_back(history_contents_provider_); + } else { + history_contents_provider_ = NULL; + } + for (ACProviders::iterator i(providers_.begin()); i != providers_.end(); ++i) + (*i)->AddRef(); +} + +AutocompleteController::~AutocompleteController() { + for (ACProviders::iterator i(providers_.begin()); i != providers_.end(); ++i) + (*i)->Release(); + + providers_.clear(); // Not really necessary. +} + +void AutocompleteController::SetProfile(Profile* profile) { + for (ACProviders::iterator i(providers_.begin()); i != providers_.end(); ++i) + (*i)->SetProfile(profile); +} + +bool AutocompleteController::Start(const AutocompleteInput& input, + bool minimal_changes, + bool synchronous_only) { + input_ = input; + for (ACProviders::iterator i(providers_.begin()); i != providers_.end(); + ++i) { + (*i)->Start(input, minimal_changes, synchronous_only); + if (synchronous_only) + DCHECK((*i)->done()); + } + + return QueryComplete(); +} + +void AutocompleteController::Stop() const { + for (ACProviders::const_iterator i(providers_.begin()); + i != providers_.end(); ++i) { + if (!(*i)->done()) + (*i)->Stop(); + } +} + +void AutocompleteController::OnProviderUpdate(bool updated_matches) { + // Notify listener when something has changed. + if (!listener_) { + NOTREACHED(); // This should never be called for synchronous queries, and + // since |listener_| is NULL, the owner of the controller + // should only be running synchronous queries. + return; // But, this isn't fatal, so don't crash. + } + const bool query_complete = QueryComplete(); + if (updated_matches || query_complete) + listener_->OnAutocompleteUpdate(updated_matches, query_complete); +} + +void AutocompleteController::GetResult(AutocompleteResult* result) { + // Add all providers' results. + result->Reset(); + for (ACProviders::const_iterator i(providers_.begin()); + i != providers_.end(); ++i) + result->AppendMatches((*i)->matches()); + + // Sort the matches and trim to a small number of "best" matches. + result->SortAndCull(); + + if (history_contents_provider_) + AddHistoryContentsShortcut(result); + +#ifndef NDEBUG + result->Validate(); +#endif +} + +bool AutocompleteController::QueryComplete() const { + for (ACProviders::const_iterator i(providers_.begin()); + i != providers_.end(); ++i) { + if (!(*i)->done()) + return false; + } + + return true; +} + +size_t AutocompleteController::CountMatchesNotInResult( + const AutocompleteProvider* provider, + const AutocompleteResult* result, + AutocompleteMatch* first_match) { + DCHECK(provider && result && first_match); + + // Determine the set of destination URLs. + std::set<std::wstring> destination_urls; + for (AutocompleteResult::const_iterator i(result->begin()); + i != result->end(); ++i) + destination_urls.insert(i->destination_url); + + const ACMatches& provider_matches = provider->matches(); + bool found_first_unique_match = false; + size_t showing_count = 0; + for (ACMatches::const_iterator i = provider_matches.begin(); + i != provider_matches.end(); ++i) { + if (destination_urls.find(i->destination_url) != destination_urls.end()) { + showing_count++; + } else if (!found_first_unique_match) { + found_first_unique_match = true; + *first_match = *i; + } + } + return provider_matches.size() - showing_count; +} + +void AutocompleteController::AddHistoryContentsShortcut( + AutocompleteResult* result) { + DCHECK(result && history_contents_provider_); + // Only check the history contents provider if the history contents provider + // is done and has matches. + if (!history_contents_provider_->done() || + !history_contents_provider_->db_match_count()) { + return; + } + + if ((history_contents_provider_->db_match_count() <= result->size() + 1) || + history_contents_provider_->db_match_count() == 1) { + // We only want to add a shortcut if we're not already showing the matches. + AutocompleteMatch first_unique_match; + size_t matches_not_shown = CountMatchesNotInResult( + history_contents_provider_, result, &first_unique_match); + if (matches_not_shown == 0) + return; + if (matches_not_shown == 1) { + // Only one match not shown, add it. The relevance may be negative, + // which means we need to negate it to get the true relevance. + if (first_unique_match.relevance < 0) + first_unique_match.relevance = -first_unique_match.relevance; + result->AddMatch(first_unique_match); + return; + } // else, fall through and add item. + } + + AutocompleteMatch match(NULL, 0, false); + match.type = AutocompleteMatch::HISTORY_SEARCH; + match.fill_into_edit = input_.text(); + + // Mark up the text such that the user input text is bold. + size_t keyword_offset = std::wstring::npos; // Offset into match.contents. + if (history_contents_provider_->db_match_count() == + history_contents_provider_->kMaxMatchCount) { + // History contents searcher has maxed out. + match.contents = l10n_util::GetStringF(IDS_OMNIBOX_RECENT_HISTORY_MANY, + input_.text(), + &keyword_offset); + } else { + // We can report exact matches when there aren't too many. + std::vector<size_t> content_param_offsets; + match.contents = + l10n_util::GetStringF(IDS_OMNIBOX_RECENT_HISTORY, + FormatNumber(history_contents_provider_-> + db_match_count()), + input_.text(), + &content_param_offsets); + + // content_param_offsets is ordered based on supplied params, we expect + // that the second one contains the query (first is the number). + if (content_param_offsets.size() == 2) { + keyword_offset = content_param_offsets[1]; + } else { + // See comments on an identical NOTREACHED() in search_provider.cc. + NOTREACHED(); + } + } + + // NOTE: This comparison succeeds when keyword_offset == std::wstring::npos. + if (keyword_offset > 0) { + match.contents_class.push_back( + ACMatchClassification(0, ACMatchClassification::NONE)); + } + match.contents_class.push_back( + ACMatchClassification(keyword_offset, ACMatchClassification::MATCH)); + if (keyword_offset + input_.text().size() < match.contents.size()) { + match.contents_class.push_back( + ACMatchClassification(keyword_offset + input_.text().size(), + ACMatchClassification::NONE)); + } + match.destination_url = + UTF8ToWide(HistoryTabUI::GetHistoryURLWithSearchText( + input_.text()).spec()); + match.transition = PageTransition::AUTO_BOOKMARK; + match.provider = history_contents_provider_; + result->AddMatch(match); +} diff --git a/chrome/browser/autocomplete/autocomplete.h b/chrome/browser/autocomplete/autocomplete.h new file mode 100644 index 0000000..cdcc0d1 --- /dev/null +++ b/chrome/browser/autocomplete/autocomplete.h @@ -0,0 +1,800 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#ifndef CHROME_BROWSER_AUTOCOMPLETE_AUTOCOMPLETE_H__ +#define CHROME_BROWSER_AUTOCOMPLETE_AUTOCOMPLETE_H__ + +#include <map> +#include <string> +#include <vector> +#include "base/logging.h" +#include "base/ref_counted.h" +#include "chrome/common/page_transition_types.h" +#include "googleurl/src/url_parse.h" + +// The AutocompleteController is the center of the autocomplete system. A +// class implementing AutocompleteController::Listener creates an instance of +// the controller, which in turn creates a set of AutocompleteProviders to +// serve it. The listener can ask the controller to Start() a query; the +// controller in turn passes this call down to the providers, each of which +// keeps track of its own results and whether it has finished processing the +// query. When a provider gets more results or finishes processing, it +// notifies the controller, which merges the combined results together and +// returns them to the listener. +// +// The listener may also cancel the current query by calling Stop(), which the +// controller will in turn communicate to all the providers. No callbacks will +// happen after a request has been stopped. +// +// IMPORTANT: There is NO THREAD SAFETY built into this portion of the +// autocomplete system. All calls to and from the AutocompleteController should +// happen on the same thread. AutocompleteProviders are responsible for doing +// their own thread management when they need to return results asynchronously. +// +// The AutocompleteProviders each return one kind of results, such as history +// results or search results. These results are given "relevance" scores. +// Historically the relevance for each column added up to 100, then scores +// were from 1-100. Both have proved a bit painful, and will be changed going +// forward. The important part is that higher relevance scores are more +// important than lower relevance scores. The relevance scores and class +// providing the result are as follows: +// +// UNKNOWN input type: +// --------------------------------------------------------------------|----- +// Keyword (non-substituting, exact match) | 1500 +// HistoryURL (exact or inline autocomplete match) | 1400 +// Search (what you typed) | 1300 +// HistoryURL (what you typed) | 1200 +// Keyword (substituting, exact match) | 1100 +// Search (past query in history) | 1050-- +// HistoryContents (any match in title of starred page) | 1000++ +// HistoryURL (inexact match) | 900++ +// Search (navigational suggestion) | 800++ +// HistoryContents (any match in title of nonstarred page) | 700++ +// Search (suggestion) | 600++ +// HistoryContents (any match in body of starred page) | 550++ +// HistoryContents (any match in body of nonstarred page) | 500++ +// Keyword (inexact match) | 450 +// +// REQUESTED_URL input type: +// --------------------------------------------------------------------|----- +// Keyword (non-substituting, exact match) | 1500 +// HistoryURL (exact or inline autocomplete match) | 1400 +// HistoryURL (what you typed) | 1300 +// Search (what you typed) | 1200 +// Keyword (substituting, exact match) | 1100 +// Search (past query in history) | 1050-- +// HistoryContents (any match in title of starred page) | 1000++ +// HistoryURL (inexact match) | 900++ +// Search (navigational suggestion) | 800++ +// HistoryContents (any match in title of nonstarred page) | 700++ +// Search (suggestion) | 600++ +// HistoryContents (any match in body of starred page) | 550++ +// HistoryContents (any match in body of nonstarred page) | 500++ +// Keyword (inexact match) | 450 +// +// URL input type: +// --------------------------------------------------------------------|----- +// Keyword (non-substituting, exact match) | 1500 +// HistoryURL (exact or inline autocomplete match) | 1400 +// HistoryURL (what you typed) | 1200 +// Keyword (substituting, exact match) | 1100 +// HistoryURL (inexact match) | 900++ +// Search (what you typed) | 850 +// Search (navigational suggestion) | 800++ +// Search (past query in history) | 750-- +// Keyword (inexact match) | 700 +// Search (suggestion) | 300++ +// +// QUERY input type: +// --------------------------------------------------------------------|----- +// Keyword (non-substituting, exact match) | 1500 +// Keyword (substituting, exact match) | 1400 +// Search (what you typed) | 1300 +// Search (past query in history) | 1250-- +// HistoryContents (any match in title of starred page) | 1200++ +// Search (navigational suggestion) | 1000++ +// HistoryContents (any match in title of nonstarred page) | 900++ +// Search (suggestion) | 800++ +// HistoryContents (any match in body of starred page) | 750++ +// HistoryContents (any match in body of nonstarred page) | 700++ +// Keyword (inexact match) | 650 +// +// FORCED_QUERY input type: +// --------------------------------------------------------------------|----- +// Search (what you typed) | 1500 +// Search (past query in history) | 1250-- +// HistoryContents (any match in title of starred page) | 1200++ +// Search (navigational suggestion) | 1000++ +// HistoryContents (any match in title of nonstarred page) | 900++ +// Search (suggestion) | 800++ +// HistoryContents (any match in body of starred page) | 750++ +// HistoryContents (any match in body of nonstarred page) | 700++ +// +// (A search keyword is a keyword with a replacement string; a bookmark keyword +// is a keyword with no replacement string, that is, a shortcut for a URL.) +// +// The value column gives the ranking returned from the various providers. +// ++: a series of results with relevance from n up to (n + max_matches). +// --: relevance score falls off over time (discounted 50 points @ 15 minutes, +// 450 points @ two weeks) + +class AutocompleteInput; +struct AutocompleteMatch; +class AutocompleteProvider; +class AutocompleteResult; +class AutocompleteController; +class GURL; +class HistoryContentsProvider; +class KeywordProvider; +class Profile; +class TemplateURL; + +typedef std::vector<AutocompleteMatch> ACMatches; +typedef std::vector<AutocompleteProvider*> ACProviders; + +// AutocompleteInput ---------------------------------------------------------- + +// The user input for an autocomplete query. Allows copying. +class AutocompleteInput { + public: + enum Type { + INVALID, // Empty input + UNKNOWN, // Valid input whose type cannot be determined + REQUESTED_URL, // Input autodetected as UNKNOWN, which the user wants to + // treat as an URL by specifying a desired_tld + URL, // Input autodetected as a URL + QUERY, // Input autodetected as a query + FORCED_QUERY, // Input forced to be a query by an initial '?' + }; + + AutocompleteInput() : type_(INVALID), prevent_inline_autocomplete_(false) {} + AutocompleteInput(const std::wstring& text, + const std::wstring& desired_tld, + bool prevent_inline_autocomplete); + + // Parses |text| and returns the type of input this will be interpreted as. + // The components of the input are stored in the output parameter |parts|. + static Type Parse(const std::wstring& text, + const std::wstring& desired_tld, + url_parse::Parsed* parts, + std::wstring* scheme); + + // User-provided text to be completed. + const std::wstring& text() const { return text_; } + + // Use of this setter is risky, since no other internal state is updated + // besides |text_|. Only callers who know that they're not changing the + // type/scheme/etc. should use this. + void set_text(const std::wstring& text) { text_ = text; } + + // The type of input supplied. + Type type() const { return type_; } + + // The scheme parsed from the provided text; only meaningful when type_ is + // URL. + const std::wstring& scheme() const { return scheme_; } + + // User's desired TLD, if one is not already present in the text to + // autocomplete. When this is non-empty, it also implies that "www." should + // be prepended to the domain where possible. This should not have a leading + // '.' (use "com" instead of ".com"). + const std::wstring& desired_tld() const { return desired_tld_; } + + // Returns whether inline autocompletion should be prevented. + const bool prevent_inline_autocomplete() const { + return prevent_inline_autocomplete_; + } + + // operator==() by another name. + bool Equals(const AutocompleteInput& other) const; + + // Resets all internal variables to the null-constructed state. + void Clear(); + + private: + std::wstring text_; + Type type_; + std::wstring scheme_; + std::wstring desired_tld_; + bool prevent_inline_autocomplete_; +}; + +// AutocompleteMatch ---------------------------------------------------------- + +// A single result line with classified spans. The autocomplete popup displays +// the 'contents' and the 'description' (the description is optional) in the +// autocomplete dropdown, and fills in 'fill_into_edit' into the textbox when +// that line is selected. fill_into_edit may be the same as 'description' for +// things like URLs, but may be different for searches or other providers. For +// example, a search result may say "Search for asdf" as the description, but +// "asdf" should appear in the box. +struct AutocompleteMatch { + // Autocomple results return strings that are classified according to a + // separate vector of styles. This vector must be sorted, and associates + // flags with portions of the strings. It is required that all text be + // inside a classification range. Even if you have no classification, you + // should create an entry at offset 0 with no flags. + // + // Example: The user typed "goog" + // http://www.google.com/ Google + // ^ ^ ^ ^ ^ + // 0, | 15, | 4, + // 11,match 0,match + // + // This structure holds the classifiction information for each span. + struct ACMatchClassification { + // The values in here are not mutually exclusive -- use them like a + // bitfield. This also means we use "int" instead of this enum type when + // passing the values around, so the compiler doesn't complain. + enum Style { + NONE = 0, + URL = 1 << 0, // A URL + MATCH = 1 << 1, // A match for the user's search term + DIM = 1 << 2, // "Helper text" + }; + + ACMatchClassification(size_t offset, int style) + : offset(offset), + style(style) { + } + + // Offset within the string that this classification starts + size_t offset; + + int style; + }; + + typedef std::vector<ACMatchClassification> ACMatchClassifications; + + // The type of this match. + // URL: a url, typically one the user previously entered but it may have + // also been suggested. This is the default. + // KEYWORD: a keyword. + // SEARCH: short cut for typing type into the Google homepage. This should + // only be used if the full URL is not shown. + enum Type { + // Something that looks like a URL ("http://foo.com", "internal-server/"). + // This is the default. + URL, + + // A manually created or auto-generated keyword, with or without a query + // component. Auto-generated keywords may look similar to urls. See + // keyword_autocomplete.cc. + KEYWORD, + + // A search term or phrase for the user's default search provider + // ("games", "foo"). These visually look similar to keywords. See + // google_autocomplete.cc. + SEARCH, + + // Shortcut that takes the user to destinations->history. + HISTORY_SEARCH + }; + + AutocompleteMatch(); + AutocompleteMatch(AutocompleteProvider* provider, + int relevance, + bool deletable); + + // Comparison function for determining when one match is better than another. + static bool MoreRelevant(const AutocompleteMatch& elem1, + const AutocompleteMatch& elem2); + + // Comparison functions for removing matches with duplicate destinations. + static bool DestinationSortFunc(const AutocompleteMatch& elem1, + const AutocompleteMatch& elem2); + static bool DestinationsEqual(const AutocompleteMatch& elem1, + const AutocompleteMatch& elem2); + + // Helper functions for classes creating matches: + // Fills in the classifications for |text|, using |style| as the base style + // and marking the first instance of |find_text| as a match. (This match + // will also not be dimmed, if |style| has DIM set.) + static void ClassifyMatchInString(const std::wstring& find_text, + const std::wstring& text, + int style, + ACMatchClassifications* classifications); + + // Similar to ClassifyMatchInString(), but for cases where the range to mark + // as matching is already known (avoids calling find()). This can be helpful + // when find() would be misleading (e.g. you want to mark the second match in + // a string instead of the first). + static void ClassifyLocationInString(size_t match_location, + size_t match_length, + size_t overall_length, + int style, + ACMatchClassifications* classifications); + + // The provider of this match, used to remember which provider the user had + // selected when the input changes. This may be NULL, in which case there is + // no provider (or memory of the user's selection). + AutocompleteProvider* provider; + + // The relevance of this match. See table above for scores returned by + // various providers. This is used to rank matches among all responding + // providers, so different providers must be carefully tuned to supply + // matches with appropriate relevance. + // + // If the relevance is negative, it will only be displayed if there are not + // enough non-negative items in all the providers to max out the popup. In + // this case, the relevance of the additional items will be inverted so they + // can be mixed in with the rest of the relevances. This allows a provider + // to group its results, having the added items appear intermixed with its + // other results. + // + // TODO(pkasting): http://b/1111299 This should be calculated algorithmically, + // rather than being a fairly fixed value defined by the table above. + int relevance; + + // True if the user should be able to delete this match. + bool deletable; + + // This string is loaded into the location bar when the item is selected + // by pressing the arrow keys. This may be different than a URL, for example, + // for search suggestions, this would just be the search terms. + std::wstring fill_into_edit; + + // The position within fill_into_edit from which we'll display the inline + // autocomplete string. This will be std::wstring::npos if this match should + // not be inline autocompleted. + size_t inline_autocomplete_offset; + + // The URL to actually load when the autocomplete item is selected. This URL + // should be canonical so we can compare URLs with strcmp to avoid dupes. + // It may be empty if there is no possible navigation. + std::wstring destination_url; + + // The text displayed on the left in the search results + std::wstring contents; + ACMatchClassifications contents_class; + + // Displayed to the right of the result as the title or other helper info + std::wstring description; + ACMatchClassifications description_class; + + // The transition type to use when the user opens this match. By default + // this is TYPED. Providers whose matches do not look like URLs should set + // it to GENERATED. + PageTransition::Type transition; + + // True when this match is the "what you typed" match from the history + // system. + bool is_history_what_you_typed_match; + + // Type of this match. + Type type; + + // If this match corresponds to a keyword, this is the TemplateURL the + // keyword was obtained from. + const TemplateURL* template_url; + + // True if the user has starred the destination URL. + bool starred; + +#ifndef NDEBUG + // Does a data integrity check on this match. + void Validate() const; + + // Checks one text/classifications pair for valid values. + void ValidateClassifications( + const std::wstring& text, + const ACMatchClassifications& classifications) const; +#endif +}; + +typedef AutocompleteMatch::ACMatchClassification ACMatchClassification; +typedef std::vector<ACMatchClassification> ACMatchClassifications; + +// AutocompleteProvider ------------------------------------------------------- + +// A single result provider for the autocomplete system. Given user input, the +// provider decides what (if any) matches to return, their relevance, and their +// classifications. +class AutocompleteProvider + : public base::RefCountedThreadSafe<AutocompleteProvider> { + public: + class ACProviderListener { + public: + // Called by a provider as a notification that something has changed. + // |updated_matches| should be true iff the matches have changed in some + // way (they may not have changed if, for example, the provider did an + // asynchronous query to get more results, came up with none, and is now + // giving up). + // + // NOTE: Providers MUST only call this method while processing asynchronous + // queries. Do not call this for a synchronous query. + // + // NOTE: There's no parameter to tell the listener _which_ provider is + // calling it. Because the AutocompleteController (the typical listener) + // doesn't cache the providers' individual results locally, it has to get + // them all again when this is called anyway, so such a parameter wouldn't + // actually be useful. + virtual void OnProviderUpdate(bool updated_matches) = 0; + }; + + AutocompleteProvider(ACProviderListener* listener, + Profile* profile, + char* name) + : listener_(listener), + profile_(profile), + done_(true), + name_(name) { + } + + virtual ~AutocompleteProvider(); + + // Invoked when the profile changes. + void SetProfile(Profile* profile); + + // Called to start an autocomplete query. The provider is responsible for + // tracking its results for this query and whether it is done processing the + // query. When new results are available or the provider finishes, it + // calls the controller's OnProviderUpdate() method. The controller can then + // get the new results using the provider's accessors. + // Exception: Results available immediately after starting the query (that + // is, synchronously) do not cause any notifications to be sent. The + // controller is expected to check for these without prompting (since + // otherwise, starting each provider running would result in a flurry of + // notifications). + // + // Once Stop() has been called, no more notifications should be sent. + // + // |minimal_changes| is an optimization that lets the provider do less work + // when the |input|'s text hasn't changed. See the body of + // AutocompletePopup::StartAutocomplete(). + // + // If |synchronous_only| is true, no asynchronous work should be scheduled; + // the provider should stop after it has returned all the + // synchronously-available results. This also means any in-progress + // asynchronous work should be canceled, so the provider does not call back at + // a later time. + virtual void Start(const AutocompleteInput& input, + bool minimal_changes, + bool synchronous_only) = 0; + + // Called when a provider must not make any more callbacks for the current + // query. + virtual void Stop() { + done_ = true; + } + + // Returns the set of matches for the current query. + const ACMatches& matches() const { return matches_; } + + // Returns whether the provider is done processing the query. + bool done() const { return done_; } + + // Returns the name of this provider. + const char* name() const { return name_; } + + // Called to delete a match and the backing data that produced it. This + // match should not appear again in this or future queries. This can only be + // called for matches the provider marks as deletable. + // NOTE: Remember to call OnProviderUpdate() if matches_ is updated. + virtual void DeleteMatch(const AutocompleteMatch& match) {} + + static void set_max_matches(size_t max_matches) { + max_matches_ = max_matches; + } + + static size_t max_matches() { return max_matches_; } + + protected: + // The profile associated with the AutocompleteProvider. Reference is not + // owned by us. + Profile* profile_; + + ACProviderListener* listener_; + ACMatches matches_; + bool done_; + + // The name of this provider. Used for logging. + const char* name_; + + // A convenience function to call gfx::ElideUrl() with the current set of + // "Accept Languages" when check_accept_lang is true. Otherwise, it's called + // with an empty list. + std::wstring StringForURLDisplay(const GURL& url, bool check_accept_lang); + + private: + // A suggested upper bound for how many matches a provider should return. + // TODO(pkasting): http://b/1111299 , http://b/933133 This should go away once + // we have good relevance heuristics; the controller should handle all + // culling. + static size_t max_matches_; + + DISALLOW_EVIL_CONSTRUCTORS(AutocompleteProvider); +}; + +typedef AutocompleteProvider::ACProviderListener ACProviderListener; + +// AutocompleteResult --------------------------------------------------------- + +// All matches from all providers for a particular query. This also tracks +// what the default match should be if the user doesn't manually select another +// match. +class AutocompleteResult { + public: + typedef ACMatches::const_iterator const_iterator; + typedef ACMatches::iterator iterator; + + // The "Selection" struct is the information we need to select the same match + // in one result set that was selected in another. + struct Selection { + Selection() + : provider_affinity(NULL), + is_history_what_you_typed_match(false) { + } + + // Clear the selection entirely. + void Clear(); + + // True when the selection is empty. + bool empty() const { + return destination_url.empty() && !provider_affinity && + !is_history_what_you_typed_match; + } + + // The desired destination URL. + std::wstring destination_url; + + // The desired provider. If we can't find a match with the specified + // |destination_url|, we'll use the best match from this provider. + const AutocompleteProvider* provider_affinity; + + // True when this is the HistoryURLProvider's "what you typed" match. This + // can't be tracked using |destination_url| because its URL changes on every + // keystroke, so if this is set, we'll preserve the selection by simply + // choosing the new "what you typed" entry and ignoring |destination_url|. + bool is_history_what_you_typed_match; + }; + + static void set_max_matches(size_t max_matches) { + max_matches_ = max_matches; + } + static size_t max_matches() { return max_matches_; } + + AutocompleteResult(); + + // operator=() by another name. + void CopyFrom(const AutocompleteResult& rhs); + + // Adds a single match. The match is inserted at the appropriate position + // based on relevancy and display order. This is ONLY for use after + // SortAndCull has been invoked. + void AddMatch(const AutocompleteMatch& match); + + // Adds a new set of matches to the set of results. + void AppendMatches(const ACMatches& matches); + + // Removes duplicates, puts the list in sorted order and culls to leave only + // the best kMaxMatches results. + void SortAndCull(); + + // Vector-style accessors/operators. + size_t size() const { return matches_.size(); } + bool empty() const { return matches_.empty(); } + const_iterator begin() const { return matches_.begin(); } + iterator begin() { return matches_.begin(); } + const_iterator end() const { return matches_.end(); } + iterator end() { return matches_.end(); } + + // Returns the match at the given index. + const AutocompleteMatch& match_at(size_t index) const { + DCHECK(index < matches_.size()); + return matches_[index]; + } + + // Get the default match for the query (not necessarily the first). Returns + // end() if there is no default match. + const_iterator default_match() const { return default_match_; } + + // Sets the default match to the best result. When a particular URL is + // desired, we return that if available; otherwise, if a provider affinity is + // specified, we pick the most relevant match from that provider; otherwise, + // we return the best match overall. + // Returns true if the selection specified an exact match and we were able to + // find and use it. + bool SetDefaultMatch(const Selection& selection); + + // Given some input and a particular match in this result set, returns the + // "alternate navigation URL", if any, for that match. This is a URL to try + // offering as a navigational option in case the user didn't actually mean to + // navigate to the URL of |match|. For example, if the user's local intranet + // contains site "foo", and the user types "foo", we default to searching for + // "foo" when the user may have meant to navigate there. In cases like this, + // |match| will point to the "search for 'foo'" result, and this function will + // return "http://foo/". + std::wstring GetAlternateNavURL(const AutocompleteInput& input, + const_iterator match) const; + + // Releases the resources associated with this object. Some callers may + // want to perform several searches without creating new results each time. + // They can call this function to re-use the result for another query. + void Reset() { + matches_.clear(); + default_match_ = end(); + } + +#ifndef NDEBUG + // Does a data integrity check on this result. + void Validate() const; +#endif + + private: + // Max number of matches we'll show from the various providers. We may end + // up showing an additional shortcut for Destinations->History, see + // AddHistoryContentsShortcut. + static size_t max_matches_; + ACMatches matches_; + const_iterator default_match_; + + DISALLOW_EVIL_CONSTRUCTORS(AutocompleteResult); +}; + +// AutocompleteController ----------------------------------------------------- + +// The coordinator for autocomplete queries, responsible for combining the +// results from a series of providers into one AutocompleteResult and +// interacting with the Listener that owns it. +class AutocompleteController : public ACProviderListener { + public: + class ACControllerListener { + public: + // Called by the controller when new results are available and/or the query + // is complete. The listener can then call GetResult() and provide an + // AutocompleteResult* to be filled in. + // + // Note that this function is never called for synchronous_only queries + // (see Start()). If you're only using those, you can create the controller + // with a NULL listener. + virtual void OnAutocompleteUpdate(bool updated_result, + bool query_complete) = 0; + }; + + // Used to indicate an index that is not selected in a call to Update() + // and for merging results. + static const int kNoItemSelected; + + // Normally, you will call the first constructor. Unit tests can use the + // second to set the providers to some known testing providers. The default + // providers will be overridden and the controller will take ownership of the + // providers, Release()ing them on destruction. + // + // It is safe to pass NULL for |listener| iff you only ever use synchronous + // queries. + AutocompleteController(ACControllerListener* listener, Profile* profile); +#ifdef UNIT_TEST + AutocompleteController(ACControllerListener* listener, + const ACProviders& providers) + : listener_(listener), + providers_(providers), + keyword_provider_(NULL), + history_contents_provider_(NULL) { + } +#endif + ~AutocompleteController(); + + // Invoked when the profile changes. This forwards the call down to all + // the AutocompleteProviders. + void SetProfile(Profile* profile); + + // Starts an autocomplete query, which continues until all providers are + // done or the query is Stop()ed. It is safe to Start() a new query without + // Stop()ing the previous one. + // + // If |minimal_changes| is true, |input| is the same as in the previous + // query, except for a different desired_tld_ and possibly type_. Most + // providers should just be able to recalculate priorities in this case and + // return synchronously, or at least faster than otherwise. + // + // If |synchronous_only| is true, the controller asks the providers to only + // return results which are synchronously available, which should mean that + // all providers will be done immediately. + // + // The controller does not notify the listener about any results available + // immediately; the caller should call GetResult() manually if it wants + // these. The return value is whether the query is complete; if it is + // false, then the controller will call OnAutocompleteUpdate() with future + // result updates (unless the query is Stop()ed). + bool Start(const AutocompleteInput& input, + bool minimal_changes, + bool synchronous_only); + + // Cancels the current query, ensuring there will be no future callbacks to + // OnAutocompleteUpdate() (until Start() is called again). + void Stop() const; + + // Called by the listener to get the current results of the query. + void GetResult(AutocompleteResult* result); + + // From AutocompleteProvider::Listener + virtual void OnProviderUpdate(bool updated_matches); + + KeywordProvider* keyword_provider() const { return keyword_provider_; } + + private: + // Returns true if all providers have finished processing the query. + bool QueryComplete() const; + + // Returns the number of matches from provider whose destination urls are + // not in result. first_match is set to the first match whose destination url + // is NOT in result. + size_t CountMatchesNotInResult(const AutocompleteProvider* provider, + const AutocompleteResult* result, + AutocompleteMatch* first_match); + + // If the HistoryContentsAutocomplete provider is done and there are more + // matches in the database than currently shown, an entry is added to + // result to show all history matches. + void AddHistoryContentsShortcut(AutocompleteResult* result); + + ACControllerListener* listener_; // May be NULL. + + // A list of all providers. + ACProviders providers_; + + KeywordProvider* keyword_provider_; + + HistoryContentsProvider* history_contents_provider_; + + // Input passed to Start. + AutocompleteInput input_; + + DISALLOW_EVIL_CONSTRUCTORS(AutocompleteController); +}; + +typedef AutocompleteController::ACControllerListener ACControllerListener; + +// AutocompleteLog ------------------------------------------------------------ + +// The data to log (via the metrics service) when the user selects an item +// from the omnibox popup. +struct AutocompleteLog { + AutocompleteLog(std::wstring text, + size_t selected_index, + size_t inline_autocompleted_length, + const AutocompleteResult& result) + : text(text), + selected_index(selected_index), + inline_autocompleted_length(inline_autocompleted_length), + result(result) { + } + // The user's input text in the omnibox. + std::wstring text; + // Selected index (if selected) or -1 (AutocompletePopup::kNoMatch). + size_t selected_index; + // Inline autocompleted length (if displayed). + size_t inline_autocompleted_length; + // Result set. + const AutocompleteResult& result; +}; + +#endif // CHROME_BROWSER_AUTOCOMPLETE_AUTOCOMPLETE_H__ diff --git a/chrome/browser/autocomplete/autocomplete_edit.cc b/chrome/browser/autocomplete/autocomplete_edit.cc new file mode 100644 index 0000000..5cb875d --- /dev/null +++ b/chrome/browser/autocomplete/autocomplete_edit.cc @@ -0,0 +1,2333 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "chrome/browser/autocomplete/autocomplete_edit.h" + +#include <locale> + +#include "base/base_drag_source.h" +#include "base/clipboard_util.h" +#include "base/gfx/skia_utils.h" +#include "base/ref_counted.h" +#include "base/string_util.h" +#include "chrome/app/chrome_dll_resource.h" +#include "chrome/browser/autocomplete/edit_drop_target.h" +#include "chrome/browser/autocomplete/keyword_provider.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/controller.h" +#include "chrome/browser/drag_utils.h" +#include "chrome/browser/profile.h" +#include "chrome/browser/tab_contents.h" +#include "chrome/browser/template_url.h" +#include "chrome/browser/template_url_model.h" +#include "chrome/browser/url_fixer_upper.h" +#include "chrome/browser/user_metrics.h" +#include "chrome/browser/views/location_bar_view.h" +#include "chrome/common/clipboard_service.h" +#include "chrome/common/gfx/chrome_canvas.h" +#include "chrome/common/gfx/utils.h" +#include "chrome/common/l10n_util.h" +#include "chrome/common/os_exchange_data.h" +#include "chrome/common/win_util.h" +#include "chrome/views/accessibility/autocomplete_accessibility.h" +#include "googleurl/src/url_util.h" + +#include "generated_resources.h" + +#pragma comment(lib, "oleacc.lib") // Needed for accessibility support. + +// TODO (jcampan): these colors should be derived from the system colors to +// ensure they show properly. Bug #948807. +// Colors used to emphasize the scheme in the URL. +static const COLORREF kSecureSchemeColor = RGB(0, 150, 20); +static const COLORREF kInsecureSchemeColor = RGB(200, 0, 0); + +// Colors used to strike-out the scheme when it is insecure. +static const SkColor kSchemeStrikeoutColor = SkColorSetRGB(210, 0, 0); +static const SkColor kSchemeSelectedStrikeoutColor = + SkColorSetRGB(255, 255, 255); + +/////////////////////////////////////////////////////////////////////////////// +// Helper classes + +AutocompleteEdit::ScopedFreeze::ScopedFreeze(AutocompleteEdit* edit, + ITextDocument* text_object_model) + : edit_(edit), + text_object_model_(text_object_model) { + // Freeze the screen. + if (text_object_model_) { + long count; + text_object_model_->Freeze(&count); + } +} + +AutocompleteEdit::ScopedFreeze::~ScopedFreeze() { + // Unfreeze the screen. + // NOTE: If this destructor is reached while the edit is being destroyed (for + // example, because we double-clicked the edit of a popup and caused it to + // transform to an unconstrained window), it will no longer have an HWND, and + // text_object_model_ may point to a destroyed object, so do nothing here. + if (edit_->IsWindow() && text_object_model_) { + long count; + text_object_model_->Unfreeze(&count); + if (count == 0) { + // We need to UpdateWindow() here instead of InvalidateRect() because, as + // far as I can tell, the edit likes to synchronously erase its background + // when unfreezing, thus requiring us to synchronously redraw if we don't + // want flicker. + edit_->UpdateWindow(); + } + } +} + +AutocompleteEdit::ScopedSuspendUndo::ScopedSuspendUndo( + ITextDocument* text_object_model) + : text_object_model_(text_object_model) { + // Suspend Undo processing. + if (text_object_model_) + text_object_model_->Undo(tomSuspend, NULL); +} + +AutocompleteEdit::ScopedSuspendUndo::~ScopedSuspendUndo() { + // Resume Undo processing. + if (text_object_model_) + text_object_model_->Undo(tomResume, NULL); +} + +/////////////////////////////////////////////////////////////////////////////// +// AutocompleteEdit + +// These are used to hook the CRichEditCtrl's calls to BeginPaint() and +// EndPaint() and provide a memory DC instead. See OnPaint(). +static HWND edit_hwnd = NULL; +static PAINTSTRUCT paint_struct; + +// A single AutocompleteController used solely for making synchronous calls to +// determine how to deal with the clipboard contents for Paste And Go +// functionality. We avoid using the popup's controller here because we don't +// want to interrupt in-progress queries or modify the popup state just +// because the user right-clicked the edit. We don't need a controller for +// every edit because this will always be accessed on the main thread, so we +// won't have thread-safety problems. +static AutocompleteController* paste_and_go_controller = NULL; +static int paste_and_go_controller_refcount = 0; + +AutocompleteEdit::AutocompleteEdit(const ChromeFont& font, + Controller* controller, + ToolbarModel* model, + ChromeViews::View* parent_view, + HWND hwnd, + Profile* profile, + CommandController* command_controller, + bool popup_window_mode) + : controller_(controller), + model_(model), + popup_(new AutocompletePopup(font, this, profile)), + popup_window_mode_(popup_window_mode), + has_focus_(false), + user_input_in_progress_(false), + just_deleted_text_(false), + has_temporary_text_(false), + paste_state_(NONE), + tracking_click_(false), + tracking_double_click_(false), + double_click_time_(0), + can_discard_mousemove_(false), + control_key_state_(UP), + command_controller_(command_controller), + parent_view_(parent_view), + font_(font), + profile_(profile), + possible_drag_(false), + in_drag_(false), + initiated_drag_(false), + drop_highlight_position_(-1), + is_keyword_hint_(false), + disable_keyword_ui_(false), + show_search_hint_(true), + background_color_(0), + scheme_security_level_(ToolbarModel::NORMAL) { + if (!popup_window_mode_ && ++paste_and_go_controller_refcount == 1) { + // We don't have a controller yet, so create one. No listener is needed + // since we'll only be doing synchronous calls, and no profile is set since + // we'll set this before each call to the controller. + paste_and_go_controller = new AutocompleteController(NULL, NULL); + } + + saved_selection_for_focus_change_.cpMin = -1; + + // Statics used for global patching of riched20.dll. + static HMODULE richedit_module = NULL; + static iat_patch::IATPatchFunction patch_begin_paint; + static iat_patch::IATPatchFunction patch_end_paint; + + if (!richedit_module) { + richedit_module = LoadLibrary(L"riched20.dll"); + if (richedit_module) { + DCHECK(!patch_begin_paint.is_patched()); + patch_begin_paint.Patch(richedit_module, "user32.dll", "BeginPaint", + &BeginPaintIntercept); + DCHECK(!patch_end_paint.is_patched()); + patch_end_paint.Patch(richedit_module, "user32.dll", "EndPaint", + &EndPaintIntercept); + } + } + + Create(hwnd, 0, 0, 0, l10n_util::GetExtendedStyles()); + SetReadOnly(popup_window_mode_); + SetFont(font_.hfont()); + + // NOTE: Do not use SetWordBreakProcEx() here, that is no longer supported as + // of Rich Edit 2.0 onward. + SendMessage(m_hWnd, EM_SETWORDBREAKPROC, 0, + reinterpret_cast<LPARAM>(&WordBreakProc)); + + // Get the metrics for the font. + HDC dc = ::GetDC(NULL); + SelectObject(dc, font_.hfont()); + TEXTMETRIC tm = {0}; + GetTextMetrics(dc, &tm); + font_ascent_ = tm.tmAscent; + const float kXHeightRatio = 0.7f; // The ratio of a font's x-height to its + // cap height. Sadly, Windows doesn't + // provide a true value for a font's + // x-height in its text metrics, so we + // approximate. + font_x_height_ = static_cast<int>((static_cast<float>(font_ascent_ - + tm.tmInternalLeading) * kXHeightRatio) + 0.5); + const int kTextBaseline = 18; // The distance from the top of the field to + // the desired baseline of the rendered text. + font_y_adjustment_ = kTextBaseline - font_ascent_; + font_descent_ = tm.tmDescent; + + // Get the number of twips per pixel, which we need below to offset our text + // by the desired number of pixels. + const long kTwipsPerPixel = kTwipsPerInch / GetDeviceCaps(dc, LOGPIXELSY); + ::ReleaseDC(NULL, dc); + + // Set the default character style -- adjust to our desired baseline and make + // text grey. + CHARFORMAT cf = {0}; + cf.dwMask = CFM_OFFSET | CFM_COLOR; + cf.yOffset = -font_y_adjustment_ * kTwipsPerPixel; + cf.crTextColor = GetSysColor(COLOR_GRAYTEXT); + SetDefaultCharFormat(cf); + + // Set up context menu. + context_menu_.reset(new Menu(this, Menu::TOPLEFT, m_hWnd)); + if (popup_window_mode_) { + context_menu_->AppendMenuItemWithLabel(IDS_COPY, + l10n_util::GetString(IDS_COPY)); + } else { + context_menu_->AppendMenuItemWithLabel(IDS_UNDO, + l10n_util::GetString(IDS_UNDO)); + context_menu_->AppendSeparator(); + context_menu_->AppendMenuItemWithLabel(IDS_CUT, + l10n_util::GetString(IDS_CUT)); + context_menu_->AppendMenuItemWithLabel(IDS_COPY, + l10n_util::GetString(IDS_COPY)); + context_menu_->AppendMenuItemWithLabel(IDS_PASTE, + l10n_util::GetString(IDS_PASTE)); + // GetContextualLabel() will override this next label with the + // IDS_PASTE_AND_SEARCH label as needed. + context_menu_->AppendMenuItemWithLabel( + IDS_PASTE_AND_GO, l10n_util::GetString(IDS_PASTE_AND_GO)); + context_menu_->AppendSeparator(); + context_menu_->AppendMenuItemWithLabel(IDS_SELECTALL, + l10n_util::GetString(IDS_SELECTALL)); + context_menu_->AppendSeparator(); + context_menu_->AppendMenuItemWithLabel( + IDS_EDIT_SEARCH_ENGINES, l10n_util::GetString(IDS_EDIT_SEARCH_ENGINES)); + } + + // By default RichEdit has a drop target. Revoke it so that we can install our + // own. Revoke takes care of deleting the existing one. + RevokeDragDrop(m_hWnd); + + // Register our drop target. RichEdit appears to invoke RevokeDropTarget when + // done so that we don't have to explicitly. + if (!popup_window_mode_) { + scoped_refptr<EditDropTarget> drop_target = new EditDropTarget(this); + RegisterDragDrop(m_hWnd, drop_target.get()); + } +} + +AutocompleteEdit::~AutocompleteEdit() { + NotificationService::current()->Notify(NOTIFY_AUTOCOMPLETE_EDIT_DESTROYED, + Source<AutocompleteEdit>(this), NotificationService::NoDetails()); + + if (!popup_window_mode_ && --paste_and_go_controller_refcount == 0) + delete paste_and_go_controller; +} + +void AutocompleteEdit::Update(const TabContents* tab_for_state_restoring) { + // When there's a new URL, and the user is not editing anything or the edit + // doesn't have focus, we want to revert the edit to show the new URL. (The + // common case where the edit doesn't have focus is when the user has started + // an edit and then abandoned it and clicked a link on the page.) + std::wstring permanent_text = model_->GetText(); + const bool visibly_changed_permanent_text = + (permanent_text_ != permanent_text) && + (!user_input_in_progress_ || !has_focus_); + + permanent_text_ = permanent_text; + + COLORREF background_color = + LocationBarView::kBackgroundColorByLevel[model_->GetSchemeSecurityLevel()]; + + // Bail early when no visible state will actually change (prevents an + // unnecessary ScopedFreeze, and thus UpdateWindow()). + if ((background_color == background_color_) && + (model_->GetSchemeSecurityLevel() == scheme_security_level_) && + !visibly_changed_permanent_text && + !tab_for_state_restoring) + return; + + // Update our local state as desired. We set scheme_security_level_ here so + // it will already be correct before we get to any RevertAll()s below and use + // it. + ScopedFreeze freeze(this, GetTextObjectModel()); + if (background_color_ != background_color) { + background_color_ = background_color; + SetBackgroundColor(background_color_); + } + const bool changed_security_level = + (model_->GetSchemeSecurityLevel() != scheme_security_level_); + scheme_security_level_ = model_->GetSchemeSecurityLevel(); + + // When we're switching to a new tab, restore its state, if any. + if (tab_for_state_restoring) { + // Make sure we reset our own state first. The new tab may not have any + // saved state, or it may not have had input in progress, in which case we + // won't overwrite all our local state. + RevertAll(); + + const AutocompleteEdit::State* const state = + tab_for_state_restoring->saved_location_bar_state(); + if (state) { + // Restore any user editing. + if (state->user_input_in_progress) { + // NOTE: Be sure and set keyword-related state BEFORE invoking + // DisplayTextFromUserText(), as its result depends upon this state. + keyword_ = state->keyword; + is_keyword_hint_ = state->is_keyword_hint; + disable_keyword_ui_ = state->disable_keyword_ui; + show_search_hint_ = state->show_search_hint; + SetUserText(state->user_text, DisplayTextFromUserText(state->user_text), + false); + popup_->manually_selected_match_ = state->manually_selected_match; + } + + // Restore user's selection. We do this after restoring the user_text + // above so we're selecting in the correct string. + SetSelectionRange(state->selection); + saved_selection_for_focus_change_ = + state->saved_selection_for_focus_change; + } + } else if (visibly_changed_permanent_text) { + // Not switching tabs, just updating the permanent text. (In the case where + // we _were_ switching tabs, the RevertAll() above already drew the new + // permanent text.) + + // Tweak: if the edit was previously nonempty and had all the text selected, + // select all the new text. This makes one particular case better: the + // user clicks in the box to change it right before the permanent URL is + // changed. Since the new URL is still fully selected, the user's typing + // will replace the edit contents as they'd intended. + // + // NOTE: The selection can be longer than the text length if the edit is in + // in rich text mode and the user has selected the "phantom newline" at the + // end, so use ">=" instead of "==" to see if all the text is selected. In + // theory we prevent this case from ever occurring, but this is still safe. + CHARRANGE sel; + GetSelection(sel); + const bool was_reversed = (sel.cpMin > sel.cpMax); + const bool was_sel_all = (sel.cpMin != sel.cpMax) && IsSelectAll(sel); + + RevertAll(); + + if (was_sel_all) + SelectAll(was_reversed); + } else if (changed_security_level) { + // Only the security style changed, nothing else. Redraw our text using it. + EmphasizeURLComponents(); + } +} + +void AutocompleteEdit::SetProfile(Profile* profile) { + DCHECK(profile); + profile_ = profile; + popup_->SetProfile(profile); +} + +void AutocompleteEdit::SaveStateToTab(TabContents* tab) { + DCHECK(tab); + + // Like typing, switching tabs "accepts" the temporary text as the user + // text, because it makes little sense to have temporary text when the + // popup is closed. + if (user_input_in_progress_) + InternalSetUserText(UserTextFromDisplayText(GetText())); + + CHARRANGE selection; + GetSelection(selection); + tab->set_saved_location_bar_state(new State(selection, + saved_selection_for_focus_change_, user_input_in_progress_, user_text_, + popup_->manually_selected_match_, keyword_, is_keyword_hint_, + disable_keyword_ui_, show_search_hint_)); +} + +std::wstring AutocompleteEdit::GetText() const { + const int len = GetTextLength() + 1; + std::wstring str; + GetWindowText(WriteInto(&str, len), len); + return str; +} + +std::wstring AutocompleteEdit::GetURLForCurrentText( + PageTransition::Type* transition, + bool* is_history_what_you_typed_match, + std::wstring* alternate_nav_url) { + return (popup_->is_open() || popup_->query_in_progress()) ? + popup_->URLsForCurrentSelection(transition, + is_history_what_you_typed_match, + alternate_nav_url) : + popup_->URLsForDefaultMatch(UserTextFromDisplayText(GetText()), + GetDesiredTLD(), transition, + is_history_what_you_typed_match, + alternate_nav_url); +} + +void AutocompleteEdit::SelectAll(bool reversed) { + if (reversed) + SetSelection(GetTextLength(), 0); + else + SetSelection(0, GetTextLength()); +} + +void AutocompleteEdit::RevertAll() { + ScopedFreeze freeze(this, GetTextObjectModel()); + ClosePopup(); + popup_->manually_selected_match_.Clear(); + SetInputInProgress(false); + paste_state_ = NONE; + InternalSetUserText(std::wstring()); + SetWindowText(permanent_text_.c_str()); + keyword_.clear(); + is_keyword_hint_ = false; + disable_keyword_ui_ = false; + show_search_hint_ = permanent_text_.empty(); + PlaceCaretAt(has_focus_ ? permanent_text_.length() : 0); + saved_selection_for_focus_change_.cpMin = -1; + has_temporary_text_ = false; + TextChanged(); +} + +void AutocompleteEdit::AcceptInput(WindowOpenDisposition disposition, + bool for_drop) { + // Get the URL and transition type for the selected entry. + PageTransition::Type transition; + bool is_history_what_you_typed_match; + std::wstring alternate_nav_url; + const std::wstring url(GetURLForCurrentText(&transition, + &is_history_what_you_typed_match, + &alternate_nav_url)); + if (url.empty()) + return; + + if (url == permanent_text_) { + // When the user hit enter on the existing permanent URL, treat it like a + // reload for scoring purposes. We could detect this by just checking + // user_input_in_progress_, but it seems better to treat "edits" that end + // up leaving the URL unchanged (e.g. deleting the last character and then + // retyping it) as reloads too. + transition = PageTransition::RELOAD; + } else if (for_drop || ((paste_state_ != NONE) && + is_history_what_you_typed_match)) { + // When the user pasted in a URL and hit enter, score it like a link click + // rather than a normal typed URL, so it doesn't get inline autocompleted + // as aggressively later. + transition = PageTransition::LINK; + } + + OpenURL(url, disposition, transition, alternate_nav_url, + AutocompletePopup::kNoMatch, + is_keyword_hint_ ? std::wstring() : keyword_); +} + +void AutocompleteEdit::OpenURL(const std::wstring& url, + WindowOpenDisposition disposition, + PageTransition::Type transition, + const std::wstring& alternate_nav_url, + size_t selected_line, + const std::wstring& keyword) { + if (url.empty()) + return; + + ScopedFreeze freeze(this, GetTextObjectModel()); + SendOpenNotification(selected_line, keyword); + + if (disposition != NEW_BACKGROUND_TAB) + RevertAll(); // Revert the box to its unedited state + controller_->OnAutocompleteAccept(url, disposition, transition, + alternate_nav_url); +} + +void AutocompleteEdit::ClosePopup() { + popup_->StopAutocomplete(); +} + +IAccessible* AutocompleteEdit::GetIAccessible() { + if (!autocomplete_accessibility_) { + CComObject<AutocompleteAccessibility>* accessibility = NULL; + if (!SUCCEEDED(CComObject<AutocompleteAccessibility>::CreateInstance( + &accessibility)) || !accessibility) + return NULL; + + // Wrap the created object in a smart pointer so it won't leak. + CComPtr<IAccessible> accessibility_comptr(accessibility); + if (!SUCCEEDED(accessibility->Initialize(this))) + return NULL; + + // Copy to the class smart pointer, and notify that an instance of + // IAccessible was allocated for m_hWnd. + autocomplete_accessibility_ = accessibility_comptr; + NotifyWinEvent(EVENT_OBJECT_CREATE, m_hWnd, OBJID_CLIENT, CHILDID_SELF); + } + // Detach to leave ref counting to the caller. + return autocomplete_accessibility_.Detach(); +} + +void AutocompleteEdit::SetDropHighlightPosition(int position) { + if (drop_highlight_position_ != position) { + RepaintDropHighlight(drop_highlight_position_); + drop_highlight_position_ = position; + RepaintDropHighlight(drop_highlight_position_); + } +} + +void AutocompleteEdit::MoveSelectedText(int new_position) { + const std::wstring selected_text(GetSelectedText()); + CHARRANGE sel; + GetSel(sel); + DCHECK((sel.cpMax != sel.cpMin) && (new_position >= 0) && + (new_position <= GetTextLength())); + + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + + // Nuke the selected text. + ReplaceSel(L"", TRUE); + + // And insert it into the new location. + if (new_position >= sel.cpMin) + new_position -= (sel.cpMax - sel.cpMin); + PlaceCaretAt(new_position); + ReplaceSel(selected_text.c_str(), TRUE); + + OnAfterPossibleChange(); +} + +void AutocompleteEdit::InsertText(int position, const std::wstring& text) { + DCHECK((position >= 0) && (position <= GetTextLength())); + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + SetSelection(position, position); + ReplaceSel(text.c_str()); + OnAfterPossibleChange(); +} + +void AutocompleteEdit::PasteAndGo(const std::wstring& text) { + if (CanPasteAndGo(text)) + PasteAndGo(); +} + +bool AutocompleteEdit::OverrideAccelerator( + const ChromeViews::Accelerator& accelerator) { + // Only override <esc>, and only when there is input in progress -- otherwise, + // if focus happens to be in the location bar, users can't still hit <esc> to + // stop a load. + if ((accelerator.GetKeyCode() != VK_ESCAPE) || accelerator.IsAltDown() || + !user_input_in_progress_) + return false; + + if (!has_temporary_text_ || + (popup_->URLsForCurrentSelection(NULL, NULL, NULL) == original_url_)) { + // The popup isn't open or the selection in it is still the default + // selection, so revert the box all the way back to its unedited state. + RevertAll(); + return true; + } + + // The user typed something, then selected a different item. Restore the + // text they typed and change back to the default item. + // NOTE: This purposefully does not reset paste_state_. + ScopedFreeze freeze(this, GetTextObjectModel()); + just_deleted_text_ = false; + const std::wstring new_window_text(user_text_ + + inline_autocomplete_text_); + SetWindowText(new_window_text.c_str()); + SetSelectionRange(original_selection_); + has_temporary_text_ = false; + popup_->manually_selected_match_ = original_selected_match_; + UpdatePopup(); + TextChanged(); + return true; +} + +void AutocompleteEdit::HandleExternalMsg(UINT msg, + UINT flags, + const CPoint& screen_point) { + if (msg == WM_CAPTURECHANGED) { + SendMessage(msg, 0, NULL); + return; + } + + CPoint client_point(screen_point); + ::MapWindowPoints(NULL, m_hWnd, &client_point, 1); + SendMessage(msg, flags, MAKELPARAM(client_point.x, client_point.y)); +} + +void AutocompleteEdit::OnPopupDataChanged( + const std::wstring& text, + bool is_temporary_text, + const AutocompleteResult::Selection& previous_selected_match, + const std::wstring& keyword, + bool is_keyword_hint, + bool can_show_search_hint) { + // We don't want to show the search hint if we're showing a keyword hint or + // selected keyword, or (subtle!) if we would be showing a selected keyword + // but for disable_keyword_ui_. + can_show_search_hint &= keyword.empty(); + + // Update keyword/hint-related local state. + bool keyword_state_changed = (keyword_ != keyword) || + ((is_keyword_hint_ != is_keyword_hint) && !keyword.empty()) || + (show_search_hint_ != can_show_search_hint); + if (keyword_state_changed) { + keyword_ = keyword; + is_keyword_hint_ = is_keyword_hint; + show_search_hint_ = can_show_search_hint; + } + + // Handle changes to temporary text. + if (is_temporary_text) { + if (!has_temporary_text_) { + // Save the original selection and URL so it can be reverted later. + has_temporary_text_ = true; + GetSelection(original_selection_); + original_url_ = popup_->URLsForCurrentSelection(NULL, NULL, NULL); + original_selected_match_ = previous_selected_match; + } + + // Set new text and cursor position. Sometimes this does extra work (e.g. + // when the new text and the old text are identical), but it's only called + // when the user manually changes the selected line in the popup, so that's + // not really a problem. Also, even when the text hasn't changed we'd want + // to update the caret, because if the user had the cursor in the middle of + // the text and then arrowed to another entry with the same text, we'd still + // want to move the caret. + const std::wstring display_text(DisplayTextFromUserText(text)); + ScopedFreeze freeze(this, GetTextObjectModel()); + SetWindowText(display_text.c_str()); + PlaceCaretAt(display_text.length()); + TextChanged(); + return; + } + + // Handle changes to inline autocomplete text. Don't make changes if the user + // is showing temporary text. Making display changes would be obviously + // wrong; making changes to the inline_autocomplete_text_ itself turns out to + // be more subtlely wrong, because it means hitting esc will no longer revert + // to the original state before arrowing. + if (!has_temporary_text_) { + inline_autocomplete_text_ = text; + // Update the text and selection. Because this can be called repeatedly + // while typing, we've careful not to freeze the edit unless we really need + // to. Also, unlike in the temporary text case above, here we don't want to + // update the caret/selection unless we have to, since this might make the + // user's caret position change without warning during typing. + const std::wstring display_text( + DisplayTextFromUserText(user_text_ + inline_autocomplete_text_)); + if (display_text != GetText()) { + ScopedFreeze freeze(this, GetTextObjectModel()); + SetWindowText(display_text.c_str()); + // Set a reversed selection to keep the caret in the same position, which + // avoids scrolling the user's text. + SetSelection( + static_cast<LONG>(display_text.length()), + static_cast<LONG>(DisplayTextFromUserText(user_text_).length())); + TextChanged(); + return; + } + } + + // If the above changes didn't warrant a text update but we did change keyword + // state, we have yet to notify the controller about it. + if (keyword_state_changed) + controller_->OnChanged(); +} + +bool AutocompleteEdit::IsCommandEnabled(int id) const { + switch (id) { + case IDS_UNDO: return !!CanUndo(); + case IDS_CUT: return !!CanCut(); + case IDS_COPY: return !!CanCopy(); + case IDS_PASTE: return !!CanPaste(); + case IDS_PASTE_AND_GO: return CanPasteAndGo(GetClipboardText()); + case IDS_SELECTALL: return !!CanSelectAll(); + case IDS_EDIT_SEARCH_ENGINES: + return command_controller_->IsCommandEnabled(IDC_EDIT_SEARCH_ENGINES); + default: NOTREACHED(); return false; + } +} + +bool AutocompleteEdit::GetContextualLabel(int id, std::wstring* out) const { + if ((id != IDS_PASTE_AND_GO) || + // No need to change the default IDS_PASTE_AND_GO label for a typed + // destination (this is also the type set when Paste And Go is disabled). + (paste_and_go_transition_ == PageTransition::TYPED)) + return false; + + out->assign(l10n_util::GetString(IDS_PASTE_AND_SEARCH)); + return true; +} + +void AutocompleteEdit::ExecuteCommand(int id) { + ScopedFreeze freeze(this, GetTextObjectModel()); + if (id == IDS_PASTE_AND_GO) { + // This case is separate from the switch() below since we don't want to wrap + // it in OnBefore/AfterPossibleChange() calls. + PasteAndGo(); + return; + } + + OnBeforePossibleChange(); + switch (id) { + case IDS_UNDO: + Undo(); + break; + + case IDS_CUT: + Cut(); + break; + + case IDS_COPY: + Copy(); + break; + + case IDS_PASTE: + Paste(); + break; + + case IDS_SELECTALL: + SelectAll(false); + break; + + case IDS_EDIT_SEARCH_ENGINES: + command_controller_->ExecuteCommand(IDC_EDIT_SEARCH_ENGINES); + break; + + default: + NOTREACHED(); + break; + } + OnAfterPossibleChange(); +} + +// static +int CALLBACK AutocompleteEdit::WordBreakProc(LPTSTR edit_text, + int current_pos, + int num_bytes, + int action) { + // TODO(pkasting): http://b/1111308 We should let other people, like ICU and + // GURL, do the work for us here instead of writing all this ourselves. + + // Sadly, even though the MSDN docs claim that the third parameter here is a + // number of characters, they lie. It's a number of bytes. + const int length = num_bytes / sizeof(wchar_t); + + // With no clear guidance from the MSDN docs on how to handle "not found" in + // the "find the nearest xxx..." cases below, I cap the return values at + // [0, length]. Since one of these (0) is also a valid position, the return + // values are thus ambiguous :( + switch (action) { + // Find nearest character before current position that begins a word. + case WB_LEFT: + case WB_MOVEWORDLEFT: { + if (current_pos < 2) { + // Either current_pos == 0, so we have a "not found" case and return 0, + // or current_pos == 1, and the only character before this position is + // at 0. + return 0; + } + + // Look for a delimiter before the previous character; the previous word + // starts immediately after. (If we looked for a delimiter before the + // current character, we could stop on the immediate prior character, + // which would mean we'd return current_pos -- which isn't "before the + // current position".) + const int prev_delim = + WordBreakProc(edit_text, current_pos - 1, num_bytes, WB_LEFTBREAK); + + if ((prev_delim == 0) && + !WordBreakProc(edit_text, 0, num_bytes, WB_ISDELIMITER)) { + // Got back 0, but position 0 isn't a delimiter. This was a "not + // found" 0, so return one of our own. + return 0; + } + + return prev_delim + 1; + } + + // Find nearest character after current position that begins a word. + case WB_RIGHT: + case WB_MOVEWORDRIGHT: { + if (WordBreakProc(edit_text, current_pos, num_bytes, WB_ISDELIMITER)) { + // The current character is a delimiter, so the next character starts + // a new word. Done. + return current_pos + 1; + } + + // Look for a delimiter after the current character; the next word starts + // immediately after. + const int next_delim = + WordBreakProc(edit_text, current_pos, num_bytes, WB_RIGHTBREAK); + if (next_delim == length) { + // Didn't find a delimiter. Return length to signal "not found". + return length; + } + + return next_delim + 1; + } + + // Determine if the current character delimits words. + case WB_ISDELIMITER: + return !!(WordBreakProc(edit_text, current_pos, num_bytes, WB_CLASSIFY) & + WBF_BREAKLINE); + + // Return the classification of the current character. + case WB_CLASSIFY: + if (IsWhitespace(edit_text[current_pos])) { + // Whitespace normally breaks words, but the MSDN docs say that we must + // not break on the CRs in a "CR, LF" or a "CR, CR, LF" sequence. Just + // check for an arbitrarily long sequence of CRs followed by LF and + // report "not a delimiter" for the current CR in that case. + while ((current_pos < (length - 1)) && + (edit_text[current_pos] == 0x13)) { + if (edit_text[++current_pos] == 0x10) + return WBF_ISWHITE; + } + return WBF_BREAKLINE | WBF_ISWHITE; + } + + // Punctuation normally breaks words, but the first two characters in + // "://" (end of scheme) should not be breaks, so that "http://" will be + // treated as one word. + if (ispunct(edit_text[current_pos], std::locale()) && + !SchemeEnd(edit_text, current_pos, length) && + !SchemeEnd(edit_text, current_pos - 1, length)) + return WBF_BREAKLINE; + + // Normal character, no flags. + return 0; + + // Finds nearest delimiter before current position. + case WB_LEFTBREAK: + for (int i = current_pos - 1; i >= 0; --i) { + if (WordBreakProc(edit_text, i, num_bytes, WB_ISDELIMITER)) + return i; + } + return 0; + + // Finds nearest delimiter after current position. + case WB_RIGHTBREAK: + for (int i = current_pos + 1; i < length; ++i) { + if (WordBreakProc(edit_text, i, num_bytes, WB_ISDELIMITER)) + return i; + } + return length; + } + + NOTREACHED(); + return 0; +} + +// static +bool AutocompleteEdit::SchemeEnd(LPTSTR edit_text, + int current_pos, + int length) { + return (current_pos >= 0) && + ((length - current_pos) > 2) && + (edit_text[current_pos] == ':') && + (edit_text[current_pos + 1] == '/') && + (edit_text[current_pos + 2] == '/'); +} + +// static +HDC AutocompleteEdit::BeginPaintIntercept(HWND hWnd, LPPAINTSTRUCT lpPaint) { + if (!edit_hwnd || (hWnd != edit_hwnd)) + return ::BeginPaint(hWnd, lpPaint); + + *lpPaint = paint_struct; + return paint_struct.hdc; +} + +// static +BOOL AutocompleteEdit::EndPaintIntercept(HWND hWnd, + CONST PAINTSTRUCT* lpPaint) { + return (edit_hwnd && (hWnd == edit_hwnd)) ? + true : ::EndPaint(hWnd, lpPaint); +} + +void AutocompleteEdit::OnChar(TCHAR ch, UINT repeat_count, UINT flags) { + // Don't let alt-enter beep. Not sure this is necessary, as the standard + // alt-enter will hit DiscardWMSysChar() and get thrown away, and + // ctrl-alt-enter doesn't seem to reach here for some reason? At least not on + // my system... still, this is harmless and maybe necessary in other locales. + if (ch == VK_RETURN && (flags & KF_ALTDOWN)) + return; + + // Escape is processed in OnKeyDown. Don't let any WM_CHAR messages propagate + // as we don't want the RichEdit to do anything funky. + if (ch == VK_ESCAPE && !(flags & KF_ALTDOWN)) + return; + + if (ch == VK_TAB) { + // Don't add tabs to the input. + return; + } + + HandleKeystroke(GetCurrentMessage()->message, ch, repeat_count, flags); +} + +void AutocompleteEdit::OnContextMenu(HWND window, const CPoint& point) { + if (point.x == -1 || point.y == -1) { + POINT p; + GetCaretPos(&p); + MapWindowPoints(HWND_DESKTOP, &p, 1); + context_menu_->RunMenuAt(p.x, p.y); + } else { + context_menu_->RunMenuAt(point.x, point.y); + } +} + +void AutocompleteEdit::OnCopy() { + const std::wstring text(GetSelectedText()); + if (text.empty()) + return; + + ClipboardService* clipboard = g_browser_process->clipboard_service(); + clipboard->Clear(); + clipboard->WriteText(text); + + // Check if the user is copying the whole address bar. If they are, we + // assume they are trying to copy a URL and write this to the clipboard as a + // hyperlink. + if (static_cast<int>(text.length()) < GetTextLength()) + return; + + // The entire control is selected. Let's see what the user typed. Usually + // we'd use GetDesiredTLD() to figure out the TLD, but right now the user is + // probably holding down control to cause the copy (which would make us always + // think the user wanted ".com" added). + url_parse::Parsed parts; + const AutocompleteInput::Type type = AutocompleteInput::Parse( + UserTextFromDisplayText(text), std::wstring(), &parts, NULL); + if (type == AutocompleteInput::URL) { + const GURL url(URLFixerUpper::FixupURL(text, std::wstring())); + clipboard->WriteHyperlink(text, url.spec()); + } +} + +void AutocompleteEdit::OnCut() { + OnCopy(); + + // This replace selection will have no effect (even on the undo stack) if the + // current selection is empty. + ReplaceSel(L"", true); +} + +LRESULT AutocompleteEdit::OnGetObject(UINT uMsg, WPARAM wparam, LPARAM lparam) { + // Accessibility readers will send an OBJID_CLIENT message. + if (lparam == OBJID_CLIENT) { + // Re-attach for internal re-usage of accessibility pointer. + autocomplete_accessibility_.Attach(GetIAccessible()); + + if (autocomplete_accessibility_) { + return LresultFromObject(IID_IAccessible, wparam, + autocomplete_accessibility_.p); + } + } + return 0; +} + +LRESULT AutocompleteEdit::OnImeComposition(UINT message, + WPARAM wparam, + LPARAM lparam) { + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + LRESULT result = DefWindowProc(message, wparam, lparam); + if (!OnAfterPossibleChange() && (lparam & GCS_RESULTSTR)) { + // The result string changed, but the text in the popup didn't actually + // change. This means the user finalized the composition. Rerun + // autocomplete so that we can now trigger inline autocomplete if + // applicable. + // + // Note that if we're in the midst of losing focus, UpdatePopup() won't + // actually rerun autocomplete, but will just set local state correctly. + UpdatePopup(); + } + return result; +} + +void AutocompleteEdit::OnKeyDown(TCHAR key, UINT repeat_count, UINT flags) { + if (OnKeyDownAllModes(key, repeat_count, flags) || popup_window_mode_ || + OnKeyDownOnlyWritable(key, repeat_count, flags)) + return; + + // CRichEditCtrl changes its text on WM_KEYDOWN instead of WM_CHAR for many + // different keys (backspace, ctrl-v, ...), so we call this in both cases. + HandleKeystroke(GetCurrentMessage()->message, key, repeat_count, flags); +} + +void AutocompleteEdit::OnKeyUp(TCHAR key, UINT repeat_count, UINT flags) { + if ((key == VK_CONTROL) && (control_key_state_ != UP)) { + control_key_state_ = UP; + if (popup_->is_open()) { + // Autocomplete history provider results may change, so refresh the + // popup. This will force user_input_in_progress_ to true, but if the + // popup is open, that should have already been the case. + UpdatePopup(); + } + } + + SetMsgHandled(false); +} + +void AutocompleteEdit::OnKillFocus(HWND focus_wnd) { + if (m_hWnd == focus_wnd) { + // Focus isn't actually leaving. + SetMsgHandled(false); + return; + } + + has_focus_ = false; + control_key_state_ = UP; + paste_state_ = NONE; + + // Close the popup. + ClosePopup(); + + // Save the user's existing selection to restore it later. + GetSelection(saved_selection_for_focus_change_); + + // Like typing, killing focus "accepts" the temporary text as the user + // text, because it makes little sense to have temporary text when the + // popup is closed. + InternalSetUserText(UserTextFromDisplayText(GetText())); + has_temporary_text_ = false; + + // Let the CRichEditCtrl do its default handling. This will complete any + // in-progress IME composition. We must do this after setting has_focus_ to + // false so that UpdatePopup() will know not to rerun autocomplete. + ScopedFreeze freeze(this, GetTextObjectModel()); + DefWindowProc(WM_KILLFOCUS, reinterpret_cast<WPARAM>(focus_wnd), 0); + + // Hide the "Type to search" hint if necessary. We do this after calling + // DefWindowProc() because processing the resulting IME messages may notify + // the controller that input is in progress, which could cause the visible + // hints to change. (I don't know if there's a real scenario where they + // actually do change, but this is safest.) + if (show_search_hint_ || (is_keyword_hint_ && !keyword_.empty())) + controller_->OnChanged(); + + // Cancel any user selection and scroll the text back to the beginning of the + // URL. We have to do this after calling DefWindowProc() because otherwise + // an in-progress IME composition will be completed at the new caret position, + // resulting in the string jumping unexpectedly to the front of the edit. + PlaceCaretAt(0); +} + +void AutocompleteEdit::OnLButtonDblClk(UINT keys, const CPoint& point) { + // Save the double click info for later triple-click detection. + tracking_double_click_ = true; + double_click_point_ = point; + double_click_time_ = GetCurrentMessage()->time; + possible_drag_ = false; + + // Modifying the selection counts as accepting any inline autocompletion, so + // track "changes" made by clicking the mouse button. + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + DefWindowProc(WM_LBUTTONDBLCLK, keys, + MAKELPARAM(ClipXCoordToVisibleText(point.x, false), point.y)); + OnAfterPossibleChange(); + + gaining_focus_.reset(); // See NOTE in OnMouseActivate(). +} + +void AutocompleteEdit::OnLButtonDown(UINT keys, const CPoint& point) { + if (gaining_focus_.get()) { + // This click is giving us focus, so we need to track how much the mouse + // moves to see if it's a drag or just a click. Clicks should select all + // the text. + tracking_click_ = true; + mouse_down_point_ = point; + + // When Chrome was already the activated app, we haven't reached + // OnSetFocus() yet. When we get there, don't restore the saved selection, + // since it will just screw up the user's interaction with the edit. + saved_selection_for_focus_change_.cpMin = -1; + + // Crazy hack: In this particular case, the CRichEditCtrl seems to have an + // internal flag that discards the next WM_LBUTTONDOWN without processing + // it, so that clicks on the edit when its owning app is not activated are + // eaten rather than processed (despite whatever the return value of + // DefWindowProc(WM_MOUSEACTIVATE, ...) may say). This behavior is + // confusing and we want the click to be treated normally. So, to reset the + // CRichEditCtrl's internal flag, we pass it an extra WM_LBUTTONDOWN here + // (as well as a matching WM_LBUTTONUP, just in case we'd be confusing some + // kind of state tracking otherwise). + DefWindowProc(WM_LBUTTONDOWN, keys, MAKELPARAM(point.x, point.y)); + DefWindowProc(WM_LBUTTONUP, keys, MAKELPARAM(point.x, point.y)); + } + + // Check for triple click, then reset tracker. Should be safe to subtract + // double_click_time_ from the current message's time even if the timer has + // wrapped in between. + const bool is_triple_click = tracking_double_click_ && + win_util::IsDoubleClick(double_click_point_, point, + GetCurrentMessage()->time - double_click_time_); + tracking_double_click_ = false; + + if (!gaining_focus_.get() && !is_triple_click) + OnPossibleDrag(point); + + + // Modifying the selection counts as accepting any inline autocompletion, so + // track "changes" made by clicking the mouse button. + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + DefWindowProc(WM_LBUTTONDOWN, keys, + MAKELPARAM(ClipXCoordToVisibleText(point.x, is_triple_click), + point.y)); + OnAfterPossibleChange(); + + gaining_focus_.reset(); +} + +void AutocompleteEdit::OnLButtonUp(UINT keys, const CPoint& point) { + // default processing should happen first so we can see the result of the + // selection + ScopedFreeze freeze(this, GetTextObjectModel()); + DefWindowProc(WM_LBUTTONUP, keys, + MAKELPARAM(ClipXCoordToVisibleText(point.x, false), point.y)); + + // When the user has clicked and released to give us focus, select all. + if (tracking_click_ && !win_util::IsDrag(mouse_down_point_, point)) { + // Select all in the reverse direction so as not to scroll the caret + // into view and shift the contents jarringly. + SelectAll(true); + possible_drag_ = false; + } + + tracking_click_ = false; + + UpdateDragDone(keys); +} + +LRESULT AutocompleteEdit::OnMouseActivate(HWND window, + UINT hit_test, + UINT mouse_message) { + // First, give other handlers a chance to handle the message to see if we are + // actually going to activate and gain focus. + LRESULT result = DefWindowProc(WM_MOUSEACTIVATE, + reinterpret_cast<WPARAM>(window), + MAKELPARAM(hit_test, mouse_message)); + // Check if we're getting focus from a left click. We have to do this here + // rather than in OnLButtonDown() since in many scenarios OnSetFocus() will be + // reached before OnLButtonDown(), preventing us from detecting this properly + // there. Also in those cases, we need to already know in OnSetFocus() that + // we should not restore the saved selection. + if (!has_focus_ && (mouse_message == WM_LBUTTONDOWN) && + (result == MA_ACTIVATE)) { + DCHECK(!gaining_focus_.get()); + gaining_focus_.reset(new ScopedFreeze(this, GetTextObjectModel())); + // NOTE: Despite |mouse_message| being WM_LBUTTONDOWN here, we're not + // guaranteed to call OnLButtonDown() later! Specifically, if this is the + // second click of a double click, we'll reach here but later call + // OnLButtonDblClk(). Make sure |gaining_focus_| gets reset both places, or + // we'll have visual glitchiness and then DCHECK failures. + + // Don't restore saved selection, it will just screw up our interaction + // with this edit. + saved_selection_for_focus_change_.cpMin = -1; + } + return result; +} + +void AutocompleteEdit::OnMouseMove(UINT keys, const CPoint& point) { + if (possible_drag_) { + StartDragIfNecessary(point); + // Don't fall through to default mouse handling, otherwise a second + // drag session may start. + return; + } + + if (tracking_click_ && !win_util::IsDrag(mouse_down_point_, point)) + return; + + tracking_click_ = false; + + // Return quickly if this can't change the selection/cursor, so we don't + // create a ScopedFreeze (and thus do an UpdateWindow()) on every + // WM_MOUSEMOVE. + if (!(keys & MK_LBUTTON)) { + DefWindowProc(WM_MOUSEMOVE, keys, MAKELPARAM(point.x, point.y)); + return; + } + + // Clamp the selection to the visible text so the user can't drag to select + // the "phantom newline". In theory we could achieve this by clipping the X + // coordinate, but in practice the edit seems to behave nondeterministically + // with similar sequences of clipped input coordinates fed to it. Maybe it's + // reading the mouse cursor position directly? + // + // This solution has a minor visual flaw, however: if there's a visible cursor + // at the edge of the text (only true when there's no selection), dragging the + // mouse around outside that edge repaints the cursor on every WM_MOUSEMOVE + // instead of allowing it to blink normally. To fix this, we special-case + // this exact case and discard the WM_MOUSEMOVE messages instead of passing + // them along. + // + // But even this solution has a flaw! (Argh.) In the case where the user has + // a selection that starts at the edge of the edit, and proceeds to the middle + // of the edit, and the user is dragging back past the start edge to remove + // the selection, there's a redraw problem where the change between having the + // last few bits of text still selected and having nothing selected can be + // slow to repaint (which feels noticeably strange). This occurs if you only + // let the edit receive a single WM_MOUSEMOVE past the edge of the text. I + // think on each WM_MOUSEMOVE the edit is repainting its previous state, then + // updating its internal variables to the new state but not repainting. To + // fix this, we allow one more WM_MOUSEMOVE through after the selection has + // supposedly been shrunk to nothing; this makes the edit redraw the selection + // quickly so it feels smooth. + CHARRANGE selection; + GetSel(selection); + const bool possibly_can_discard_mousemove = + (selection.cpMin == selection.cpMax) && + (((selection.cpMin == 0) && + (ClipXCoordToVisibleText(point.x, false) > point.x)) || + ((selection.cpMin == GetTextLength()) && + (ClipXCoordToVisibleText(point.x, false) < point.x))); + if (!can_discard_mousemove_ || !possibly_can_discard_mousemove) { + can_discard_mousemove_ = possibly_can_discard_mousemove; + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + // Force the Y coordinate to the center of the clip rect. The edit + // behaves strangely when the cursor is dragged vertically: if the cursor + // is in the middle of the text, drags inside the clip rect do nothing, + // and drags outside the clip rect act as if the cursor jumped to the + // left edge of the text. When the cursor is at the right edge, drags of + // just a few pixels vertically end up selecting the "phantom newline"... + // sometimes. + RECT r; + GetRect(&r); + DefWindowProc(WM_MOUSEMOVE, keys, + MAKELPARAM(point.x, (r.bottom - r.top) / 2)); + OnAfterPossibleChange(); + } +} + +void AutocompleteEdit::OnPaint(HDC bogus_hdc) { + // We need to paint over the top of the edit. If we simply let the edit do + // its default painting, then do ours into the window DC, the screen is + // updated in between and we can get flicker. To avoid this, we force the + // edit to paint into a memory DC, which we also paint onto, then blit the + // whole thing to the screen. + + // Don't paint if not necessary. + CRect paint_clip_rect; + if (!GetUpdateRect(&paint_clip_rect, true)) + return; + + // Begin painting, and create a memory DC for the edit to paint into. + CPaintDC paint_dc(m_hWnd); + CDC memory_dc(CreateCompatibleDC(paint_dc)); + CRect rect; + GetClientRect(&rect); + // NOTE: This next call uses |paint_dc| instead of |memory_dc| because + // |memory_dc| contains a 1x1 monochrome bitmap by default, which will cause + // |memory_bitmap| to be monochrome, which isn't what we want. + CBitmap memory_bitmap(CreateCompatibleBitmap(paint_dc, rect.Width(), + rect.Height())); + HBITMAP old_bitmap = memory_dc.SelectBitmap(memory_bitmap); + + // Tell our intercept functions to supply our memory DC to the edit when it + // tries to call BeginPaint(). + // + // The sane way to do this would be to use WM_PRINTCLIENT to ask the edit to + // paint into our desired DC. Unfortunately, the Rich Edit 3.0 that ships + // with Windows 2000/XP/Vista doesn't handle WM_PRINTCLIENT correctly; it + // treats it just like WM_PAINT and calls BeginPaint(), ignoring our provided + // DC. The Rich Edit 6.0 that ships with Office 2007 handles this better, but + // has other issues, and we can't redistribute that DLL anyway. So instead, + // we use this scary hack. + // + // NOTE: It's possible to get nested paint calls (!) (try setting the + // permanent URL to something longer than the edit width, then selecting the + // contents of the edit, typing a character, and hitting <esc>), so we can't + // DCHECK(!edit_hwnd_) here. Instead, just save off the old HWND, which most + // of the time will be NULL. + HWND old_edit_hwnd = edit_hwnd; + edit_hwnd = m_hWnd; + paint_struct = paint_dc.m_ps; + paint_struct.hdc = memory_dc; + DefWindowProc(WM_PAINT, reinterpret_cast<WPARAM>(bogus_hdc), 0); + + // Make the selection look better. + EraseTopOfSelection(&memory_dc, rect, paint_clip_rect); + + // Draw a slash through the scheme if this is insecure. + if (insecure_scheme_component_.is_nonempty()) + DrawSlashForInsecureScheme(memory_dc, rect, paint_clip_rect); + + // Draw the drop highlight. + if (drop_highlight_position_ != -1) + DrawDropHighlight(memory_dc, rect, paint_clip_rect); + + // Blit the memory DC to the actual paint DC and clean up. + BitBlt(paint_dc, rect.left, rect.top, rect.Width(), rect.Height(), memory_dc, + rect.left, rect.top, SRCCOPY); + memory_dc.SelectBitmap(old_bitmap); + edit_hwnd = old_edit_hwnd; +} + +void AutocompleteEdit::OnNonLButtonDown(UINT keys, const CPoint& point) { + // Interestingly, the edit doesn't seem to cancel triple clicking when the + // x-buttons (which usually means "thumb buttons") are pressed, so we only + // call this for M and R down. + tracking_double_click_ = false; + + OnPossibleDrag(point); + + SetMsgHandled(false); +} + +void AutocompleteEdit::OnNonLButtonUp(UINT keys, const CPoint& point) { + UpdateDragDone(keys); + + // Let default handler have a crack at this. + SetMsgHandled(false); +} + +void AutocompleteEdit::OnPaste() { + // Replace the selection if we have something to paste. + const std::wstring text(GetClipboardText()); + if (!text.empty()) { + // If this paste will be replacing all the text, record that, so we can do + // different behaviors in such a case. + CHARRANGE sel; + GetSel(sel); + if (IsSelectAll(sel)) + paste_state_ = REPLACING_ALL; + ReplaceSel(text.c_str(), true); + } +} + +void AutocompleteEdit::OnSetFocus(HWND focus_wnd) { + has_focus_ = true; + control_key_state_ = (GetKeyState(VK_CONTROL) < 0) ? DOWN_WITHOUT_CHANGE : UP; + + // Notify controller if it needs to show hint UI of some kind. + ScopedFreeze freeze(this, GetTextObjectModel()); + if (show_search_hint_ || (is_keyword_hint_ && !keyword_.empty())) + controller_->OnChanged(); + + // Restore saved selection if available. + if (saved_selection_for_focus_change_.cpMin != -1) { + SetSelectionRange(saved_selection_for_focus_change_); + saved_selection_for_focus_change_.cpMin = -1; + } + + SetMsgHandled(false); +} + +void AutocompleteEdit::OnSysChar(TCHAR ch, UINT repeat_count, UINT flags) { + // Nearly all alt-<xxx> combos result in beeping rather than doing something + // useful, so we discard most. Exceptions: + // * ctrl-alt-<xxx>, which is sometimes important, generates WM_CHAR instead + // of WM_SYSCHAR, so it doesn't need to be handled here. + // * alt-space gets translated by the default WM_SYSCHAR handler to a + // WM_SYSCOMMAND to open the application context menu, so we need to allow + // it through. + if (ch == VK_SPACE) + SetMsgHandled(false); +} + +void AutocompleteEdit::HandleKeystroke(UINT message, TCHAR key, + UINT repeat_count, UINT flags) { + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + DefWindowProc(message, key, MAKELPARAM(repeat_count, flags)); + OnAfterPossibleChange(); +} + +bool AutocompleteEdit::OnKeyDownOnlyWritable(TCHAR key, + UINT repeat_count, + UINT flags) { + // NOTE: Annoyingly, ctrl-alt-<key> generates WM_KEYDOWN rather than + // WM_SYSKEYDOWN, so we need to check (flags & KF_ALTDOWN) in various places + // in this function even with a WM_SYSKEYDOWN handler. + + int count = repeat_count; + switch (key) { + case VK_RETURN: + AcceptInput((flags & KF_ALTDOWN) ? NEW_FOREGROUND_TAB : CURRENT_TAB, + false); + return true; + + case VK_UP: + count = -count; + // FALL THROUGH + case VK_DOWN: + if (flags & KF_ALTDOWN) + return false; + + // NOTE: VK_DOWN/VK_UP purposefully don't trigger any code that resets + // paste_state_. + disable_keyword_ui_ = false; + if (!popup_->is_open()) { + if (!popup_->query_in_progress()) { + // The popup is neither open nor working on a query already. So, + // start an autocomplete query for the current text. This also sets + // user_input_in_progress_ to true, which we want: if the user has + // started to interact with the popup, changing the permanent_text_ + // shouldn't change the displayed text. + // Note: This does not force the popup to open immediately. + if (!user_input_in_progress_) + InternalSetUserText(permanent_text_); + DCHECK(user_text_ == UserTextFromDisplayText(GetText())); + UpdatePopup(); + } + + // Now go ahead and force the popup to open, and copy the text of the + // default item into the edit. We ignore |count|, since without the + // popup open, the user doesn't really know what they're interacting + // with. Since the user hit an arrow key to explicitly open the popup, + // we assume that they prefer the temporary text of the default item + // to their own text, like we do when they arrow around an already-open + // popup. In many cases the existing text in the edit and the new text + // will be the same, and the only visible effect will be to cancel any + // selection and place the cursor at the end of the edit. + popup_->Move(0); + } else { + // The popup is open, so the user should be able to interact with it + // normally. + popup_->Move(count); + } + return true; + + // Hijacking Editing Commands + // + // We hijack the keyboard short-cuts for Cut, Copy, and Paste here so that + // they go through our clipboard routines. This allows us to be smarter + // about how we interact with the clipboard and avoid bugs in the + // CRichEditCtrl. If we didn't hijack here, the edit control would handle + // these internally with sending the WM_CUT, WM_COPY, or WM_PASTE messages. + // + // Cut: Shift-Delete and Ctrl-x are treated as cut. Ctrl-Shift-Delete and + // Ctrl-Shift-x are not treated as cut even though the underlying + // CRichTextEdit would treat them as such. + // Copy: Ctrl-c is treated as copy. Shift-Ctrl-c is not. (This is handled + // in OnKeyDownAllModes().) + // Paste: Shift-Insert and Ctrl-v are tread as paste. Ctrl-Shift-Insert and + // Ctrl-Shift-v are not. + // + // This behavior matches most, but not all Windows programs, and largely + // conforms to what users expect. + + case VK_DELETE: + if ((flags & KF_ALTDOWN) || GetKeyState(VK_SHIFT) >= 0) + return false; + if (control_key_state_ == UP) { + // Cut text if possible. + CHARRANGE selection; + GetSel(selection); + if (selection.cpMin != selection.cpMax) { + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + Cut(); + OnAfterPossibleChange(); + } else if (popup_->is_open()) { + // This is a bit overloaded, but we hijack Shift-Delete in this + // case to delete the current item from the pop-up. We prefer cutting + // to this when possible since that's the behavior more people expect + // from Shift-Delete, and it's more commonly useful. + popup_->TryDeletingCurrentItem(); + } + } + return true; + + case 'X': + if ((flags & KF_ALTDOWN) || (control_key_state_ == UP)) + return false; + if (GetKeyState(VK_SHIFT) >= 0) { + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + Cut(); + OnAfterPossibleChange(); + } + return true; + + case VK_INSERT: + case 'V': + if ((flags & KF_ALTDOWN) || ((key == 'V') ? + (control_key_state_ == UP) : (GetKeyState(VK_SHIFT) >= 0))) + return false; + if ((key == 'V') ? + (GetKeyState(VK_SHIFT) >= 0) : (control_key_state_ == UP)) { + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + Paste(); + OnAfterPossibleChange(); + } + return true; + + case VK_BACK: { + if ((flags & KF_ALTDOWN) || is_keyword_hint_ || keyword_.empty()) + return false; + + { + CHARRANGE selection; + GetSel(selection); + if ((selection.cpMin != selection.cpMax) || (selection.cpMin != 0)) + return false; + } + + // We're showing a keyword and the user pressed backspace at the beginning + // of the text. Delete the trailing space from the keyword forcing the + // selected keyword to become empty. + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + const std::wstring window_text(keyword_ + GetText()); + SetWindowText(window_text.c_str()); + PlaceCaretAt(keyword_.length()); + popup_->manually_selected_match_.Clear(); + keyword_.clear(); + OnAfterPossibleChange(); + just_deleted_text_ = true; // OnAfterPossibleChange() fails to clear this + // since the edit contents have actually grown + // longer. + return true; + } + + case VK_TAB: { + if (is_keyword_hint_ && !keyword_.empty()) { + // Accept the keyword. + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + SetWindowText(L""); + popup_->manually_selected_match_.Clear(); + popup_->manually_selected_match_.provider_affinity = + popup_->autocomplete_controller()->keyword_provider(); + is_keyword_hint_ = false; + disable_keyword_ui_ = false; + OnAfterPossibleChange(); + just_deleted_text_ = false; // OnAfterPossibleChange() erroneously sets + // this since the edit contents have + // disappeared. It doesn't really matter, + // but we clear it to be consistent. + + // Send out notification (primarily for logging). + UserMetrics::RecordAction(L"AcceptedKeywordHint", profile_); + } + return true; + } + + case 0xbb: // Ctrl-'='. Triggers subscripting (even in plain text mode). + return true; + + default: + return false; + } +} + +bool AutocompleteEdit::OnKeyDownAllModes(TCHAR key, + UINT repeat_count, + UINT flags) { + // See KF_ALTDOWN comment atop OnKeyDownOnlyWriteable(). + + switch (key) { + case VK_CONTROL: + if (control_key_state_ == UP) { + control_key_state_ = DOWN_WITHOUT_CHANGE; + if (popup_->is_open()) { + DCHECK(!popup_window_mode_); // How did the popup get open in read-only mode? + // Autocomplete history provider results may change, so refresh the + // popup. This will force user_input_in_progress_ to true, but if the + // popup is open, that should have already been the case. + UpdatePopup(); + } + } + return false; + + case 'C': + // See more detailed comments in OnKeyDownOnlyWriteable(). + if ((flags & KF_ALTDOWN) || (control_key_state_ == UP)) + return false; + if (GetKeyState(VK_SHIFT) >= 0) + Copy(); + return true; + + default: + return false; + } +} + +void AutocompleteEdit::OnBeforePossibleChange() { + // Record our state. + text_before_change_ = GetText(); + GetSelection(sel_before_change_); + select_all_before_change_ = IsSelectAll(sel_before_change_); +} + +bool AutocompleteEdit::OnAfterPossibleChange() { + // Prevent the user from selecting the "phantom newline" at the end of the + // edit. If they try, we just silently move the end of the selection back to + // the end of the real text. + CHARRANGE new_sel; + GetSelection(new_sel); + const int length = GetTextLength(); + if ((new_sel.cpMin > length) || (new_sel.cpMax > length)) { + if (new_sel.cpMin > length) + new_sel.cpMin = length; + if (new_sel.cpMax > length) + new_sel.cpMax = length; + SetSelectionRange(new_sel); + } + const bool selection_differs = (new_sel.cpMin != sel_before_change_.cpMin) || + (new_sel.cpMax != sel_before_change_.cpMax); + + // See if the text or selection have changed since OnBeforePossibleChange(). + const std::wstring new_text(GetText()); + const bool text_differs = (new_text != text_before_change_); + + // Update the paste state as appropriate: if we're just finishing a paste + // that replaced all the text, preserve that information; otherwise, if we've + // made some other edit, clear paste tracking. + if (paste_state_ == REPLACING_ALL) + paste_state_ = REPLACED_ALL; + else if (text_differs) + paste_state_ = NONE; + + // If something has changed while the control key is down, prevent + // "ctrl-enter" until the control key is released. When we do this, we need + // to update the popup if it's open, since the desired_tld will have changed. + if ((text_differs || selection_differs) && + (control_key_state_ == DOWN_WITHOUT_CHANGE)) { + control_key_state_ = DOWN_WITH_CHANGE; + if (!text_differs && !popup_->is_open()) + return false; // Don't open the popup for no reason. + } else if (!text_differs && + (inline_autocomplete_text_.empty() || !selection_differs)) { + return false; + } + + const bool had_keyword = !is_keyword_hint_ && !keyword_.empty(); + + // Modifying the selection counts as accepting the autocompleted text. + InternalSetUserText(UserTextFromDisplayText(new_text)); + has_temporary_text_ = false; + + if (text_differs) { + // When the user has deleted text, don't allow inline autocomplete. Make + // sure to not flag cases like selecting part of the text and then pasting + // (or typing) the prefix of that selection. (We detect these by making + // sure the caret, which should be after any insertion, hasn't moved + // forward of the old selection start.) + just_deleted_text_ = (text_before_change_.length() > new_text.length()) && + (new_sel.cpMin <= std::min(sel_before_change_.cpMin, + sel_before_change_.cpMax)); + + // When the user doesn't have a selected keyword, deleting text or replacing + // all of it with something else should reset the provider affinity. The + // typical use case for deleting is that the user starts typing, sees that + // some entry is close to what he wants, arrows to it, and then deletes some + // unnecessary bit from the end of the string. In this case the user didn't + // actually want "provider X", he wanted the string from that entry for + // editing purposes, and he's no longer looking at the popup to notice that, + // despite deleting some text, the action we'll take on enter hasn't changed + // at all. + if (!had_keyword && (just_deleted_text_ || select_all_before_change_)) { + popup_->manually_selected_match_.Clear(); + } + } + + // Disable the fancy keyword UI if the user didn't already have a visible + // keyword and is not at the end of the edit. This prevents us from showing + // the fancy UI (and interrupting the user's editing) if the user happens to + // have a keyword for 'a', types 'ab' then puts a space between the 'a' and + // the 'b'. + disable_keyword_ui_ = (is_keyword_hint_ || keyword_.empty()) && + ((new_sel.cpMax != length) || (new_sel.cpMin != length)); + + UpdatePopup(); + + if (!had_keyword && !is_keyword_hint_ && !keyword_.empty()) { + // Went from no selected keyword to a selected keyword. Set the affinity to + // the keyword provider. This forces the selected keyword to persist even + // if the user deletes all the text. + popup_->manually_selected_match_.Clear(); + popup_->manually_selected_match_.provider_affinity = + popup_->autocomplete_controller()->keyword_provider(); + } + + if (text_differs) + TextChanged(); + + return true; +} + +void AutocompleteEdit::SetUserText(const std::wstring& text, + const std::wstring& display_text, + bool update_popup) { + ScopedFreeze freeze(this, GetTextObjectModel()); + SetInputInProgress(true); + paste_state_ = NONE; + InternalSetUserText(text); + SetWindowText(display_text.c_str()); + PlaceCaretAt(display_text.length()); + saved_selection_for_focus_change_.cpMin = -1; + has_temporary_text_ = false; + popup_->manually_selected_match_.Clear(); + if (update_popup) + UpdatePopup(); + TextChanged(); +} + +void AutocompleteEdit::InternalSetUserText(const std::wstring& text) { + user_text_ = text; + just_deleted_text_ = false; + inline_autocomplete_text_.clear(); +} + +void AutocompleteEdit::GetSelection(CHARRANGE& sel) const { + GetSel(sel); + + // See if we need to reverse the direction of the selection. + CComPtr<ITextSelection> selection; + const HRESULT hr = GetTextObjectModel()->GetSelection(&selection); + DCHECK(hr == S_OK); + long flags; + selection->GetFlags(&flags); + if (flags & tomSelStartActive) + std::swap(sel.cpMin, sel.cpMax); +} + +std::wstring AutocompleteEdit::GetSelectedText() const { + // Figure out the length of the selection. + CHARRANGE sel; + GetSel(sel); + + // Grab the selected text. + std::wstring str; + GetSelText(WriteInto(&str, sel.cpMax - sel.cpMin + 1)); + return str; +} + +void AutocompleteEdit::SetSelection(LONG start, LONG end) { + SetSel(start, end); + + if (start <= end) + return; + + // We need to reverse the direction of the selection. + CComPtr<ITextSelection> selection; + const HRESULT hr = GetTextObjectModel()->GetSelection(&selection); + DCHECK(hr == S_OK); + selection->SetFlags(tomSelStartActive); +} + +void AutocompleteEdit::PlaceCaretAt(std::wstring::size_type pos) { + SetSelection(static_cast<LONG>(pos), static_cast<LONG>(pos)); +} + +bool AutocompleteEdit::IsSelectAll(const CHARRANGE& sel) const { + const int text_length = GetTextLength(); + return ((sel.cpMin == 0) && (sel.cpMax >= text_length)) || + ((sel.cpMax == 0) && (sel.cpMin >= text_length)); +} + +LONG AutocompleteEdit::ClipXCoordToVisibleText(LONG x, + bool is_triple_click) const { + // Clip the X coordinate to the left edge of the text. Careful: + // PosFromChar(0) may return a negative X coordinate if the beginning of the + // text has scrolled off the edit, so don't go past the clip rect's edge. + RECT r; + GetRect(&r); + const int left_bound = std::max(r.left, PosFromChar(0).x); + if (x < left_bound) + return left_bound; + + // See if we need to clip to the right edge of the text. + const int length = GetTextLength(); + // Asking for the coordinate of any character past the end of the text gets + // the pixel just to the right of the last character. + const int right_bound = std::min(r.right, PosFromChar(length).x); + if ((length == 0) || (x < right_bound)) + return x; + + // For trailing characters that are 2 pixels wide of less (like "l" in some + // fonts), we have a problem: + // * Clicks on any pixel within the character will place the cursor before + // the character. + // * Clicks on the pixel just after the character will not allow triple- + // click to work properly (true for any last character width). + // So, we move to the last pixel of the character when this is a + // triple-click, and moving to one past the last pixel in all other + // scenarios. This way, all clicks that can move the cursor will place it at + // the end of the text, but triple-click will still work. + return is_triple_click ? (right_bound - 1) : right_bound; +} + +void AutocompleteEdit::EmphasizeURLComponents() { + ITextDocument* const text_object_model = GetTextObjectModel(); + ScopedFreeze freeze(this, text_object_model); + ScopedSuspendUndo suspend_undo(text_object_model); + + // Save the selection. + CHARRANGE saved_sel; + GetSelection(saved_sel); + + // See whether the contents are a URL with a non-empty host portion, which we + // should emphasize. + url_parse::Parsed parts; + AutocompleteInput::Parse(GetText(), GetDesiredTLD(), &parts, NULL); + bool emphasize = (parts.host.len > 0); + // If !user_input_in_progress_, the permanent text is showing, which should + // always be a URL, so no further checking is needed. By avoiding checking in + // this case, we avoid calling into the autocomplete providers, and thus + // initializing the history system, as long as possible, which speeds startup. + if (user_input_in_progress_) { + // Rather than using the type returned by Parse(), key off the desired page + // transition for this input since that can tell us whether an UNKNOWN input + // string is going to be treated as a search or a navigation. This is the + // same method the Paste And Go system uses. + PageTransition::Type transition = PageTransition::LINK; + GetURLForCurrentText(&transition, NULL, NULL); + if (transition != PageTransition::TYPED) + emphasize = false; + } + + // Set the baseline emphasis. + CHARFORMAT cf = {0}; + cf.dwMask = CFM_COLOR; + cf.dwEffects = 0; + cf.crTextColor = (emphasize ? GetSysColor(COLOR_GRAYTEXT) : + GetSysColor(COLOR_WINDOWTEXT)); + SelectAll(false); + SetSelectionCharFormat(cf); + + if (emphasize) { + // We've found a host name, give it more emphasis. + cf.crTextColor = GetSysColor(COLOR_WINDOWTEXT); + SetSelection(parts.host.begin, parts.host.end()); + SetSelectionCharFormat(cf); + } + + // Emphasize the scheme for security UI display purposes (if necessary). + insecure_scheme_component_.reset(); + if (!user_input_in_progress_ && parts.scheme.is_nonempty() && + ((scheme_security_level_ == ToolbarModel::SECURE) || + (scheme_security_level_ == ToolbarModel::INSECURE))) { + if (scheme_security_level_ == ToolbarModel::SECURE) { + cf.crTextColor = kSecureSchemeColor; + } else { + insecure_scheme_component_.begin = parts.scheme.begin; + insecure_scheme_component_.len = parts.scheme.len; + cf.crTextColor = kInsecureSchemeColor; + } + SetSelection(parts.scheme.begin, parts.scheme.end()); + SetSelectionCharFormat(cf); + } + + // Restore the selection. + SetSelectionRange(saved_sel); +} + +void AutocompleteEdit::EraseTopOfSelection(CDC* dc, + const CRect& client_rect, + const CRect& paint_clip_rect) { + // Find the area we care about painting. We could calculate the rect + // containing just the selected portion, but there's no harm in simply erasing + // the whole top of the client area, and at least once I saw us manage to + // select the "phantom newline" briefly, which looks very weird if not clipped + // off at the same height. + CRect erase_rect(client_rect.left, client_rect.top, client_rect.right, + client_rect.top + font_y_adjustment_); + erase_rect.IntersectRect(erase_rect, paint_clip_rect); + + // Erase to the background color. + if (!erase_rect.IsRectNull()) + dc->FillSolidRect(&erase_rect, background_color_); +} + +void AutocompleteEdit::DrawSlashForInsecureScheme( + HDC hdc, + const CRect& client_rect, + const CRect& paint_clip_rect) { + DCHECK(insecure_scheme_component_.is_nonempty()); + + // Calculate the rect, in window coordinates, containing the portion of the + // scheme where we'll be drawing the slash. Vertically, we draw across one + // x-height of text, plus an additional 3 stroke diameters (the stroke width + // plus a half-stroke width of space between the stroke and the text, both + // above and below the text). + const int font_top = client_rect.top + font_y_adjustment_; + const SkScalar kStrokeWidthPixels = SkIntToScalar(2); + const int kAdditionalSpaceOutsideFont = + static_cast<int>(ceil(kStrokeWidthPixels * 1.5f)); + const CRect scheme_rect(PosFromChar(insecure_scheme_component_.begin).x, + font_top + font_ascent_ - font_x_height_ - + kAdditionalSpaceOutsideFont, + PosFromChar(insecure_scheme_component_.end()).x, + font_top + font_ascent_ + + kAdditionalSpaceOutsideFont); + + // Clip to the portion we care about and translate to canvas coordinates + // (see the canvas creation below) for use later. + CRect canvas_clip_rect, canvas_paint_clip_rect; + canvas_clip_rect.IntersectRect(scheme_rect, client_rect); + canvas_paint_clip_rect.IntersectRect(canvas_clip_rect, paint_clip_rect); + if (canvas_paint_clip_rect.IsRectNull()) + return; // We don't need to paint any of this region, so just bail early. + canvas_clip_rect.OffsetRect(-scheme_rect.left, -scheme_rect.top); + canvas_paint_clip_rect.OffsetRect(-scheme_rect.left, -scheme_rect.top); + + // Create a paint context for drawing the antialiased stroke. + SkPaint paint; + paint.setAntiAlias(true); + paint.setStrokeWidth(kStrokeWidthPixels); + paint.setStrokeCap(SkPaint::kRound_Cap); + + // Create a canvas as large as |scheme_rect| to do our drawing, and initialize + // it to fully transparent so any antialiasing will look nice when painted + // atop the edit. + ChromeCanvas canvas(scheme_rect.Width(), scheme_rect.Height(), false); + // TODO (jcampan): This const_cast should not be necessary once the SKIA + // API has been changed to return a non-const bitmap. + (const_cast<SkBitmap&>(canvas.getDevice()->accessBitmap(true))). + eraseARGB(0, 0, 0, 0); + + // Calculate the start and end of the stroke, which are just the lower left + // and upper right corners of the canvas, inset by the radius of the endcap + // so we don't clip the endcap off. + const SkScalar kEndCapRadiusPixels = kStrokeWidthPixels / SkIntToScalar(2); + const SkPoint start_point = { + kEndCapRadiusPixels, + SkIntToScalar(scheme_rect.Height()) - kEndCapRadiusPixels }; + const SkPoint end_point = { + SkIntToScalar(scheme_rect.Width()) - kEndCapRadiusPixels, + kEndCapRadiusPixels }; + + // Calculate the selection rectangle in canvas coordinates, which we'll use + // to clip the stroke so we can draw the unselected and selected portions. + CHARRANGE sel; + GetSel(sel); + const SkRect selection_rect = { + SkIntToScalar(PosFromChar(sel.cpMin).x - scheme_rect.left), + SkIntToScalar(0), + SkIntToScalar(PosFromChar(sel.cpMax).x - scheme_rect.left), + SkIntToScalar(scheme_rect.Height()) }; + + // Draw the unselected portion of the stroke. + canvas.save(); + if (selection_rect.isEmpty() || + canvas.clipRect(selection_rect, SkRegion::kDifference_Op)) { + paint.setColor(kSchemeStrikeoutColor); + canvas.drawLine(start_point.fX, start_point.fY, + end_point.fX, end_point.fY, paint); + } + canvas.restore(); + + // Draw the selected portion of the stroke. + if (!selection_rect.isEmpty() && canvas.clipRect(selection_rect)) { + paint.setColor(kSchemeSelectedStrikeoutColor); + canvas.drawLine(start_point.fX, start_point.fY, + end_point.fX, end_point.fY, paint); + } + + // Now copy what we drew to the target HDC. + canvas.getTopPlatformDevice().drawToHDC(hdc, + scheme_rect.left + canvas_paint_clip_rect.left - canvas_clip_rect.left, + std::max(scheme_rect.top, client_rect.top) + canvas_paint_clip_rect.top - + canvas_clip_rect.top, &canvas_paint_clip_rect); +} + +void AutocompleteEdit::DrawDropHighlight(HDC hdc, + const CRect& client_rect, + const CRect& paint_clip_rect) { + DCHECK(drop_highlight_position_ != -1); + + const int highlight_y = client_rect.top + font_y_adjustment_; + const int highlight_x = PosFromChar(drop_highlight_position_).x - 1; + const CRect highlight_rect(highlight_x, + highlight_y, + highlight_x + 1, + highlight_y + font_ascent_ + font_descent_); + + // Clip the highlight to the region being painted. + CRect clip_rect; + clip_rect.IntersectRect(highlight_rect, paint_clip_rect); + if (clip_rect.IsRectNull()) + return; + + HGDIOBJ last_pen = SelectObject(hdc, CreatePen(PS_SOLID, 1, RGB(0, 0, 0))); + MoveToEx(hdc, clip_rect.left, clip_rect.top, NULL); + LineTo(hdc, clip_rect.left, clip_rect.bottom); + DeleteObject(SelectObject(hdc, last_pen)); +} + +std::wstring AutocompleteEdit::GetDesiredTLD() const { + return (control_key_state_ == DOWN_WITHOUT_CHANGE) ? L"com" : L""; +} + +void AutocompleteEdit::UpdatePopup() { + ScopedFreeze freeze(this, GetTextObjectModel()); + SetInputInProgress(true); + + if (!has_focus_) { + // When we're in the midst of losing focus, don't rerun autocomplete. This + // can happen when losing focus causes the IME to cancel/finalize a + // composition. We still want to note that user input is in progress, we + // just don't want to do anything else. + // + // Note that in this case the ScopedFreeze above was unnecessary; however, + // we're inside the callstack of OnKillFocus(), which has already frozen the + // edit, so this will never result in an unnecessary UpdateWindow() call. + return; + } + + // Figure out whether the user is trying to compose something in an IME. + bool ime_composing = false; + HIMC context = ImmGetContext(m_hWnd); + if (context) { + ime_composing = !!ImmGetCompositionString(context, GCS_COMPSTR, NULL, 0); + ImmReleaseContext(m_hWnd, context); + } + + // Don't inline autocomplete when: + // * The user is deleting text + // * The caret/selection isn't at the end of the text + // * The user has just pasted in something that replaced all the text + // * The user is trying to compose something in an IME + CHARRANGE sel; + GetSel(sel); + popup_->StartAutocomplete(user_text_, GetDesiredTLD(), + just_deleted_text_ || (sel.cpMax < GetTextLength()) || + (paste_state_ != NONE) || ime_composing); +} + +void AutocompleteEdit::TextChanged() { + ScopedFreeze freeze(this, GetTextObjectModel()); + EmphasizeURLComponents(); + controller_->OnChanged(); +} + +std::wstring AutocompleteEdit::DisplayTextFromUserText( + const std::wstring& text) const { + return (is_keyword_hint_ || keyword_.empty()) ? + text : KeywordProvider::SplitReplacementStringFromInput(text); +} + +std::wstring AutocompleteEdit::UserTextFromDisplayText( + const std::wstring& text) const { + return (is_keyword_hint_ || keyword_.empty()) ? + text : (keyword_ + L" " + text); +} + +std::wstring AutocompleteEdit::GetClipboardText() const { + // Try text format. + ClipboardService* clipboard = g_browser_process->clipboard_service(); + if (clipboard->IsFormatAvailable(CF_UNICODETEXT)) { + std::wstring text; + clipboard->ReadText(&text); + + // Note: Unlike in the find popup and textfield view, here we completely + // remove whitespace strings containing newlines. We assume users are + // most likely pasting in URLs that may have been split into multiple + // lines in terminals, email programs, etc., and so linebreaks indicate + // completely bogus whitespace that would just cause the input to be + // invalid. + return CollapseWhitespace(text, true); + } + + // Try bookmark format. + // + // It is tempting to try bookmark format first, but the URL we get out of a + // bookmark has been cannonicalized via GURL. This means if a user copies + // and pastes from the URL bar to itself, the text will get fixed up and + // cannonicalized, which is not what the user expects. By pasting in this + // order, we are sure to paste what the user copied. + if (clipboard->IsFormatAvailable(ClipboardUtil::GetUrlWFormat()->cfFormat)) { + std::string url_str; + clipboard->ReadBookmark(NULL, &url_str); + // pass resulting url string through GURL to normalize + GURL url(url_str); + if (url.is_valid()) + return UTF8ToWide(url.spec()); + } + + return std::wstring(); +} + +bool AutocompleteEdit::CanPasteAndGo(const std::wstring& text) const { + if (popup_window_mode_) + return false; + + // Reset local state. + paste_and_go_url_.clear(); + paste_and_go_transition_ = PageTransition::TYPED; + paste_and_go_alternate_nav_url_.clear(); + + // See if the clipboard text can be parsed. + const AutocompleteInput input(text, std::wstring(), true); + if (input.type() == AutocompleteInput::INVALID) + return false; + + // Ask the controller what do do with this input. + paste_and_go_controller->SetProfile(profile_); + // This is cheap, and since there's one + // paste_and_go_controller for many tabs which + // may all have different profiles, it ensures + // we're always using the right one. + const bool done = paste_and_go_controller->Start(input, false, true); + DCHECK(done); + AutocompleteResult result; + paste_and_go_controller->GetResult(&result); + if (result.empty()) + return false; + + // Set local state based on the default action for this input. + result.SetDefaultMatch(AutocompleteResult::Selection()); + const AutocompleteResult::const_iterator match(result.default_match()); + DCHECK(match != result.end()); + paste_and_go_url_ = match->destination_url; + paste_and_go_transition_ = match->transition; + paste_and_go_alternate_nav_url_ = result.GetAlternateNavURL(input, match); + + return !paste_and_go_url_.empty(); +} + +void AutocompleteEdit::PasteAndGo() { + // The final parameter to OpenURL, keyword, is not quite correct here: it's + // possible to "paste and go" a string that contains a keyword. This is + // enough of an edge case that we ignore this possibility. + RevertAll(); + OpenURL(paste_and_go_url_, CURRENT_TAB, paste_and_go_transition_, + paste_and_go_alternate_nav_url_, AutocompletePopup::kNoMatch, + std::wstring()); +} + +void AutocompleteEdit::SetInputInProgress(bool in_progress) { + if (user_input_in_progress_ == in_progress) + return; + + user_input_in_progress_ = in_progress; + controller_->OnInputInProgress(in_progress); +} + +void AutocompleteEdit::SendOpenNotification(size_t selected_line, + const std::wstring& keyword) { + // We only care about cases where there is a selection (i.e. the popup is + // open). + if (popup_->is_open()) { + scoped_ptr<AutocompleteLog> log(popup_->GetAutocompleteLog()); + if (selected_line != AutocompletePopup::kNoMatch) + log->selected_index = selected_line; + else if (!has_temporary_text_) + log->inline_autocompleted_length = inline_autocomplete_text_.length(); + NotificationService::current()->Notify( + NOTIFY_OMNIBOX_OPENED_URL, Source<Profile>(profile_), + Details<AutocompleteLog>(log.get())); + } + + TemplateURLModel* template_url_model = profile_->GetTemplateURLModel(); + if (keyword.empty() || !template_url_model) + return; + + const TemplateURL* const template_url = + template_url_model->GetTemplateURLForKeyword(keyword); + if (template_url) { + UserMetrics::RecordAction(L"AcceptedKeyword", profile_); + template_url_model->IncrementUsageCount(template_url); + } + + // NOTE: We purposefully don't increment the usage count of the default search + // engine, if applicable; see comments in template_url.h. +} + +ITextDocument* AutocompleteEdit::GetTextObjectModel() const { + if (!text_object_model_) { + // This is lazily initialized, instead of being initialized in the + // constructor, in order to avoid hurting startup performance. + CComPtr<IRichEditOle> ole_interface; + ole_interface.Attach(GetOleInterface()); + text_object_model_ = ole_interface; + } + return text_object_model_; +} + +void AutocompleteEdit::StartDragIfNecessary(const CPoint& point) { + if (initiated_drag_ || !win_util::IsDrag(mouse_down_point_, point)) + return; + + scoped_refptr<OSExchangeData> data = new OSExchangeData; + + DWORD supported_modes = DROPEFFECT_COPY; + + CHARRANGE sel; + GetSelection(sel); + + // We're about to start a drag session, but the edit is expecting a mouse up + // that it uses to reset internal state. If we don't send a mouse up now, + // when the mouse moves back into the edit the edit will reset the selection. + // So, we send the event now which resets the selection. We then restore the + // selection and start the drag. We always send lbuttonup as otherwise we + // might trigger a context menu (right up). This seems scary, but doesn't + // seem to cause problems. + { + ScopedFreeze freeze(this, GetTextObjectModel()); + DefWindowProc(WM_LBUTTONUP, 0, + MAKELPARAM(mouse_down_point_.x, mouse_down_point_.y)); + SetSelectionRange(sel); + } + + const std::wstring start_text(GetText()); + if (IsSelectAll(sel)) { + // All the text is selected, export as URL. + const std::wstring url(GetURLForCurrentText(NULL, NULL, NULL)); + std::wstring title; + SkBitmap favicon; + if (url == permanent_text_) { + title = controller_->GetTitle(); + favicon = controller_->GetFavIcon(); + } + const GURL gurl(url); + drag_utils::SetURLAndDragImage(gurl, title, favicon, data.get()); + data->SetURL(gurl, title); + supported_modes |= DROPEFFECT_LINK; + UserMetrics::RecordAction(L"Omnibox_DragURL", profile_); + } else { + supported_modes |= DROPEFFECT_MOVE; + UserMetrics::RecordAction(L"Omnibox_DragString", profile_); + } + + data->SetString(GetSelectedText()); + + scoped_refptr<BaseDragSource> drag_source(new BaseDragSource); + DWORD dropped_mode; + in_drag_ = true; + if (DoDragDrop(data, drag_source, supported_modes, &dropped_mode) == + DRAGDROP_S_DROP) { + if ((dropped_mode == DROPEFFECT_MOVE) && (start_text == GetText())) { + ScopedFreeze freeze(this, GetTextObjectModel()); + OnBeforePossibleChange(); + SetSelectionRange(sel); + ReplaceSel(L"", true); + OnAfterPossibleChange(); + } + // else case, not a move or it was a move and the drop was on us. + // If the drop was on us, EditDropTarget took care of the move so that + // we don't have to delete the text. + possible_drag_ = false; + } else { + // Drag was canceled or failed. The mouse may still be down and + // over us, in which case we need possible_drag_ to remain true so + // that we don't forward mouse move events to the edit which will + // start another drag. + // + // NOTE: we didn't use mouse capture during the mouse down as DoDragDrop + // does its own capture. + CPoint cursor_location; + GetCursorPos(&cursor_location); + + CRect client_rect; + GetClientRect(&client_rect); + + CPoint client_origin_on_screen(client_rect.left, client_rect.top); + ClientToScreen(&client_origin_on_screen); + client_rect.MoveToXY(client_origin_on_screen.x, + client_origin_on_screen.y); + possible_drag_ = (client_rect.PtInRect(cursor_location) && + ((GetKeyState(VK_LBUTTON) != 0) || + (GetKeyState(VK_MBUTTON) != 0) || + (GetKeyState(VK_RBUTTON) != 0))); + } + + in_drag_ = false; + initiated_drag_ = true; + tracking_click_ = false; +} + +void AutocompleteEdit::OnPossibleDrag(const CPoint& point) { + if (possible_drag_) + return; + + mouse_down_point_ = point; + initiated_drag_ = false; + + CHARRANGE selection; + GetSel(selection); + if (selection.cpMin != selection.cpMax) { + const POINT min_sel_location(PosFromChar(selection.cpMin)); + const POINT max_sel_location(PosFromChar(selection.cpMax)); + // NOTE: we don't consider the y location here as we always pass a + // y-coordinate in the middle to the default handler which always triggers + // a drag regardless of the y-coordinate. + possible_drag_ = (point.x >= min_sel_location.x) && + (point.x < max_sel_location.x); + } +} + +void AutocompleteEdit::UpdateDragDone(UINT keys) { + possible_drag_ = (possible_drag_ && + ((keys & (MK_LBUTTON | MK_MBUTTON | MK_RBUTTON)) != 0)); +} + +void AutocompleteEdit::RepaintDropHighlight(int position) { + if ((position != -1) && (position <= GetTextLength())) { + const POINT min_loc(PosFromChar(position)); + const RECT highlight_bounds = {min_loc.x - 1, font_y_adjustment_, + min_loc.x + 2, font_ascent_ + font_descent_ + font_y_adjustment_}; + InvalidateRect(&highlight_bounds, false); + } +} diff --git a/chrome/browser/autocomplete/autocomplete_edit.h b/chrome/browser/autocomplete/autocomplete_edit.h new file mode 100644 index 0000000..9f3ecca --- /dev/null +++ b/chrome/browser/autocomplete/autocomplete_edit.h @@ -0,0 +1,813 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#ifndef CHROME_BROWSER_AUTOCOMPLETE_AUTOCOMPLETE_EDIT_H__ +#define CHROME_BROWSER_AUTOCOMPLETE_AUTOCOMPLETE_EDIT_H__ + +#include <atlbase.h> +#include <atlapp.h> +#include <atlcomcli.h> +#include <atlctrls.h> +#include <oleacc.h> +#include <tom.h> // For ITextDocument, a COM interface to CRichEditCtrl + +#include "base/iat_patch.h" +#include "base/scoped_ptr.h" +#include "chrome/browser/autocomplete/autocomplete_popup.h" +#include "chrome/browser/security_style.h" +#include "chrome/browser/toolbar_model.h" +#include "chrome/common/gfx/chrome_font.h" +#include "chrome/views/menu.h" + +class CommandController; +class Profile; +class TabContents; + +// Provides the implementation of an edit control with a drop-down +// autocomplete box. The box itself is implemented in autocomplete_popup.cc +// This file implements the edit box and management for the popup. +// +// This implementation is currently appropriate for the URL bar, where the +// autocomplete dropdown is always displayed because there is always a +// default item. For web page autofill and other applications, this is +// probably not appropriate. We may want to add a flag to determine which +// of these modes we're in. +class AutocompleteEdit + : public CWindowImpl<AutocompleteEdit, + CRichEditCtrl, + CWinTraits<WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL | + ES_NOHIDESEL> >, + public CRichEditCommands<AutocompleteEdit>, + public Menu::Delegate { + public: + DECLARE_WND_CLASS(L"Chrome_AutocompleteEdit"); + + // Embedders of this control must implement this class. + class Controller { + public: + // When the user presses enter or selects a line with the mouse, this + // function will get called synchronously with the url to open and + // disposition and transition to use when opening it. + // + // |alternate_nav_url|, if non-empty, contains the alternate navigation URL + // for |url|, which the controller can check for existence. See comments on + // AutocompleteResult::GetAlternateNavURL(). + virtual void OnAutocompleteAccept(const std::wstring& url, + WindowOpenDisposition disposition, + PageTransition::Type transition, + const std::wstring& alternate_nav_url) = 0; + + // Called when anything has changed that might affect the layout or contents + // of the views around the edit, including the text of the edit and the + // status of any keyword- or hint-related state. + virtual void OnChanged() = 0; + + // Called whenever the user starts or stops an input session (typing, + // interacting with the edit, etc.). When user input is not in progress, + // the edit is guaranteed to be showing the permanent text. + virtual void OnInputInProgress(bool in_progress) = 0; + + // Returns the favicon of the current page. + virtual SkBitmap GetFavIcon() const = 0; + + // Returns the title of the current page. + virtual std::wstring GetTitle() const = 0; + }; + + // The State struct contains enough information about the AutocompleteEdit to + // save/restore a user's typing, caret position, etc. across tab changes. We + // explicitly don't preserve things like whether the popup was open as this + // might be weird. + struct State { + State(const CHARRANGE& selection, + const CHARRANGE& saved_selection_for_focus_change, + bool user_input_in_progress, + const std::wstring& user_text, + const AutocompleteResult::Selection& manually_selected_match, + const std::wstring& keyword, + bool is_keyword_hint, + bool disable_keyword_ui, + bool show_search_hint) + : selection(selection), + saved_selection_for_focus_change(saved_selection_for_focus_change), + user_input_in_progress(user_input_in_progress), + user_text(user_text), + manually_selected_match(manually_selected_match), + keyword(keyword), + is_keyword_hint(is_keyword_hint), + disable_keyword_ui(disable_keyword_ui), + show_search_hint(show_search_hint) { + } + + const CHARRANGE selection; + const CHARRANGE saved_selection_for_focus_change; + bool user_input_in_progress; + const std::wstring user_text; + AutocompleteResult::Selection manually_selected_match; + const std::wstring keyword; + const bool is_keyword_hint; + const bool disable_keyword_ui; + const bool show_search_hint; + }; + + // The given observer is notified when the user selects an item + AutocompleteEdit(const ChromeFont& font, + Controller* controller, + ToolbarModel* model, + ChromeViews::View* parent_view, + HWND hwnd, + Profile* profile, + CommandController* command_controller, + bool popup_window_mode); + ~AutocompleteEdit(); + + // Called when any LocationBarView state changes. If + // |tab_for_state_restoring| is non-NULL, it points to a TabContents whose + // state we should restore. + void Update(const TabContents* tab_for_state_restoring); + + // Invoked when the profile has changed. + void SetProfile(Profile* profile); + + // For use when switching tabs, this saves the current state onto the tab so + // that it can be restored during a later call to Update(). + void SaveStateToTab(TabContents* tab); + + // Returns the current text of the edit control, which could be the + // "temporary" text set by the popup, the "permanent" text set by the + // browser, or just whatever the user has currently typed. + std::wstring GetText() const; + + // Returns the URL. If the user has not edited the text, this returns the + // permanent text. If the user has edited the text, this returns the default + // match based on the current text, which may be a search URL, or keyword + // generated URL. + // + // See AutocompleteEdit for a description of the args (they may be null if + // not needed). + std::wstring GetURLForCurrentText(PageTransition::Type* transition, + bool* is_history_what_you_typed_match, + std::wstring* alternate_nav_url); + + // The user text is the text the user has manually keyed in. When present, + // this is shown in preference to the permanent text; hitting escape will + // revert to the permanent text. + void SetUserText(const std::wstring& text) { SetUserText(text, text, true); } + + // Selects all the text in the edit. Use this in place of SetSelAll() to + // avoid selecting the "phantom newline" at the end of the edit. + void SelectAll(bool reversed); + + // Reverts the edit and popup back to their unedited state (permanent text + // showing, popup closed, no user input in progress). + void RevertAll(); + + // Asks the browser to load the popup's currently selected item, using the + // supplied disposition. This may close the popup. If |for_drop| is true, + // it indicates the input is being accepted as part of a drop operation and + // the transition should be treated as LINK (so that it won't trigger the + // URL to be autocompleted). + void AcceptInput(WindowOpenDisposition disposition, + bool for_drop); + + // Asks the browser to load the specified URL, which is assumed to be one of + // the popup entries, using the supplied disposition and transition type. + // |alternate_nav_url|, if non-empty, contains the alternate navigation URL + // for |url|. See comments on AutocompleteResult::GetAlternateNavURL(). + // + // |selected_line| is passed to AutocompletePopup::LogOpenedURL(); see + // comments there. + // + // If the URL was expanded from a keyword, |keyword| is that keyword. + // + // This may close the popup. + void OpenURL(const std::wstring& url, + WindowOpenDisposition disposition, + PageTransition::Type transition, + const std::wstring& alternate_nav_url, + size_t selected_line, + const std::wstring& keyword); + + // Closes the autocomplete popup, if it's open. + void ClosePopup(); + + // Accessors for keyword-related state (see comments on keyword_ and + // is_keyword_hint_). + std::wstring keyword() const { + return ((is_keyword_hint_ && has_focus_) || + (!is_keyword_hint_ && !disable_keyword_ui_)) ? + keyword_ : std::wstring(); + } + bool is_keyword_hint() const { return is_keyword_hint_; } + + // True if we should show the "Type to search" hint (see comments on + // show_search_hint_). + bool show_search_hint() const { return has_focus_ && show_search_hint_; } + + ChromeViews::View* parent_view() const { return parent_view_; } + + // Returns true if a query to an autocomplete provider is currently + // in progress. This logic should in the future live in + // AutocompleteController but resides here for now. This method is used by + // AutomationProvider::AutocompleteEditIsQueryInProgress. + bool query_in_progress() const { return popup_->query_in_progress(); } + + // Returns the lastest autocomplete results. This logic should in the future + // live in AutocompleteController but resides here for now. This method is + // used by AutomationProvider::AutocompleteEditGetMatches. + const AutocompleteResult* latest_result() const { + return popup_->latest_result(); + } + + // Exposes custom IAccessible implementation to the overall MSAA hierarchy. + IAccessible* GetIAccessible(); + + void SetDropHighlightPosition(int position); + int drop_highlight_position() const { return drop_highlight_position_; } + + // Returns true if a drag a drop session was initiated by this edit. + bool in_drag() const { return in_drag_; } + + // Moves the selected text to the specified position. + void MoveSelectedText(int new_position); + + // Inserts the text at the specified position. + void InsertText(int position, const std::wstring& text); + + // Invokes CanPasteAndGo with the specified text, and if successful navigates + // to the appropriate URL. The behavior of this is the same as if the user + // typed in the specified text and pressed enter. + void PasteAndGo(const std::wstring& text); + + // Called before an accelerator is processed to give us a chance to override + // it. + bool OverrideAccelerator(const ChromeViews::Accelerator& accelerator); + + // Handler for external events passed in to us. The View that owns us may + // send us events that we should treat as if they were events on us. + void HandleExternalMsg(UINT msg, UINT flags, const CPoint& screen_point); + + // Called back by the AutocompletePopup when any relevant data changes. This + // rolls together several separate pieces of data into one call so we can + // update all the UI efficiently: + // |text| is either the new temporary text (if |is_temporary_text| is true) + // from the user manually selecting a different match, or the inline + // autocomplete text (if |is_temporary_text| is false). + // |previous_selected_match| is only used when changing the temporary text; + // it is the match that was (manually or automatically) selected before + // the current manual selection, and is saved to be restored later if the + // user hits <esc>. + // |can_show_search_hint| is true if the current choice is nonexistent or a + // search result; in these cases it may be OK to show the "Type to search" + // hint (see comments on show_search_hint_). + // |keyword| is the keyword to show a hint for if |is_keyword_hint| is true, + // or the currently selected keyword if |is_keyword_hint| is false (see + // comments on keyword_ and is_keyword_hint_). + void OnPopupDataChanged( + const std::wstring& text, + bool is_temporary_text, + const AutocompleteResult::Selection& previous_selected_match, + const std::wstring& keyword, + bool is_keyword_hint, + bool can_show_search_hint); + + // CWindowImpl + BEGIN_MSG_MAP(AutocompleteEdit) + MSG_WM_CHAR(OnChar) + MSG_WM_CONTEXTMENU(OnContextMenu) + MSG_WM_COPY(OnCopy) + MSG_WM_CUT(OnCut) + MESSAGE_HANDLER_EX(WM_GETOBJECT, OnGetObject) + MESSAGE_HANDLER_EX(WM_IME_COMPOSITION, OnImeComposition) + MSG_WM_KEYDOWN(OnKeyDown) + MSG_WM_KEYUP(OnKeyUp) + MSG_WM_KILLFOCUS(OnKillFocus) + MSG_WM_LBUTTONDBLCLK(OnLButtonDblClk) + MSG_WM_LBUTTONDOWN(OnLButtonDown) + MSG_WM_LBUTTONUP(OnLButtonUp) + MSG_WM_MBUTTONDOWN(OnNonLButtonDown) + MSG_WM_MBUTTONUP(OnNonLButtonUp) + MSG_WM_MOUSEACTIVATE(OnMouseActivate) + MSG_WM_MOUSEMOVE(OnMouseMove) + MSG_WM_PAINT(OnPaint) + MSG_WM_PASTE(OnPaste) + MSG_WM_RBUTTONDOWN(OnNonLButtonDown) + MSG_WM_RBUTTONUP(OnNonLButtonUp) + MSG_WM_SETFOCUS(OnSetFocus) + MSG_WM_SYSCHAR(OnSysChar) // WM_SYSxxx == WM_xxx with ALT down + MSG_WM_SYSKEYDOWN(OnKeyDown) + MSG_WM_SYSKEYUP(OnKeyUp) + DEFAULT_REFLECTION_HANDLER() // avoids black margin area + END_MSG_MAP() + + // Menu::Delegate + virtual bool IsCommandEnabled(int id) const; + virtual bool GetContextualLabel(int id, std::wstring* out) const; + virtual void ExecuteCommand(int id); + + private: + // This object freezes repainting of the edit until the object is destroyed. + // Some methods of the CRichEditCtrl draw synchronously to the screen. If we + // don't freeze, the user will see a rapid series of calls to these as + // flickers. + // + // Freezing the control while it is already frozen is permitted; the control + // will unfreeze once both freezes are released (the freezes stack). + class ScopedFreeze { + public: + ScopedFreeze(AutocompleteEdit* edit, ITextDocument* text_object_model); + ~ScopedFreeze(); + + private: + AutocompleteEdit* const edit_; + ITextDocument* const text_object_model_; + + DISALLOW_EVIL_CONSTRUCTORS(ScopedFreeze); + }; + + // This object suspends placing any operations on the edit's undo stack until + // the object is destroyed. If we don't do this, some of the operations we + // perform behind the user's back will be undoable by the user, which feels + // bizarre and confusing. + class ScopedSuspendUndo { + public: + explicit ScopedSuspendUndo(ITextDocument* text_object_model); + ~ScopedSuspendUndo(); + + private: + ITextDocument* const text_object_model_; + + DISALLOW_EVIL_CONSTRUCTORS(ScopedSuspendUndo); + }; + + enum ControlKeyState { + UP, // The control key is not depressed. + DOWN_WITHOUT_CHANGE, // The control key is depressed, and the edit's + // contents/selection have not changed since it was + // depressed. This is the only state in which we + // do the "ctrl-enter" behavior when the user hits + // enter. + DOWN_WITH_CHANGE, // The control key is depressed, and the edit's + // contents/selection have changed since it was + // depressed. If the user now hits enter, we assume + // he simply hasn't released the key, rather than that + // he intended to hit "ctrl-enter". + }; + + enum PasteState { + NONE, // Most recent edit was not a paste that replaced all text. + REPLACED_ALL, // Most recent edit was a paste that replaced all text. + REPLACING_ALL, // In the middle of doing a paste that replaces all + // text. We need this intermediate state because OnPaste() + // does the actual detection of such pastes, but + // OnAfterPossibleChange() has to update the paste state + // for every edit. If OnPaste() set the state directly to + // REPLACED_ALL, OnAfterPossibleChange() wouldn't know + // whether that represented the current edit or a past one. + }; + + // Replacement word-breaking proc for the rich edit control. + static int CALLBACK WordBreakProc(LPTSTR edit_text, + int current_pos, + int num_bytes, + int action); + + // Returns true if |edit_text| starting at |current_pos| is "://". + static bool SchemeEnd(LPTSTR edit_text, int current_pos, int length); + + // Intercepts. See OnPaint(). + static HDC WINAPI BeginPaintIntercept(HWND hWnd, LPPAINTSTRUCT lpPaint); + static BOOL WINAPI EndPaintIntercept(HWND hWnd, CONST PAINTSTRUCT* lpPaint); + + // Message handlers + void OnChar(TCHAR ch, UINT repeat_count, UINT flags); + void OnContextMenu(HWND window, const CPoint& point); + void OnCopy(); + void OnCut(); + LRESULT OnGetObject(UINT uMsg, WPARAM wparam, LPARAM lparam); + LRESULT OnImeComposition(UINT message, WPARAM wparam, LPARAM lparam); + void OnKeyDown(TCHAR key, UINT repeat_count, UINT flags); + void OnKeyUp(TCHAR key, UINT repeat_count, UINT flags); + void OnKillFocus(HWND focus_wnd); + void OnLButtonDblClk(UINT keys, const CPoint& point); + void OnLButtonDown(UINT keys, const CPoint& point); + void OnLButtonUp(UINT keys, const CPoint& point); + LRESULT OnMouseActivate(HWND window, UINT hit_test, UINT mouse_message); + void OnMouseMove(UINT keys, const CPoint& point); + void OnNonLButtonDown(UINT keys, const CPoint& point); + void OnNonLButtonUp(UINT keys, const CPoint& point); + void OnPaint(HDC bogus_hdc); + void OnPaste(); + void OnSetFocus(HWND focus_wnd); + void OnSysChar(TCHAR ch, UINT repeat_count, UINT flags); + + // Helper function for OnChar() and OnKeyDown() that handles keystrokes that + // could change the text in the edit. + void HandleKeystroke(UINT message, TCHAR key, UINT repeat_count, UINT flags); + + // Helper functions for OnKeyDown() that handle accelerators applicable when + // we're not read-only and all the time, respectively. These return true if + // they handled the key. + bool OnKeyDownOnlyWritable(TCHAR key, UINT repeat_count, UINT flags); + bool OnKeyDownAllModes(TCHAR key, UINT repeat_count, UINT flags); + + // Every piece of code that can change the edit should call these functions + // before and after the change. These functions determine if anything + // meaningful changed, and do any necessary updating and notification. + void OnBeforePossibleChange(); + // OnAfterPossibleChange() returns true if there was a change that caused it + // to call UpdatePopup(). + bool OnAfterPossibleChange(); + + // Implementation for SetUserText(wstring). SetUserText(wstring) invokes this + // with |update_popup| set to false. + void SetUserText(const std::wstring& text, const std::wstring& display_text, + bool update_popup); + + // The inline autocomplete text is shown, selected, appended to the user's + // current input. For example, if the user types in "go", and the + // autocomplete system determines that the best match is "google", this + // routine might be called with |text| = "ogle". + // Returns true if the text in the edit changed. + bool SetInlineAutocompleteText(const std::wstring& text); + + // The temporary text is set when the user arrows around the autocomplete + // popup. When present, this is shown in preference to the user text; + // hitting escape will revert to the user text. + // |previous_selected_match| is used to update the member variable + // |original_selected_match_| if there wasn't already any temporary text. + // Returns true if the text in the edit changed. + bool SetTemporaryText( + const std::wstring& text, + const AutocompleteResult::Selection& previous_selected_match); + + // Called whenever user_text_ should change. + void InternalSetUserText(const std::wstring& text); + + // Like GetSel(), but returns a range where |cpMin| will be larger than + // |cpMax| if the cursor is at the start rather than the end of the selection + // (in other words, tracks selection direction as well as offsets). + // Note the non-Google-style "non-const-ref" argument, which matches GetSel(). + void GetSelection(CHARRANGE& sel) const; + + // Returns the currently selected text of the edit control. + std::wstring GetSelectedText() const; + + // Like SetSel(), but respects the selection direction implied by |start| and + // |end|: if |end| < |start|, the effective cursor will be placed at the + // beginning of the selection. + void SetSelection(LONG start, LONG end); + + // Like SetSelection(), but takes a CHARRANGE. + void SetSelectionRange(const CHARRANGE& sel) { + SetSelection(sel.cpMin, sel.cpMax); + } + + // Places the caret at the given position. This clears any selection. + void PlaceCaretAt(std::wstring::size_type pos); + + // Returns true if |sel| represents a forward or backward selection of all the + // text. + bool IsSelectAll(const CHARRANGE& sel) const; + + // Given an X coordinate in client coordinates, returns that coordinate + // clipped to be within the horizontal bounds of the visible text. + // + // This is used in our mouse handlers to work around quirky behaviors of the + // underlying CRichEditCtrl like not supporting triple-click when the user + // doesn't click on the text itself. + // + // |is_triple_click| should be true iff this is the third click of a triple + // click. Sadly, we need to clip slightly differently in this case. + LONG ClipXCoordToVisibleText(LONG x, bool is_triple_click) const; + + // Parses the contents of the control for the scheme and the host name. + // Highlights the scheme in green or red depending on it security level. + // If a host name is found, it makes it visually stronger. + void EmphasizeURLComponents(); + + // Erases the portion of the selection in the font's y-adjustment area. For + // some reason the edit draws the selection rect here even though it's not + // part of the font. + void EraseTopOfSelection(CDC* dc, + const CRect& client_rect, + const CRect& paint_clip_rect); + + // Draws a slash across the scheme if desired. + void DrawSlashForInsecureScheme(HDC hdc, + const CRect& client_rect, + const CRect& paint_clip_rect); + + // Renders the drop highlight. + void DrawDropHighlight(HDC hdc, + const CRect& client_rect, + const CRect& paint_clip_rect); + + // When the user is pressing the control key, we interpret this as requesting + // us to add the top-level domain "com" to the contents of the edit control. + // Some users expect this behavior because it is present in other browsers. + std::wstring GetDesiredTLD() const; + + // Updates the autocomplete popup and other state after the text has been + // changed by the user. + void UpdatePopup(); + + // Internally invoked whenever the text changes in some way. + void TextChanged(); + + // Conversion between user text and display text. User text is the text the + // user has input. Display text is the text being shown in the edit. The + // two are different if a keyword is selected. + std::wstring DisplayTextFromUserText(const std::wstring& text) const; + std::wstring UserTextFromDisplayText(const std::wstring& text) const; + + // Returns the current clipboard contents as a string that can be pasted in. + // In addition to just getting CF_UNICODETEXT out, this can also extract URLs + // from bookmarks on the clipboard. + std::wstring AutocompleteEdit::GetClipboardText() const; + + // Determines whether the user can "paste and go", given the specified text. + // This also updates the internal paste-and-go-related state variables as + // appropriate so that the controller doesn't need to be repeatedly queried + // for the same text in every clipboard-related function. + bool CanPasteAndGo(const std::wstring& text) const; + + // Navigates to the destination last supplied to CanPasteAndGo. + void PasteAndGo(); + + // Sets the state of user_input_in_progress_, and notifies the observer if + // that state has changed. + void SetInputInProgress(bool in_progress); + + // As necessary, sends out notification that the user is accepting a URL in + // the edit. If the accepted URL is from selecting a keyword, |keyword| is + // the selected keyword. + // If |selected_line| is kNoMatch, the currently selected line is used for the + // metrics log record; otherwise, the provided value is used as the selected + // line. This is used when the user opens a URL without actually selecting + // its entry, such as middle-clicking it. + void SendOpenNotification(size_t selected_line, const std::wstring& keyword); + + // Getter for the text_object_model_, used by the ScopedXXX classes. Note + // that the pointer returned here is only valid as long as the + // AutocompleteEdit is still alive. + ITextDocument* GetTextObjectModel() const; + + // Invoked during a mouse move. As necessary starts a drag and drop session. + void StartDragIfNecessary(const CPoint& point); + + // Invoked during a mouse down. If the mouse location is over the selection + // this sets possible_drag_ to true to indicate a drag should start if the + // user moves the mouse far enough to start a drag. + void OnPossibleDrag(const CPoint& point); + + // Invoked when a mouse button is released. If none of the buttons are still + // down, this sets possible_drag_ to false. + void UpdateDragDone(UINT keys); + + // Redraws the necessary region for a drop highlight at the specified + // position. This does nothing if position is beyond the bounds of the + // text. + void RepaintDropHighlight(int position); + + Controller* controller_; + + // The Popup itself. + scoped_ptr<AutocompletePopup> popup_; + + // When true, the location bar view is read only and also is has a slightly + // different presentation (font size / color). This is used for popups. + bool popup_window_mode_; + + // Whether the edit has focus. + bool has_focus_; + + // Non-null when the edit is gaining focus from a left click. This is only + // needed between when WM_MOUSEACTIVATE and WM_LBUTTONDOWN get processed. It + // serves two purposes: first, by communicating to OnLButtonDown() that we're + // gaining focus from a left click, it allows us to work even with the + // inconsistent order in which various Windows messages get sent (see comments + // in OnMouseActivate()). Second, by holding the edit frozen, it ensures that + // when we process WM_SETFOCUS the edit won't first redraw itself with the + // caret at the beginning, and then have it blink to where the mouse cursor + // really is shortly afterward. + scoped_ptr<ScopedFreeze> gaining_focus_; + + // The URL of the currently displayed page. + std::wstring permanent_text_; + + // This flag is true when the user has modified the contents of the edit, but + // not yet accepted them. We use this to determine when we need to save + // state (on switching tabs) and whether changes to the page URL should be + // immediately displayed. + // This flag will be true in a superset of the cases where the popup is open. + bool user_input_in_progress_; + + // The text that the user has entered. This does not include inline + // autocomplete text that has not yet been accepted. + std::wstring user_text_; + + // When the user closes the popup, we need to remember the URL for their + // desired choice, so that if they hit enter without reopening the popup we + // know where to go. We could simply rerun autocomplete in this case, but + // we'd need to either wait for all results to come in (unacceptably slow) or + // do the wrong thing when the user had chosen some provider whose results + // were not returned instantaneously. + // + // This variable is only valid when user_input_in_progress_ is true, since + // when it is false the user has either never input anything (so there won't + // be a value here anyway) or has canceled their input, which should be + // treated the same way. Also, since this is for preserving a desired URL + // after the popup has been closed, we ignore this if the popup is open, and + // simply ask the popup for the desired URL directly. As a result, the + // contents of this variable only need to be updated when the popup is closed + // but user_input_in_progress_ is not being cleared. + std::wstring url_for_remembered_user_selection_; + + // Inline autocomplete is allowed if the user has not just deleted text, and + // no temporary text is showing. In this case, inline_autocomplete_text_ is + // appended to the user_text_ and displayed selected (at least initially). + // + // NOTE: When the popup is closed there should never be inline autocomplete + // text (actions that close the popup should either accept the text, convert + // it to a normal selection, or change the edit entirely). + bool just_deleted_text_; + std::wstring inline_autocomplete_text_; + + // Used by Set/RevertTemporaryText to keep track of whether there + // is currently a temporary text. The original_selection_ is only valid when + // there is temporary text. + // + // Example of use: If the user types "goog", then arrows down in the + // autocomplete popup until, say, "google.com" appears in the edit box, then + // the user_text_ is still "goog", and "google.com" is "temporary text". + // When the user hits <esc>, the edit box reverts to "goog". Hit <esc> again + // and the popup is closed and "goog" is replaced by the permanent_text_, + // which is the URL of the current page. + // + // original_url_ is valid in the same cases as original_selection_, and is + // used as the unique identifier of the originally selected item. Thus, if + // the user arrows to a different item with the same text, we can still + // distinguish them and not revert all the way to the permanent_text_. + // + // original_selected_match_, which is valid in the same cases as the other + // two original_* members, is the manually selected match to revert the popup + // to, if any. This can be non-empty when the user has selected a keyword + // (by hitting <tab> when applicable), or when the user has manually selected + // a match and then continued to edit it. + bool has_temporary_text_; + CHARRANGE original_selection_; + std::wstring original_url_; + AutocompleteResult::Selection original_selected_match_; + + // When the user's last action was to paste and replace all the text, we + // disallow inline autocomplete (on the theory that the user is trying to + // paste in a new URL or part of one, and in either case inline autocomplete + // would get in the way). + PasteState paste_state_; + + // When the user clicks to give us focus, we watch to see if they're clicking + // or dragging. When they're clicking, we select nothing until mouseup, then + // select all the text in the edit. During this process, tracking_click_ is + // true and mouse_down_point_ holds the original click location. At other + // times, tracking_click_ is false, and the contents of mouse_down_point_ + // should be ignored. + bool tracking_click_; + CPoint mouse_down_point_; + + // We need to know if the user triple-clicks, so track double click points + // and times so we can see if subsequent clicks are actually triple clicks. + bool tracking_double_click_; + CPoint double_click_point_; + DWORD double_click_time_; + + // Used to discard unnecessary WM_MOUSEMOVE events after the first such + // unnecessary event. See detailed comments in OnMouseMove(). + bool can_discard_mousemove_; + + // Variables for tracking state before and after a possible change. + std::wstring text_before_change_; + CHARRANGE sel_before_change_; + bool select_all_before_change_; + + // Holds the user's selection across focus changes. cpMin holds -1 when + // there is no saved selection. + CHARRANGE saved_selection_for_focus_change_; + + // The context menu for the edit. + scoped_ptr<Menu> context_menu_; + + // Whether the control key is depressed. We track this to avoid calling + // UpdatePopup() repeatedly if the user holds down the key, and to know + // whether to trigger "ctrl-enter" behavior. + ControlKeyState control_key_state_; + + // The object that handles additional command functionality exposed on the + // edit, such as invoking the keyword editor. + CommandController* command_controller_; + + // The parent view for the edit, used to align the popup and for + // accessibility. + ChromeViews::View* parent_view_; + + // Font we're using. We keep a reference to make sure the font supplied to + // the constructor doesn't go away before we do. + ChromeFont font_; + + // Metrics about the font, which we keep so we don't need to recalculate them + // every time we paint. |font_y_adjustment_| is the number of pixels we need + // to shift the font vertically in order to make its baseline be at our + // desired baseline in the edit. + int font_ascent_; + int font_descent_; + int font_x_height_; + int font_y_adjustment_; + + // If true, indicates the mouse is down and if the mouse is moved enough we + // should start a drag. + bool possible_drag_; + + // If true, we're in a call to DoDragDrop. + bool in_drag_; + + // If true indicates we've run a drag and drop session. This is used to + // avoid starting two drag and drop sessions if the drag is canceled while + // the mouse is still down. + bool initiated_drag_; + + // Position of the drop highlight. If this is -1, there is no drop highlight. + int drop_highlight_position_; + + // The keyword associated with the current match. The user may have an actual + // selected keyword, or just some input text that looks like a keyword (so we + // can show a hint to press <tab>). This is the keyword in either case; + // is_keyword_hint_ (below) distinguishes the two cases. + std::wstring keyword_; + + // True if the keyword associated with this match is merely a hint, i.e. the + // user hasn't actually selected a keyword yet. When this is true, we can use + // keyword_ to show a "Press <tab> to search" sort of hint. + bool is_keyword_hint_; + + // In some cases, such as when the user is editing in the middle of the input + // string, the input might look like a keyword, but we don't want to display + // the keyword UI, so as not to interfere with the user's editing. + bool disable_keyword_ui_; + + // True when it's safe to show a "Type to search" hint to the user (when the + // edit is empty, or the user is in the process of searching). + bool show_search_hint_; + + // Security UI-related data. + COLORREF background_color_; + ToolbarModel::SecurityLevel scheme_security_level_; + + // Paste And Go-related state. See CanPasteAndGo(). + mutable std::wstring paste_and_go_url_; + mutable PageTransition::Type paste_and_go_transition_; + mutable std::wstring paste_and_go_alternate_nav_url_; + + // This interface is useful for accessing the CRichEditCtrl at a low level. + mutable CComQIPtr<ITextDocument> text_object_model_; + + ToolbarModel* model_; + + Profile* profile_; + + // This contains the scheme char start and stop indexes that should be + // striken-out when displaying an insecure scheme. + url_parse::Component insecure_scheme_component_; + + // Instance of accessibility information and handling. + mutable CComPtr<IAccessible> autocomplete_accessibility_; + + DISALLOW_EVIL_CONSTRUCTORS(AutocompleteEdit); +}; + +#endif // CHROME_BROWSER_AUTOCOMPLETE_AUTOCOMPLETE_EDIT_H__ diff --git a/chrome/browser/autocomplete/autocomplete_popup.cc b/chrome/browser/autocomplete/autocomplete_popup.cc new file mode 100644 index 0000000..278bc28 --- /dev/null +++ b/chrome/browser/autocomplete/autocomplete_popup.cc @@ -0,0 +1,1187 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "chrome/browser/autocomplete/autocomplete_popup.h" + +#include <cmath> + +#include "base/scoped_ptr.h" +#include "base/string_util.h" +#include "chrome/app/theme/theme_resources.h" +#include "chrome/browser/autocomplete/autocomplete_edit.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/net/dns_global.h" +#include "chrome/browser/profile.h" +#include "chrome/browser/template_url.h" +#include "chrome/browser/template_url_model.h" +#include "chrome/browser/views/location_bar_view.h" +#include "chrome/common/gfx/chrome_canvas.h" +#include "chrome/common/resource_bundle.h" +#include "third_party/icu38/public/common/unicode/ubidi.h" + +namespace { +// The amount of time we'll wait after a provider returns before updating, +// in order to coalesce results. +const int kPopupCoalesceMs = 100; + +// The maximum time we'll allow the popup to go without updating. +const int kPopupUpdateMaxDelayMs = 300; + +// Padding between text and the star indicator, in pixels. +const int kStarPadding = 4; + +}; + +// A simple wrapper class for the bidirectional iterator of ICU. +// This class uses the bidirctional iterator of ICU to split a line of +// bidirectional texts into visual runs in its display order. +class BiDiLineIterator { + public: + BiDiLineIterator(); + ~BiDiLineIterator(); + + // Initialize the bidirectional iterator with the specified text. + // Parameters + // * text [in] (const std::wstring&) + // Represents the text to be iterated with this iterator. + // * right_to_left [in] (bool) + // Represents whether or not the default text direction is right-to-left. + // Possible parameters are listed below: + // - true, the default text direction is right-to-left. + // - false, the default text direction is left-to-right. + // * url [in] (bool) + // Represents whether or not this text is a URL. + // Return values + // * true + // The bidirectional iterator is created successfully. + // * false + // An error occured while creating the bidirectional iterator. + UBool Open(const std::wstring& text, bool right_to_left, bool url); + + // Retrieve the number of visual runs in the text. + // Return values + // * A positive value + // Represents the number of visual runs. + // * 0 + // Represents an error. + int CountRuns(); + + // Get the logical offset, length, and direction of the specified visual run. + // Parameters + // * index [in] (int) + // Represents the index of the visual run. This value must be less than + // the return value of the CountRuns() function. + // * start [out] (int*) + // Represents the index of the specified visual run, in characters. + // * length [out] (int*) + // Represents the length of the specified visual run, in characters. + // Return values + // * UBIDI_LTR + // Represents this run should be rendered in the left-to-right reading + // order. + // * UBIDI_RTL + // Represents this run should be rendered in the right-to-left reading + // order. + UBiDiDirection GetVisualRun(int index, int* start, int* length); + + private: + UBiDi* bidi_; + + DISALLOW_EVIL_CONSTRUCTORS(BiDiLineIterator); +}; + +BiDiLineIterator::BiDiLineIterator() + : bidi_(NULL) { +} + +BiDiLineIterator::~BiDiLineIterator() { + if (bidi_) { + ubidi_close(bidi_); + bidi_ = NULL; + } +} + +UBool BiDiLineIterator::Open(const std::wstring& text, + bool right_to_left, + bool url) { + DCHECK(bidi_ == NULL); + UErrorCode error = U_ZERO_ERROR; + bidi_ = ubidi_openSized(static_cast<int>(text.length()), 0, &error); + if (U_FAILURE(error)) + return false; + if (right_to_left && url) + ubidi_setReorderingMode(bidi_, UBIDI_REORDER_RUNS_ONLY); + ubidi_setPara(bidi_, text.data(), static_cast<int>(text.length()), + right_to_left ? UBIDI_DEFAULT_RTL : UBIDI_DEFAULT_LTR, + NULL, &error); + return U_SUCCESS(error); +} + +int BiDiLineIterator::CountRuns() { + DCHECK(bidi_ != NULL); + UErrorCode error = U_ZERO_ERROR; + const int runs = ubidi_countRuns(bidi_, &error); + return U_SUCCESS(error) ? runs : 0; +} + +UBiDiDirection BiDiLineIterator::GetVisualRun(int index, + int* start, + int* length) { + DCHECK(bidi_ != NULL); + return ubidi_getVisualRun(bidi_, index, start, length); +} + +// This class implements a utility used for mirroring x-coordinates when the +// application language is a right-to-left one. +class MirroringContext { + public: + MirroringContext(); + ~MirroringContext(); + + // Initializes the bounding region used for mirroring coordinates. + // This class uses the center of this region as an axis for calculating + // mirrored coordinates. + void Initialize(int min_x, int max_x, bool enabled); + + // Return the "left" side of the specified region. + // When the application language is a right-to-left one, this function + // calculates the mirrored coordinates of the input region and returns the + // left side of the mirrored region. + // The input region must be in the bounding region specified in the + // Initialize() function. + int GetLeft(int min_x, int max_x) const; + + // Returns whether or not we are mirroring the x coordinate. + bool enabled() const { + return enabled_; + } + + private: + int min_x_; + int center_x_; + int max_x_; + bool enabled_; + + DISALLOW_EVIL_CONSTRUCTORS(MirroringContext); +}; + +MirroringContext::MirroringContext() + : min_x_(0), + center_x_(0), + max_x_(0), + enabled_(false) { +} + +MirroringContext::~MirroringContext() { +} + +void MirroringContext::Initialize(int min_x, int max_x, bool enabled) { + min_x_ = min_x; + max_x_ = max_x; + if (min_x_ > max_x_) + std::swap(min_x_, max_x_); + center_x_ = min_x + (max_x - min_x) / 2; + enabled_ = enabled; +} + +int MirroringContext::GetLeft(int min_x, int max_x) const { + if (!enabled_) + return min_x; + int mirrored_min_x = center_x_ + (center_x_ - min_x); + int mirrored_max_x = center_x_ + (center_x_ - max_x); + return std::min(mirrored_min_x, mirrored_max_x); +} + +const wchar_t AutocompletePopup::DrawLineInfo::ellipsis_str[] = L"\x2026"; + +AutocompletePopup::AutocompletePopup(const ChromeFont& font, + AutocompleteEdit* editor, + Profile* profile) + : editor_(editor), + controller_(new AutocompleteController(this, profile)), + profile_(profile), + line_info_(font), + query_in_progress_(false), + update_pending_(false), + // Creating the Timers directly instead of using StartTimer() ensures + // they won't actually start running until we use ResetTimer(). + coalesce_timer_(new Timer(kPopupCoalesceMs, this, false)), + max_delay_timer_(new Timer(kPopupUpdateMaxDelayMs, this, true)), + hovered_line_(kNoMatch), + selected_line_(kNoMatch), + mirroring_context_(new MirroringContext()), + star_(ResourceBundle::GetSharedInstance().GetBitmapNamed(IDR_CONTENT_STAR_ON)) { +} + +AutocompletePopup::~AutocompletePopup() { + StopAutocomplete(); +} + +void AutocompletePopup::SetProfile(Profile* profile) { + DCHECK(profile); + profile_ = profile; + controller_->SetProfile(profile); +} + +void AutocompletePopup::StartAutocomplete( + const std::wstring& text, + const std::wstring& desired_tld, + bool prevent_inline_autocomplete) { + // The user is interacting with the edit, so stop tracking hover. + SetHoveredLine(kNoMatch); + + // See if we can avoid rerunning autocomplete when the query hasn't changed + // much. If the popup isn't open, we threw the past results away, so no + // shortcuts are possible. + const AutocompleteInput input(text, desired_tld, prevent_inline_autocomplete); + bool minimal_changes = false; + if (is_open()) { + // When the user hits escape with a temporary selection, the edit asks us + // to update, but the text it supplies hasn't changed since the last query. + // Instead of stopping or rerunning the last query, just do an immediate + // repaint with the new (probably NULL) provider affinity. + if (input_.Equals(input)) { + SetDefaultMatchAndUpdate(true); + return; + } + + // When the user presses or releases the ctrl key, the desired_tld changes, + // and when the user finishes an IME composition, inline autocomplete may + // no longer be prevented. In both these cases the text itself hasn't + // changed since the last query, and some providers can do much less work + // (and get results back more quickly). Taking advantage of this reduces + // flicker. + if (input_.text() == text) + minimal_changes = true; + } + input_ = input; + + // If we're starting a brand new query, stop caring about any old query. + TimerManager* const tm = MessageLoop::current()->timer_manager(); + if (!minimal_changes && query_in_progress_) { + update_pending_ = false; + tm->StopTimer(coalesce_timer_.get()); + } + + // Start the new query. + query_in_progress_ = !controller_->Start(input_, minimal_changes, false); + controller_->GetResult(&latest_result_); + + // If we're not ready to show results and the max update interval timer isn't + // already running, start it now. + if (query_in_progress_ && !tm->IsTimerRunning(max_delay_timer_.get())) + tm->ResetTimer(max_delay_timer_.get()); + + SetDefaultMatchAndUpdate(!query_in_progress_); +} + +void AutocompletePopup::StopAutocomplete() { + // Close any old query. + if (query_in_progress_) { + controller_->Stop(); + query_in_progress_ = false; + update_pending_ = false; + } + + // Reset results. This will force the popup to close. + latest_result_.Reset(); + CommitLatestResults(true); + + // Clear input_ to make sure we don't try and use any of these results for + // the next query we receive. Strictly speaking this isn't necessary, since + // the popup isn't open, but it keeps our internal state consistent and + // serves as future-proofing in case the code in StartAutocomplete() changes. + input_.Clear(); +} + +std::wstring AutocompletePopup::URLsForCurrentSelection( + PageTransition::Type* transition, + bool* is_history_what_you_typed_match, + std::wstring* alternate_nav_url) const { + // The popup may be out of date, and we always want the latest match. The + // most common case of this is when the popup is open, the user changes the + // contents of the edit, and then presses enter before any results have been + // displayed, but still wants to choose the would-be-default action. + // + // Can't call CommitLatestResults(), because + // latest_result_.default_match_index() may not match selected_line_, and + // we want to preserve the user's selection. + if (latest_result_.empty()) + return std::wstring(); + + const AutocompleteResult* result; + AutocompleteResult::const_iterator match; + if (update_pending_) { + // The default match on the latest result should be up-to-date. If the user + // changed the selection since that result was generated using the arrow + // keys, Move() will have force updated the popup. + result = &latest_result_; + match = result->default_match(); + } else { + result = &result_; + 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(input_, match); + return match->destination_url; +} + +std::wstring AutocompletePopup::URLsForDefaultMatch( + const std::wstring& text, + const std::wstring& desired_tld, + PageTransition::Type* transition, + bool* is_history_what_you_typed_match, + std::wstring* alternate_nav_url) { + // Cancel any existing query. + StopAutocomplete(); + + // Run the new query and get only the synchronously available results. + const AutocompleteInput input(text, desired_tld, true); + const bool done = controller_->Start(input, false, true); + DCHECK(done); + controller_->GetResult(&result_); + if (result_.empty()) + return std::wstring(); + + // Get the URLs for the default match. + result_.SetDefaultMatch(manually_selected_match_); + const AutocompleteResult::const_iterator match = result_.default_match(); + const std::wstring url(match->destination_url); // Need to copy since we + // reset result_ below. + 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(input, match); + result_.Reset(); + + return url; +} + +AutocompleteLog* AutocompletePopup::GetAutocompleteLog() { + return new AutocompleteLog(input_.text(), selected_line_, 0, result_); +} + +void AutocompletePopup::Move(int count) { + // The user is using the keyboard to change the selection, so stop tracking + // hover. + SetHoveredLine(kNoMatch); + + // Force the popup to open/update, so the user is interacting with the + // latest results. + CommitLatestResults(false); + if (result_.empty()) + return; + + // 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)); +} + +void AutocompletePopup::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 = result_.match_at(selected_line_); + if (match.deletable) { + query_in_progress_ = true; + size_t selected_line = selected_line_; + match.provider->DeleteMatch(match); // This will synchronously call back + // to OnAutocompleteUpdate() + CommitLatestResults(false); + if (!result_.empty()) { + // Move the selection to the next choice after the deleted one, but clear + // the manual selection so this choice doesn't act "sticky". + // + // It might also be correct to only call Clear() here when + // manually_selected_match_ didn't already have a provider() (i.e. when + // there was no existing manual selection). It's not clear what the user + // wants when they shift-delete something they've arrowed to. If they + // arrowed down just to shift-delete it, we should probably Clear(); if + // they arrowed to do something else, then got a bad match while typing, + // we probably shouldn't. + SetSelectedLine(std::min(result_.size() - 1, selected_line)); + manually_selected_match_.Clear(); + } + } +} + +void AutocompletePopup::OnAutocompleteUpdate(bool updated_result, + bool query_complete) { + DCHECK(query_in_progress_); + if (updated_result) + controller_->GetResult(&latest_result_); + query_in_progress_ = !query_complete; + + SetDefaultMatchAndUpdate(query_complete); +} + +void AutocompletePopup::Run() { + CommitLatestResults(false); +} + +void AutocompletePopup::OnLButtonDown(UINT keys, const CPoint& point) { + const size_t new_hovered_line = PixelToLine(point.y); + SetHoveredLine(new_hovered_line); + SetSelectedLine(new_hovered_line); +} + +void AutocompletePopup::OnMButtonDown(UINT keys, const CPoint& point) { + SetHoveredLine(PixelToLine(point.y)); +} + +void AutocompletePopup::OnLButtonUp(UINT keys, const CPoint& point) { + OnButtonUp(point, CURRENT_TAB); +} + +void AutocompletePopup::OnMButtonUp(UINT keys, const CPoint& point) { + OnButtonUp(point, NEW_BACKGROUND_TAB); +} + +LRESULT AutocompletePopup::OnMouseActivate(HWND window, + UINT hit_test, + UINT mouse_message) { + return MA_NOACTIVATE; +} + +void AutocompletePopup::OnMouseLeave() { + // The mouse has left the window, so no line is hovered. + SetHoveredLine(kNoMatch); +} + +void AutocompletePopup::OnMouseMove(UINT keys, const CPoint& point) { + // Track hover when + // (a) The left or middle button is down (the user is interacting via the + // mouse) + // (b) The user moves the mouse from where we last stopped tracking hover + // (c) We started tracking previously due to (a) or (b) and haven't stopped + // yet (user hasn't used the keyboard to interact again) + const bool action_button_pressed = !!(keys & (MK_MBUTTON | MK_LBUTTON)); + CPoint screen_point(point); + ClientToScreen(&screen_point); + if (action_button_pressed || (last_hover_coordinates_ != screen_point) || + (hovered_line_ != kNoMatch)) { + // Determine the hovered line from the y coordinate of the event. We don't + // need to check whether the x coordinates are within the window since if + // they weren't someone else would have received the WM_MOUSEMOVE. + const size_t new_hovered_line = PixelToLine(point.y); + SetHoveredLine(new_hovered_line); + + // When the user has the left button down, update their selection + // immediately (don't wait for mouseup). + if (keys & MK_LBUTTON) + SetSelectedLine(new_hovered_line); + } +} + +void AutocompletePopup::OnPaint(HDC other_dc) { + DCHECK(!result_.empty()); // Shouldn't be drawing an empty popup + + CPaintDC dc(m_hWnd); + + RECT rc; + GetClientRect(&rc); + mirroring_context_->Initialize(rc.left, rc.right, + l10n_util::GetTextDirection() == l10n_util::RIGHT_TO_LEFT); + DrawBorder(rc, dc); + + bool all_descriptions_empty = true; + for (AutocompleteResult::const_iterator i(result_.begin()); + i != result_.end(); ++i) { + if (!i->description.empty()) { + all_descriptions_empty = false; + break; + } + } + + // Only repaint the invalid lines. + const size_t first_line = PixelToLine(dc.m_ps.rcPaint.top); + const size_t last_line = PixelToLine(dc.m_ps.rcPaint.bottom); + for (size_t i = first_line; i <= last_line; ++i) { + DrawLineInfo::LineStatus status; + // Selection should take precedence over hover. + if (i == selected_line_) + status = DrawLineInfo::SELECTED; + else if (i == hovered_line_) + status = DrawLineInfo::HOVERED; + else + status = DrawLineInfo::NORMAL; + DrawEntry(dc, rc, i, status, all_descriptions_empty, + result_.match_at(i).starred); + } +} + +void AutocompletePopup::OnButtonUp(const CPoint& point, + WindowOpenDisposition disposition) { + const size_t selected_line = PixelToLine(point.y); + const AutocompleteMatch& match = result_.match_at(selected_line); + // OpenURL() may close the popup, which will clear the result set and, by + // extension, |match| and its contents. So copy the relevant strings out to + // make sure they stay alive until the call completes. + const std::wstring url(match.destination_url); + std::wstring keyword; + const bool is_keyword_hint = GetKeywordForMatch(match, &keyword); + editor_->OpenURL(url, disposition, match.transition, std::wstring(), + selected_line, is_keyword_hint ? std::wstring() : keyword); +} + +void AutocompletePopup::SetDefaultMatchAndUpdate(bool immediately) { + if (!latest_result_.SetDefaultMatch(manually_selected_match_) && + !query_in_progress_) { + // We don't clear the provider affinity because the user didn't do + // something to indicate that they want a different provider, we just + // couldn't find the specific match they wanted. + manually_selected_match_.destination_url.clear(); + manually_selected_match_.is_history_what_you_typed_match = false; + } + + if (immediately) { + CommitLatestResults(true); + } else if (!update_pending_) { + // Coalesce the results for the next kPopupCoalesceMs milliseconds. + update_pending_ = true; + MessageLoop::current()->timer_manager()->ResetTimer(coalesce_timer_.get()); + } + + // 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; + bool can_show_search_hint = true; + const AutocompleteResult::const_iterator match( + latest_result_.default_match()); + if (match != latest_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::DnsPrefetchUrlString(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); + can_show_search_hint = (match->type == AutocompleteMatch::SEARCH); + } + editor_->OnPopupDataChanged(inline_autocomplete_text, false, + manually_selected_match_ /* ignored */, keyword, is_keyword_hint, + can_show_search_hint); +} + +void AutocompletePopup::CommitLatestResults(bool force) { + if (!force && !update_pending_) + return; + + update_pending_ = false; + + // If we're going to trim the window size to no longer include the hovered + // line, turn hover off first. We need to do this before changing result_ + // since SetHover() should be able to trust that the old and new hovered + // lines are valid. + // + // Practically, this only currently happens when we're closing the window by + // setting latest_result_ to an empty list. + if ((hovered_line_ != kNoMatch) && (latest_result_.size() <= hovered_line_)) + SetHoveredLine(kNoMatch); + + result_.CopyFrom(latest_result_); + selected_line_ = (result_.default_match() == result_.end()) ? + kNoMatch : (result_.default_match() - result_.begin()); + + UpdatePopupAppearance(); + + // The max update interval timer either needs to be reset (if more updates + // are to come) or stopped (when we're done with the query). The coalesce + // timer should always just be stopped. + TimerManager* const tm = MessageLoop::current()->timer_manager(); + tm->StopTimer(coalesce_timer_.get()); + if (query_in_progress_) + tm->ResetTimer(max_delay_timer_.get()); + else + tm->StopTimer(max_delay_timer_.get()); +} + +void AutocompletePopup::UpdatePopupAppearance() { + if (result_.empty()) { + // No results, close any existing popup. + if (m_hWnd) { + DestroyWindow(); + m_hWnd = NULL; + } + return; + } + + // Figure the coordinates of the popup: + // Get the coordinates of the location bar view; these are returned relative + // to its parent. + CRect rc; + editor_->parent_view()->GetBounds(&rc); + // Subtract the top left corner to make the coordinates relative to the + // location bar view itself, and convert to screen coordinates. + CPoint top_left(-rc.TopLeft()); + ChromeViews::View::ConvertPointToScreen(editor_->parent_view(), &top_left); + rc.OffsetRect(top_left); + // Expand by one pixel on each side since that's the amount the location bar + // view is inset from the divider line that edges the adjacent buttons. + // Deflate the top and bottom by the height of the extra graphics around the + // edit. + // TODO(pkasting): http://b/972786 This shouldn't be hardcoded to rely on + // LocationBarView constants. Instead we should just make the edit be "at the + // right coordinates", or something else generic. + rc.InflateRect(1, -LocationBarView::kTextVertMargin); + // Now rc is the exact width we want and is positioned like the edit would + // be, so shift the top and bottom downwards so the new top is where the old + // bottom is and the rect has the height we need for all our entries, plus a + // one-pixel border on top and bottom. + rc.top = rc.bottom; + rc.bottom += static_cast<int>(result_.size()) * line_info_.line_height + 2; + + if (!m_hWnd) { + // To prevent this window from being activated, we create an invisible + // window and manually show it without activating it. + Create(editor_->m_hWnd, rc, AUTOCOMPLETEPOPUP_CLASSNAME, WS_POPUP, + WS_EX_TOOLWINDOW); + // When an IME is attached to the rich-edit control, retrieve its window + // handle and show this popup window under the IME windows. + // Otherwise, show this popup window under top-most windows. + // TODO(hbono): http://b/1111369 if we exclude this popup window from the + // display area of IME windows, this workaround becomes unnecessary. + HWND ime_window = ImmGetDefaultIMEWnd(editor_->m_hWnd); + SetWindowPos(ime_window ? ime_window : HWND_NOTOPMOST, 0, 0, 0, 0, + SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW); + } else { + // Already open, just resize the window. This is a bit tricky; we want to + // repaint the whole window, since the contents may have changed, but + // MoveWindow() won't repaint portions that haven't moved or been + // added/removed. So we first call InvalidateRect(), so the next repaint + // paints the whole window, then tell MoveWindow() to do the actual + // repaint, which will also properly repaint Windows formerly under the + // popup. + InvalidateRect(NULL, false); + MoveWindow(rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, true); + } + + // TODO(pkasting): http://b/1111369 We should call ImmSetCandidateWindow() on + // the editor_'s IME context here, and exclude ourselves from its display + // area. Not clear what to pass for the lpCandidate->ptCurrentPos member, + // though... +} + +int AutocompletePopup::LineTopPixel(size_t line) const { + DCHECK(line <= result_.size()); + // The popup has a 1 px top border. + return line_info_.line_height * static_cast<int>(line) + 1; +} + +size_t AutocompletePopup::PixelToLine(int y) const { + const size_t line = std::max(y - 1, 0) / line_info_.line_height; + return (line < result_.size()) ? line : (result_.size() - 1); +} + +void AutocompletePopup::SetHoveredLine(size_t line) { + const bool is_disabling = (line == kNoMatch); + DCHECK(is_disabling || (line < result_.size())); + + if (line == hovered_line_) + return; // Nothing to do + + const bool is_enabling = (hovered_line_ == kNoMatch); + if (is_enabling || is_disabling) { + TRACKMOUSEEVENT tme; + tme.cbSize = sizeof(TRACKMOUSEEVENT); + if (is_disabling) { + // Save the current mouse position to check against for re-enabling. + GetCursorPos(&last_hover_coordinates_); // Returns screen coordinates + + // Cancel existing registration for WM_MOUSELEAVE notifications. + tme.dwFlags = TME_CANCEL | TME_LEAVE; + } else { + // Register for WM_MOUSELEAVE notifications. + tme.dwFlags = TME_LEAVE; + } + tme.hwndTrack = m_hWnd; + tme.dwHoverTime = HOVER_DEFAULT; // Not actually used + TrackMouseEvent(&tme); + } + + // 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 (!is_enabling && (hovered_line_ != selected_line_)) + 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_)) + InvalidateLine(hovered_line_); +} + +void AutocompletePopup::SetSelectedLine(size_t line) { + DCHECK(line < result_.size()); + if (result_.empty()) + return; + + if (line == selected_line_) + return; // Nothing to do + + // Update the edit with the new data for this match. + const AutocompleteMatch& match = result_.match_at(line); + std::wstring keyword; + const bool is_keyword_hint = GetKeywordForMatch(match, &keyword); + editor_->OnPopupDataChanged(match.fill_into_edit, true, + manually_selected_match_, keyword, + is_keyword_hint, + (match.type == AutocompleteMatch::SEARCH)); + + // 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; + + // 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). + InvalidateLine(selected_line_); + selected_line_ = line; + InvalidateLine(selected_line_); + UpdateWindow(); +} + +void AutocompletePopup::InvalidateLine(size_t line) { + DCHECK(line < result_.size()); + + RECT rc; + GetClientRect(&rc); + rc.top = LineTopPixel(line); + rc.bottom = rc.top + line_info_.line_height; + InvalidateRect(&rc, false); +} + +// Draws a light border around the inside of the window with the given client +// rectangle and DC. +void AutocompletePopup::DrawBorder(const RECT& rc, HDC dc) { + HPEN hpen = CreatePen(PS_SOLID, 1, RGB(199, 202, 206)); + HGDIOBJ old_pen = SelectObject(dc, hpen); + + int width = rc.right - rc.left - 1; + int height = rc.bottom - rc.top - 1; + + MoveToEx(dc, 0, 0, NULL); + LineTo(dc, 0, height); + LineTo(dc, width, height); + LineTo(dc, width, 0); + LineTo(dc, 0, 0); + + SelectObject(dc, old_pen); + DeleteObject(hpen); +} + +int AutocompletePopup::DrawString(HDC dc, + int x, + int y, + int max_x, + const wchar_t* text, + int length, + int style, + const DrawLineInfo::LineStatus status, + const MirroringContext* context, + bool text_direction_is_rtl) const { + if (length <= 0) + return 0; + + // Set up the text decorations. + SelectObject(dc, (style & ACMatchClassification::MATCH) ? + line_info_.bold_font.hfont() : line_info_.regular_font.hfont()); + const COLORREF foreground = (style & ACMatchClassification::URL) ? + line_info_.url_colors[status] : line_info_.text_colors[status]; + const COLORREF background = line_info_.background_colors[status]; + SetTextColor(dc, (style & ACMatchClassification::DIM) ? + DrawLineInfo::AlphaBlend(foreground, background, 0xAA) : foreground); + + // Retrieve the width of the decorated text and display it. When we cannot + // display this fragment in the given width, we trim the fragment and add an + // ellipsis. + // + // TODO(hbono): http:///b/1222425 We should change the following eliding code + // with more aggressive one. + int text_x = x; + int max_length = 0; + SIZE text_size = {0}; + GetTextExtentExPoint(dc, text, length, + max_x - line_info_.ellipsis_width - text_x, &max_length, + NULL, &text_size); + + if (max_length < length) + GetTextExtentPoint32(dc, text, max_length, &text_size); + + const int mirrored_x = context->GetLeft(text_x, text_x + text_size.cx); + RECT text_bounds = {mirrored_x, + 0, + mirrored_x + text_size.cx, + line_info_.line_height}; + + int flags = DT_SINGLELINE | DT_NOPREFIX; + if (text_direction_is_rtl) + // In order to make sure RTL text is displayed correctly (for example, a + // trailing space should be displayed on the left and not on the right), we + // pass the flag DT_RTLREADING. + flags |= DT_RTLREADING; + + DrawText(dc, text, length, &text_bounds, flags); + text_x += text_size.cx; + + // Draw the ellipsis. Note that since we use the mirroring context, the + // ellipsis are drawn either to the right or to the left of the text. + if (max_length < length) { + TextOut(dc, context->GetLeft(text_x, text_x + line_info_.ellipsis_width), + 0, line_info_.ellipsis_str, arraysize(line_info_.ellipsis_str) - 1); + text_x += line_info_.ellipsis_width; + } + + return text_x - x; +} + +void AutocompletePopup::DrawMatchFragments( + HDC dc, + const std::wstring& text, + const ACMatchClassifications& classifications, + int x, + int y, + int max_x, + DrawLineInfo::LineStatus status) const { + if (!text.length()) + return; + + // Check whether or not this text is a URL string. + // A URL string is basically in English with possible included words in + // Arabic or Hebrew. For such case, ICU provides a special algorithm and we + // should use it. + bool url = false; + for (ACMatchClassifications::const_iterator i = classifications.begin(); + i != classifications.end(); ++i) { + if (i->style & ACMatchClassification::URL) + url = true; + } + + // Initialize a bidirectional line iterator of ICU and split the text into + // visual runs. (A visual run is consecutive characters which have the same + // display direction and should be displayed at once.) + BiDiLineIterator bidi_line; + if (!bidi_line.Open(text, mirroring_context_->enabled(), url)) + return; + const int runs = bidi_line.CountRuns(); + + // Draw the visual runs. + // This loop splits each run into text fragments with the given + // classifications and draws the text fragments. + // When the direction of a run is right-to-left, we have to mirror the + // x-coordinate of this run and render the fragments in the right-to-left + // reading order. To handle this display order independently from the one of + // this popup window, this loop renders a run with the steps below: + // 1. Create a local display context for each run; + // 2. Render the run into the local display context, and; + // 3. Copy the local display context to the one of the popup window. + int run_x = x; + for (int run = 0; run < runs; ++run) { + int run_start = 0; + int run_length = 0; + + // The index we pass to GetVisualRun corresponds to the position of the run + // in the displayed text. For example, the string "Google in HEBREW" (where + // HEBREW is text in the Hebrew language) has two runs: "Google in " which + // is an LTR run, and "HEBREW" which is an RTL run. In an LTR context, the + // run "Google in " has the index 0 (since it is the leftmost run + // displayed). In an RTL context, the same run has the index 1 because it + // is the rightmost run. This is why the order in which we traverse the + // runs is different depending on the locale direction. + // + // Note that for URLs we always traverse the runs from lower to higher + // indexes because the return order of runs for a URL always matches the + // physical order of the context. + int current_run = + (mirroring_context_->enabled() && !url) ? (runs - run - 1) : run; + const UBiDiDirection run_direction = bidi_line.GetVisualRun(current_run, + &run_start, + &run_length); + const int run_end = run_start + run_length; + + // Set up a local display context for rendering this run. + int text_x = 0; + const int text_max_x = max_x - run_x; + MirroringContext run_context; + run_context.Initialize(0, text_max_x, run_direction == UBIDI_RTL); + + // In addition to creating a mirroring context for the run, we indicate + // whether the run needs to be rendered as RTL text. The mirroring context + // alone in not sufficient because there are cases where a mirrored RTL run + // needs to be rendered in an LTR context (for example, an RTL run within + // an URL). + bool run_direction_is_rtl = (run_direction == UBIDI_RTL) && !url; + CDC text_dc(CreateCompatibleDC(dc)); + CBitmap text_bitmap(CreateCompatibleBitmap(dc, text_max_x, + line_info_.font_height)); + SelectObject(text_dc, text_bitmap); + const RECT text_rect = {0, 0, text_max_x, line_info_.line_height}; + FillRect(text_dc, &text_rect, line_info_.brushes[status]); + SetBkMode(text_dc, TRANSPARENT); + + // Split this run with the given classifications and draw the fragments + // into the local display context. + for (ACMatchClassifications::const_iterator i = classifications.begin(); + i != classifications.end(); ++i) { + const int text_start = std::max(run_start, static_cast<int>(i->offset)); + const int text_end = std::min(run_end, (i != classifications.end() - 1) ? + static_cast<int>((i + 1)->offset) : run_end); + text_x += DrawString(text_dc, text_x, 0, text_max_x, &text[text_start], + text_end - text_start, i->style, status, + &run_context, run_direction_is_rtl); + } + + // Copy the local display context to the one of the popup window and + // delete the local display context. + BitBlt(dc, mirroring_context_->GetLeft(run_x, run_x + text_x), y, text_x, + line_info_.line_height, text_dc, run_context.GetLeft(0, text_x), 0, + SRCCOPY); + run_x += text_x; + } +} + +void AutocompletePopup::DrawEntry(HDC dc, + const RECT& client_rect, + size_t line, + DrawLineInfo::LineStatus status, + bool all_descriptions_empty, + bool starred) const { + // Calculate outer bounds of entry, and fill background. + const int top_pixel = LineTopPixel(line); + const RECT rc = {1, top_pixel, client_rect.right - client_rect.left - 1, + top_pixel + line_info_.line_height}; + FillRect(dc, &rc, line_info_.brushes[status]); + + // Calculate and display contents/description sections as follows: + // * 2 px top margin, bottom margin is handled by line_height. + const int y = rc.top + 2; + + // * 1 char left/right margin. + const int side_margin = line_info_.ave_char_width; + + // * 50% of the remaining width is initially allocated to each section, with + // a 2 char margin followed by the star column and kStarPadding padding. + const int content_min_x = rc.left + side_margin; + const int description_max_x = rc.right - side_margin; + const int mid_line = (description_max_x - content_min_x) / 2 + content_min_x; + const int star_col_width = kStarPadding + star_->width(); + const int content_right_margin = line_info_.ave_char_width * 2; + + // * If this would make the content section display fewer than 40 characters, + // the content section is increased to that minimum at the expense of the + // description section. + const int content_width = + std::max(mid_line - content_min_x - content_right_margin, + line_info_.ave_char_width * 40); + const int description_width = description_max_x - content_min_x - + content_width - star_col_width; + + // * If this would make the description section display fewer than 20 + // characters, or if there are no descriptions to display or the result is + // the HISTORY_SEARCH shortcut, the description section is eliminated, and + // all the available width is used for the content section. + int star_x; + const AutocompleteMatch& match = result_.match_at(line); + if ((description_width < (line_info_.ave_char_width * 20)) || + all_descriptions_empty || + (match.type == AutocompleteMatch::HISTORY_SEARCH)) { + star_x = description_max_x - star_col_width + kStarPadding; + DrawMatchFragments(dc, match.contents, match.contents_class, content_min_x, + y, star_x - kStarPadding, status); + } else { + star_x = description_max_x - description_width - star_col_width; + DrawMatchFragments(dc, match.contents, match.contents_class, content_min_x, + y, content_min_x + content_width, status); + DrawMatchFragments(dc, match.description, match.description_class, + description_max_x - description_width, y, + description_max_x, status); + } + if (starred) + DrawStar(dc, star_x, + (line_info_.line_height - star_->height()) / 2 + top_pixel); +} + +void AutocompletePopup::DrawStar(HDC dc, int x, int y) const { + ChromeCanvas canvas(star_->width(), star_->height(), false); + // Make the background completely transparent. + canvas.drawColor(SK_ColorBLACK, SkPorterDuff::kClear_Mode); + canvas.DrawBitmapInt(*star_, 0, 0); + canvas.getTopPlatformDevice().drawToHDC( + dc, mirroring_context_->GetLeft(x, x + star_->width()), y, NULL); +} + +bool AutocompletePopup::GetKeywordForMatch(const AutocompleteMatch& match, + std::wstring* keyword) { + // 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; +} + +AutocompletePopup::DrawLineInfo::DrawLineInfo(const ChromeFont& font) { + // Create regular and bold fonts. + regular_font = font.DeriveFont(-1); + bold_font = regular_font.DeriveFont(0, ChromeFont::BOLD); + + // The total padding added to each line (bottom padding is what is + // left over after DrawEntry() specifies its top offset). + static const int kTotalLinePadding = 5; + font_height = std::max(regular_font.height(), bold_font.height()); + line_height = font_height + kTotalLinePadding; + ave_char_width = regular_font.ave_char_width(); + ellipsis_width = std::max(regular_font.GetStringWidth(ellipsis_str), + bold_font.GetStringWidth(ellipsis_str)); + + // Create background colors. + background_colors[NORMAL] = GetSysColor(COLOR_WINDOW); + background_colors[SELECTED] = GetSysColor(COLOR_HIGHLIGHT); + background_colors[HOVERED] = + AlphaBlend(background_colors[SELECTED], background_colors[NORMAL], 0x40); + + // Create text colors. + text_colors[NORMAL] = GetSysColor(COLOR_WINDOWTEXT); + text_colors[HOVERED] = text_colors[NORMAL]; + text_colors[SELECTED] = GetSysColor(COLOR_HIGHLIGHTTEXT); + + // Create brushes and url colors. + const COLORREF dark_url(0x008000); + const COLORREF light_url(0xd0ffd0); + for (int i = 0; i < MAX_STATUS_ENTRIES; ++i) { + // Pick whichever URL color contrasts better. + const double dark_contrast = + LuminosityContrast(dark_url, background_colors[i]); + const double light_contrast = + LuminosityContrast(light_url, background_colors[i]); + url_colors[i] = (dark_contrast > light_contrast) ? dark_url : light_url; + + brushes[i] = CreateSolidBrush(background_colors[i]); + } +} + +AutocompletePopup::DrawLineInfo::~DrawLineInfo() { + for (int i = 0; i < MAX_STATUS_ENTRIES; ++i) + DeleteObject(brushes[i]); +} + +// static +double AutocompletePopup::DrawLineInfo::LuminosityContrast(COLORREF color1, + COLORREF color2) { + // This algorithm was adapted from the following text at + // http://juicystudio.com/article/luminositycontrastratioalgorithm.php : + // + // "[Luminosity contrast can be calculated as] (L1+.05) / (L2+.05) where L is + // luminosity and is defined as .2126*R + .7152*G + .0722B using linearised + // R, G, and B values. Linearised R (for example) = (R/FS)^2.2 where FS is + // full scale value (255 for 8 bit color channels). L1 is the higher value + // (of text or background) and L2 is the lower value. + // + // The Gamma correction and RGB constants are derived from the Standard + // Default Color Space for the Internet (sRGB), and the 0.05 offset is + // included to compensate for contrast ratios that occur when a value is at + // or near zero, and for ambient light effects. + const double l1 = Luminosity(color1); + const double l2 = Luminosity(color2); + return (l1 > l2) ? ((l1 + 0.05) / (l2 + 0.05)) : ((l2 + 0.05) / (l1 + 0.05)); +} + +// static +double AutocompletePopup::DrawLineInfo::Luminosity(COLORREF color) { + // See comments in LuminosityContrast(). + const double linearised_r = + pow(static_cast<double>(GetRValue(color)) / 255.0, 2.2); + const double linearised_g = + pow(static_cast<double>(GetGValue(color)) / 255.0, 2.2); + const double linearised_b = + pow(static_cast<double>(GetBValue(color)) / 255.0, 2.2); + return (0.2126 * linearised_r) + (0.7152 * linearised_g) + + (0.0722 * linearised_b); +} + +COLORREF AutocompletePopup::DrawLineInfo::AlphaBlend(COLORREF foreground, + COLORREF background, + BYTE alpha) { + if (alpha == 0) + return background; + else if (alpha == 0xff) + return foreground; + + return RGB( + ((GetRValue(foreground) * alpha) + + (GetRValue(background) * (0xff - alpha))) / 0xff, + ((GetGValue(foreground) * alpha) + + (GetGValue(background) * (0xff - alpha))) / 0xff, + ((GetBValue(foreground) * alpha) + + (GetBValue(background) * (0xff - alpha))) / 0xff); +} diff --git a/chrome/browser/autocomplete/autocomplete_popup.h b/chrome/browser/autocomplete/autocomplete_popup.h new file mode 100644 index 0000000..e96bc31 --- /dev/null +++ b/chrome/browser/autocomplete/autocomplete_popup.h @@ -0,0 +1,426 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#ifndef CHROME_BROWSER_AUTOCOMPLETE_AUTOCOMPLETE_POPUP_H__ +#define CHROME_BROWSER_AUTOCOMPLETE_AUTOCOMPLETE_POPUP_H__ + +#include <atlbase.h> +#include <atlapp.h> +#include <atlcrack.h> +#include <atlmisc.h> + +#include "base/task.h" +#include "base/timer.h" +#include "base/win_util.h" +#include "chrome/browser/autocomplete/autocomplete.h" +#include "chrome/common/gfx/chrome_font.h" +#include "chrome/views/view.h" + +#define AUTOCOMPLETEPOPUP_CLASSNAME L"Chrome_AutocompletePopup" + +class AutocompleteEdit; +class Profile; +class SkBitmap; +class MirroringContext; + +// This class implements a popup window used by AutocompleteEdit to display +// autocomplete results. +class AutocompletePopup + : public CWindowImpl<AutocompletePopup, CWindow, CControlWinTraits>, + public ACControllerListener, + public Task { + public: + DECLARE_WND_CLASS_EX(AUTOCOMPLETEPOPUP_CLASSNAME, + ((win_util::GetWinVersion() < win_util::WINVERSION_XP) ? + 0 : CS_DROPSHADOW), COLOR_WINDOW) + + BEGIN_MSG_MAP(AutocompletePopup) + MSG_WM_ERASEBKGND(OnEraseBkgnd) + MSG_WM_LBUTTONDOWN(OnLButtonDown) + MSG_WM_MBUTTONDOWN(OnMButtonDown) + MSG_WM_LBUTTONUP(OnLButtonUp) + MSG_WM_MBUTTONUP(OnMButtonUp) + MSG_WM_MOUSEACTIVATE(OnMouseActivate) + MSG_WM_MOUSELEAVE(OnMouseLeave) + MSG_WM_MOUSEMOVE(OnMouseMove) + MSG_WM_PAINT(OnPaint) + END_MSG_MAP() + + AutocompletePopup(const ChromeFont& font, + AutocompleteEdit* editor, + Profile* profile); + ~AutocompletePopup(); + + // Invoked when the profile has changed. + void SetProfile(Profile* profile); + + // Gets autocomplete results for the given text. If there are results, opens + // the popup if necessary and fills it with the new data. Otherwise, closes + // the popup if necessary. + // + // |prevent_inline_autocomplete| is true if the generated result set should + // not require inline autocomplete for the default match. This is difficult + // to explain in the abstract; the practical use case is that after the user + // deletes text in the edit, the HistoryURLProvider should make sure not to + //promote a match requiring inline autocomplete too highly. + void StartAutocomplete(const std::wstring& text, + const std::wstring& desired_tld, + bool prevent_inline_autocomplete); + + // Closes the window and cancels any pending asynchronous queries + void StopAutocomplete(); + + // Returns true if no autocomplete query is currently running. + bool query_in_progress() const { return query_in_progress_; } + + // Returns true if the popup is currently open. + bool is_open() const { return m_hWnd != NULL; } + + // Returns the URL for the selected match. If an update is in progress, + // "selected" means "default in the latest results". If there are no + // results, returns the empty string. + // + // If |transition_type| is non-NULL, it will be set to the appropriate + // transition type for the selected entry (TYPED or GENERATED). + // + // If |is_history_what_you_typed_match| is non-NULL, it will be set based on + // the selected entry's is_history_what_you_typed value. + // + // If |alternate_nav_url| is non-NULL, it will be set to the alternate + // navigation URL for |url| if one exists, or left unchanged otherwise. See + // comments on AutocompleteResult::GetAlternateNavURL(). + std::wstring URLsForCurrentSelection( + PageTransition::Type* transition, + bool* is_history_what_you_typed_match, + std::wstring* alternate_nav_url) const; + + // This is sort of a hybrid between StartAutocomplete() and + // URLForCurrentSelection(). When the popup isn't open and the user hits + // enter, we want to get the default result for the user's input immediately, + // and not open the popup, continue running autocomplete, etc. Therefore, + // this does a query for only the synchronously available results for the + // provided input parameters, sets |transition|, + // |is_history_what_you_typed_match|, and |alternate_nav_url| (if applicable) + // based on the default match, and returns its url. |transition|, + // |is_history_what_you_typed_match| and/or |alternate_nav_url| may be null, + // in which case they are not updated. + // + // If there are no matches for |text|, leaves the outparams unset and returns + // the empty string. + std::wstring URLsForDefaultMatch(const std::wstring& text, + const std::wstring& desired_tld, + PageTransition::Type* transition, + bool* is_history_what_you_typed_match, + std::wstring* alternate_nav_url); + + // Returns a pointer to a heap-allocated AutocompleteLog containing the + // current input text, selected match, and result set. The caller is + // responsible for deleting the object. + AutocompleteLog* GetAutocompleteLog(); + + // Immediately updates and opens the popup if necessary, then moves the + // current selection down (|count| > 0) or up (|count| < 0), clamping to the + // first or last result if necessary. If |count| == 0, the selection will be + // unchanged, but the popup will still redraw and modify the text in the + // AutocompleteEdit. + void Move(int count); + + // Called when the user hits shift-delete. This should determine if the item + // can be removed from history, and if so, remove it and update the popup. + void TryDeletingCurrentItem(); + + // ACControllerListener - called when more autocomplete data is available or + // when the query is complete. + // + // When the input for the current query has a provider affinity, we try to + // keep the current result set's default match as the new default match. + virtual void OnAutocompleteUpdate(bool updated_result, bool query_complete); + + // Task - called when either timer fires. Calls CommitLatestResults(). + virtual void Run(); + + // Returns the AutocompleteController used by this popup. + AutocompleteController* autocomplete_controller() const { + return controller_.get(); + } + + const AutocompleteResult* latest_result() const { + return &latest_result_; + } + + // The match the user has manually chosen, if any. + AutocompleteResult::Selection manually_selected_match_; + + // The token value for selected_line_, hover_line_ and functions dealing with + // a "line number" that indicates "no line". + static const size_t kNoMatch = -1; + + private: + // Caches GDI objects and information for drawing. + struct DrawLineInfo { + enum LineStatus { + NORMAL = 0, + HOVERED, + SELECTED, + MAX_STATUS_ENTRIES + }; + + explicit DrawLineInfo(const ChromeFont& font); + ~DrawLineInfo(); + + static COLORREF AlphaBlend(COLORREF foreground, + COLORREF background, + BYTE alpha); + + static const wchar_t ellipsis_str[]; + + ChromeFont regular_font; // Fonts used for rendering AutocompleteMatches. + ChromeFont bold_font; + int font_height; // Height (in pixels) of a line of text w/o + // padding. + int line_height; // Height (in pixels) of a line of text w/padding. + int ave_char_width; // Width (in pixels) of an average character of + // the regular font. + int ellipsis_width; // Width (in pixels) of the ellipsis_str. + + // colors + COLORREF background_colors[MAX_STATUS_ENTRIES]; + COLORREF text_colors[MAX_STATUS_ENTRIES]; + COLORREF url_colors[MAX_STATUS_ENTRIES]; + + // brushes + HBRUSH brushes[MAX_STATUS_ENTRIES]; + + private: + static double LuminosityContrast(COLORREF color1, COLORREF color2); + static double Luminosity(COLORREF color); + }; + + // message handlers + LRESULT OnEraseBkgnd(HDC hdc) { + // We do all needed erasing ourselves in OnPaint, so the only thing that + // WM_ERASEBKGND will do is cause flicker. Disable it by just returning + // nonzero here ("erase completed") without doing anything. + return 1; + } + void OnLButtonDown(UINT keys, const CPoint& point); + void OnMButtonDown(UINT keys, const CPoint& point); + void OnLButtonUp(UINT keys, const CPoint& point); + void OnMButtonUp(UINT keys, const CPoint& point); + LRESULT OnMouseActivate(HWND window, UINT hit_test, UINT mouse_message); + void OnMouseLeave(); + void OnMouseMove(UINT keys, const CPoint& point); + void OnPaint(HDC hdc); + + // Called by On*ButtonUp() to do the actual work of handling a button + // release. Opens the item at the given coordinate, using the supplied + // disposition. + void OnButtonUp(const CPoint& point, WindowOpenDisposition disposition); + + // Sets the correct default match in latest_result_, then updates the popup + // appearance to match. If |immediately| is true this update happens + // synchronously; otherwise, it's deferred until the next scheduled update. + void SetDefaultMatchAndUpdate(bool immediately); + + // If an update is pending or |force| is true, immediately updates result_ to + // match latest_result_, and calls UpdatePopup() to reflect those changes + // back to the user. + void CommitLatestResults(bool force); + + // Redraws the popup window to match any changes in result_; this may mean + // opening or closing the window. + void UpdatePopupAppearance(); + + // Gives the topmost y coordinate within |line|, which should be within the + // range of valid lines. + int LineTopPixel(size_t line) const; + + // Converts the given y-coordinate to a line. Due to drawing slop (window + // borders, etc.), |y| might be within the window but outside the range of + // pixels which correspond to lines; in this case the result will be clamped, + // i.e., the top and bottom lines will be treated as extending to the top and + // bottom edges of the window, respectively. + size_t PixelToLine(int y) const; + + // Call to change the hovered line. |line| should be within the range of + // valid lines (to enable hover) or kNoMatch (to disable hover). + void SetHoveredLine(size_t line); + + // Call to change the selected line. This will update all state and repaint + // the necessary parts of the window, as well as updating the edit with the + // new temporary text. |line| should be within the range of valid lines. + // NOTE: This assumes the popup is open, and thus both old and new values for + // the selected line should not be kNoMatch. + void SetSelectedLine(size_t line); + + // Invalidates one line of the autocomplete popup. + void InvalidateLine(size_t line); + + // Draws a light border around the inside of the window with the given client + // rectangle and DC. + void DrawBorder(const RECT& rc, HDC dc); + + // Draw a string at the specified location with the specified style. + // This function is a wrapper function of the DrawText() function to handle + // bidirectional strings. + // Parameters + // * dc [in] (HDC) + // Represents the display context to render the given string. + // * x [in] (int) + // Specifies the left of the bounding rectangle, + // * y [in] (int) + // Specifies the top of the bounding rectangle, + // * max_x [in] (int) + // Specifies the right of the bounding rectangle. + // * text [in] (const wchar_t*) + // Specifies the pointer to the string to be rendered. + // * length [in] (int) + // Specifies the number of characters in the string. + // * style [in] (int) + // Specifies the classifications for this string. + // This value is a combination of the following values: + // - ACMatchClassifications::NONE + // - ACMatchClassifications::URL + // - ACMatchClassifications::MATCH + // - ACMatchClassifications::DIM + // * status [in] (const DrawLineInfo::LineStatus) + // Specifies the classifications for this line. + // * context [in] (const MirroringContext*) + // Specifies the context used for mirroring the x-coordinates. + // * text_direction_is_rtl [in] (bool) + // Determines whether we need to render the string as an RTL string, which + // impacts, for example, which side leading/trailing whitespace and + // punctuation appear on. + // Return Values + // * a positive value + // Represents the width of the displayed string, in pixels. + int DrawString(HDC dc, + int x, + int y, + int max_x, + const wchar_t* text, + int length, + int style, + const DrawLineInfo::LineStatus status, + const MirroringContext* context, + bool text_direction_is_rtl) const; + + // Draws a string from the autocomplete controller which can have specially + // marked "match" portions. + void DrawMatchFragments(HDC dc, + const std::wstring& text, + const ACMatchClassifications& classifications, + int x, + int y, + int max_x, + DrawLineInfo::LineStatus status) const; + + // Draws one line of the text in the box. + void DrawEntry(HDC dc, + const RECT& client_rect, + size_t line, + DrawLineInfo::LineStatus status, + bool all_descriptions_empty, + bool starred) const; + + // Draws the star at the specified location + void DrawStar(HDC dc, int x, int y) const; + + // Gets the selected keyword or keyword hint for the given match. Returns + // true if |keyword| represents a keyword hint, or false if |keyword| + // represents a selected keyword. (|keyword| will always be set [though + // possibly to the empty string], and you cannot have both a selected keyword + // and a keyword hint simultaneously.) + bool GetKeywordForMatch(const AutocompleteMatch& match, + std::wstring* keyword); + + AutocompleteEdit* editor_; + scoped_ptr<AutocompleteController> controller_; + + // Profile for current tab. + Profile* profile_; + + // Cached GDI information for drawing. + DrawLineInfo line_info_; + + // The input for the current query. + AutocompleteInput input_; + + // Data from the autocomplete query. + AutocompleteResult result_; + + // True if an autocomplete query is currently running. + bool query_in_progress_; + + // The latest result available from the autocomplete service. This may be + // different than result_ if we've gotten results from our providers that we + // haven't yet shown the user. If more results may be coming, we'll wait to + // display these in hopes of minimizing flicker; see coalesce_timer_. + AutocompleteResult latest_result_; + + // True when there are newer results in latest_result_ than in result_ and + // the popup has not been updated to show them. + bool update_pending_; + + // Timer that tracks how long it's been since the last provider update we + // received. Instead of displaying each update immediately, we batch updates + // into groups, which reduces flicker. + // + // NOTE: Both coalesce_timer_ and max_delay_timer_ (below) are set up during + // the constructor, and are guaranteed non-NULL for the life of the popup. + scoped_ptr<Timer> coalesce_timer_; + + // Timer that tracks how long it's been since the last time we updated the + // onscreen results. This is used to ensure that the popup is somewhat + // responsive even when the user types continuously. + scoped_ptr<Timer> max_delay_timer_; + + // The line that's currently hovered. If we're not drawing a hover rect, + // this will be kNoMatch, even if the cursor is over the popup contents. + size_t hovered_line_; + + // When hover_line_ is kNoMatch, this holds the screen coordinates of the + // mouse position when hover tracking was turned off. If the mouse moves to a + // point over the popup that has different coordinates, hover tracking will be + // re-enabled. When hover_line_ is a valid line, the value here is + // out-of-date and should be ignored. + CPoint last_hover_coordinates_; + + // The currently selected line. This is kNoMatch when nothing is selected, + // which should only be true when the popup is closed. + size_t selected_line_; + + // Bitmap for the star. This is owned by the ResourceBundle. + SkBitmap* star_; + + // A context used for mirroring regions. + scoped_ptr<MirroringContext> mirroring_context_; +}; + +#endif // CHROME_BROWSER_AUTOCOMPLETE_AUTOCOMPLETE_POPUP_H__ diff --git a/chrome/browser/autocomplete/autocomplete_unittest.cc b/chrome/browser/autocomplete/autocomplete_unittest.cc new file mode 100644 index 0000000..e2fc2f5 --- /dev/null +++ b/chrome/browser/autocomplete/autocomplete_unittest.cc @@ -0,0 +1,361 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "base/message_loop.h" +#include "base/ref_counted.h" +#include "chrome/browser/autocomplete/autocomplete.h" +#include "testing/gtest/include/gtest/gtest.h" + +// identifiers for known autocomplete providers +#define HISTORY_IDENTIFIER L"Chrome:History" +#define SEARCH_IDENTIFIER L"google.com/websearch/en" + +namespace { + +const int num_results_per_provider = 3; + +// Autocomplete provider that provides known results. Note that this is +// refcounted so that it can also be a task on the message loop. +class TestProvider : public AutocompleteProvider { + public: + TestProvider(int relevance, const std::wstring& prefix) + : AutocompleteProvider(NULL, NULL, ""), + relevance_(relevance), + prefix_(prefix) { + } + + virtual void Start(const AutocompleteInput& input, + bool minimal_changes, + bool synchronous_only); + + void set_listener(ACProviderListener* listener) { + listener_ = listener; + } + + private: + void Run(); + + void AddResults(int start_at, int num); + + int relevance_; + const std::wstring prefix_; +}; + +void TestProvider::Start(const AutocompleteInput& input, + bool minimal_changes, + bool synchronous_only) { + if (minimal_changes) + return; + + matches_.clear(); + + // Generate one result synchronously, the rest later. + AddResults(0, 1); + + if (!synchronous_only) { + done_ = false; + MessageLoop::current()->PostTask(FROM_HERE, NewRunnableMethod( + this, &TestProvider::Run)); + } +} + +void TestProvider::Run() { + DCHECK(num_results_per_provider > 0); + AddResults(1, num_results_per_provider); + done_ = true; + DCHECK(listener_); + listener_->OnProviderUpdate(true); +} + +void TestProvider::AddResults(int start_at, int num) { + for (int i = start_at; i < num; i++) { + AutocompleteMatch match(this, relevance_ - i, false); + + wchar_t str[16]; + swprintf_s(str, L"%d", i); + match.fill_into_edit = prefix_ + str; + match.destination_url = match.fill_into_edit; + + match.contents = match.destination_url; + match.contents_class.push_back( + ACMatchClassification(0, ACMatchClassification::NONE)); + match.description = match.destination_url; + match.description_class.push_back( + ACMatchClassification(0, ACMatchClassification::NONE)); + + matches_.push_back(match); + } +} + +class AutocompleteProviderTest : public testing::Test, + public ACControllerListener { + public: + // ACControllerListener + virtual void OnAutocompleteUpdate(bool updated_result, bool query_complete); + + protected: + // testing::Test + virtual void SetUp(); + + void ResetController(bool same_destinations); + + // Runs a query on the input "a", and makes sure both providers' input is + // properly collected. + void RunTest(); + + // These providers are owned by the controller once it's created. + ACProviders providers_; + + AutocompleteResult result_; + + private: + scoped_ptr<AutocompleteController> controller_; +}; + +void AutocompleteProviderTest::SetUp() { + ResetController(false); +} + +void AutocompleteProviderTest::ResetController(bool same_destinations) { + // Forget about any existing providers. The controller owns them and will + // Release() them below, when we delete it during the call to reset(). + providers_.clear(); + + // Construct two new providers, with either the same or different prefixes. + TestProvider* providerA = new TestProvider(num_results_per_provider, L"a"); + providerA->AddRef(); + providers_.push_back(providerA); + + TestProvider* providerB = new TestProvider(num_results_per_provider * 2, + same_destinations ? L"a" : L"b"); + providerB->AddRef(); + providers_.push_back(providerB); + + // Reset the controller to contain our new providers. + AutocompleteController* controller = + new AutocompleteController(this, providers_); + controller_.reset(controller); + providerA->set_listener(controller); + providerB->set_listener(controller); +} + +void AutocompleteProviderTest::OnAutocompleteUpdate(bool updated_result, + bool query_complete) { + controller_->GetResult(&result_); + if (query_complete) + MessageLoop::current()->Quit(); +} + +void AutocompleteProviderTest::RunTest() { + result_.Reset(); + const AutocompleteInput input(L"a", std::wstring(), true); + EXPECT_FALSE(controller_->Start(input, false, false)); + + // The message loop will terminate when all autocomplete input has been + // collected. + MessageLoop::current()->Run(); +} + +} // namespace + +std::ostream& operator<<(std::ostream& os, + const AutocompleteResult::const_iterator& iter) { + return os << static_cast<const AutocompleteMatch*>(&(*iter)); +} + +// Tests that the default selection is set properly when updating results. +TEST_F(AutocompleteProviderTest, Query) { + RunTest(); + + // Make sure the default match gets set to the highest relevance match when + // we have no preference. The highest relevance matches should come from + // the second provider. + AutocompleteResult::Selection selection; + result_.SetDefaultMatch(selection); + EXPECT_EQ(num_results_per_provider * 2, result_.size()); // two providers + ASSERT_NE(result_.end(), result_.default_match()); + EXPECT_EQ(providers_[1], result_.default_match()->provider); + + // Change provider affinity. + selection.provider_affinity = providers_[0]; + result_.SetDefaultMatch(selection); + ASSERT_NE(result_.end(), result_.default_match()); + EXPECT_EQ(providers_[0], result_.default_match()->provider); +} + +TEST_F(AutocompleteProviderTest, UpdateSelection) { + RunTest(); + + // An empty selection should simply result in the default match overall. + AutocompleteResult::Selection selection; + result_.SetDefaultMatch(selection); + ASSERT_NE(result_.end(), result_.default_match()); + EXPECT_EQ(providers_[1], result_.default_match()->provider); + + // ...As should specifying a provider that didn't return results. + scoped_refptr<TestProvider> test_provider = new TestProvider(0, L""); + selection.provider_affinity = test_provider.get(); + result_.SetDefaultMatch(selection); + ASSERT_NE(result_.end(), result_.default_match()); + EXPECT_EQ(providers_[1], result_.default_match()->provider); + selection.provider_affinity = NULL; + test_provider = NULL; + + // ...As should specifying a destination URL that doesn't exist, and no + // provider. + selection.destination_url = L"garbage"; + selection.provider_affinity = NULL; + result_.SetDefaultMatch(selection); + ASSERT_NE(result_.end(), result_.default_match()); + EXPECT_EQ(providers_[1], result_.default_match()->provider); + delete selection.provider_affinity; + + // Specifying a valid provider should result in the default match from that + // provider. + selection.destination_url.clear(); + selection.provider_affinity = providers_[0]; + result_.SetDefaultMatch(selection); + ASSERT_NE(result_.end(), result_.default_match()); + EXPECT_EQ(providers_[0], result_.default_match()->provider); + + // ...And nothing should change if we specify a destination that doesn't + // exist. + selection.destination_url = L"garbage"; + result_.SetDefaultMatch(selection); + ASSERT_NE(result_.end(), result_.default_match()); + EXPECT_EQ(providers_[0], result_.default_match()->provider); + + // Specifying a particular URL should match that URL. + std::wstring non_default_url_from_provider_0(L"a2"); + selection.destination_url = non_default_url_from_provider_0; + selection.provider_affinity = providers_[0]; + result_.SetDefaultMatch(selection); + ASSERT_NE(result_.end(), result_.default_match()); + EXPECT_EQ(non_default_url_from_provider_0, + result_.default_match()->destination_url); + + // ...Even when we ask for a different provider. + selection.provider_affinity = providers_[1]; + result_.SetDefaultMatch(selection); + ASSERT_NE(result_.end(), result_.default_match()); + EXPECT_EQ(non_default_url_from_provider_0, + result_.default_match()->destination_url); + + // ...Or when we don't ask for a provider at all. + selection.provider_affinity = NULL; + result_.SetDefaultMatch(selection); + ASSERT_NE(result_.end(), result_.default_match()); + EXPECT_EQ(non_default_url_from_provider_0, + result_.default_match()->destination_url); +} + +TEST_F(AutocompleteProviderTest, RemoveDuplicates) { + // Set up the providers to provide duplicate results. + ResetController(true); + + RunTest(); + + // Make sure all the first provider's results were eliminated by the second + // provider's. + EXPECT_EQ(num_results_per_provider, result_.size()); + for (AutocompleteResult::const_iterator i(result_.begin()); + i != result_.end(); ++i) + EXPECT_EQ(providers_[1], i->provider); + + // Set things back to the default for the benefit of any tests that run after + // us. + ResetController(false); +} + +TEST(AutocompleteTest, InputType) { + struct test_data { + const wchar_t* input; + const AutocompleteInput::Type type; + } input_cases[] = { + { L"", AutocompleteInput::INVALID }, + { L"?", AutocompleteInput::FORCED_QUERY }, + { L"?foo", AutocompleteInput::FORCED_QUERY }, + { L"?foo bar", AutocompleteInput::FORCED_QUERY }, + { L"?http://foo.com/bar", AutocompleteInput::FORCED_QUERY }, + { L"foo", AutocompleteInput::UNKNOWN }, + { L"foo.com", AutocompleteInput::URL }, + { L"foo/bar", AutocompleteInput::URL }, + { L"foo/bar baz", AutocompleteInput::UNKNOWN }, + { L"http://foo/bar baz", AutocompleteInput::URL }, + { L"foo bar", AutocompleteInput::QUERY }, + { L"link:foo.com", AutocompleteInput::UNKNOWN }, + { L"www.foo.com:81", AutocompleteInput::URL }, + { L"localhost:8080", AutocompleteInput::URL }, + { L"en.wikipedia.org/wiki/James Bond", AutocompleteInput::URL }, + // In Chrome itself, mailto: will get handled by ShellExecute, but in + // unittest mode, we don't have the data loaded in the external protocol + // handler to know this. + // { L"mailto:abuse@foo.com", AutocompleteInput::URL }, + { L"view-source:http://www.foo.com/", AutocompleteInput::URL }, + { L"javascript:alert(\"Hey there!\");", AutocompleteInput::URL }, + { L"C:\\Program Files", AutocompleteInput::URL }, + { L"\\\\Server\\Folder\\File", AutocompleteInput::URL }, + { L"http://foo.com/", AutocompleteInput::URL }, + { L"127.0.0.1", AutocompleteInput::URL }, + { L"browser.tabs.closeButtons", AutocompleteInput::UNKNOWN }, + }; + + for (int i = 0; i < arraysize(input_cases); ++i) { + AutocompleteInput input(input_cases[i].input, std::wstring(), true); + EXPECT_EQ(input_cases[i].type, input.type()) << "Input: " << + input_cases[i].input; + } +} + +// Test that we can properly compare matches' relevance when at least one is +// negative. +TEST(AutocompleteMatch, MoreRelevant) { + struct RelevantCases { + int r1; + int r2; + bool expected_result; + } cases[] = { + { 10, 0, true }, + { 10, -5, true }, + { -5, 10, false }, + { 0, 10, false }, + { -10, -5, true }, + { -5, -10, false }, + }; + + AutocompleteMatch m1; + AutocompleteMatch m2; + + for (int i = 0; i < arraysize(cases); ++i) { + m1.relevance = cases[i].r1; + m2.relevance = cases[i].r2; + EXPECT_EQ(cases[i].expected_result, + AutocompleteMatch::MoreRelevant(m1, m2)); + } +} diff --git a/chrome/browser/autocomplete/edit_drop_target.cc b/chrome/browser/autocomplete/edit_drop_target.cc new file mode 100644 index 0000000..7be3259 --- /dev/null +++ b/chrome/browser/autocomplete/edit_drop_target.cc @@ -0,0 +1,177 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "chrome/browser/autocomplete/edit_drop_target.h" + +#include "base/string_util.h" +#include "chrome/browser/autocomplete/autocomplete_edit.h" +#include "chrome/common/os_exchange_data.h" +#include "googleurl/src/gurl.h" + +namespace { + +// A helper method for determining a valid DROPEFFECT given the allowed +// DROPEFFECTS. We prefer copy over link. +DWORD CopyOrLinkDropEffect(DWORD effect) { + if (effect & DROPEFFECT_COPY) + return DROPEFFECT_COPY; + if (effect & DROPEFFECT_LINK) + return DROPEFFECT_LINK; + return DROPEFFECT_NONE; +} + +} + +EditDropTarget::EditDropTarget(AutocompleteEdit* edit) + : BaseDropTarget(edit->m_hWnd), + edit_(edit), + drag_has_url_(false), + drag_has_string_(false) { +} + +DWORD EditDropTarget::OnDragEnter(IDataObject* data_object, + DWORD key_state, + POINT cursor_position, + DWORD effect) { + OSExchangeData os_data(data_object); + drag_has_url_ = os_data.HasURL(); + drag_has_string_ = !drag_has_url_ && os_data.HasString(); + if (drag_has_url_) { + if (edit_->in_drag()) { + // The edit we're associated with originated the drag. No point in + // allowing the user to drop back on us. + drag_has_url_ = false; + } + // NOTE: it would be nice to visually show all the text is going to + // be replaced by selecting all, but this caused painting problems. In + // particular the flashing caret would appear outside the edit! For now + // we stick with no visual indicator other than that shown own the mouse + // cursor. + } + return OnDragOver(data_object, key_state, cursor_position, effect); +} + +DWORD EditDropTarget::OnDragOver(IDataObject* data_object, + DWORD key_state, + POINT cursor_position, + DWORD effect) { + if (drag_has_url_) + return CopyOrLinkDropEffect(effect); + + if (drag_has_string_) { + UpdateDropHighlightPosition(cursor_position); + if (edit_->drop_highlight_position() == -1 && edit_->in_drag()) + return DROPEFFECT_NONE; + if (edit_->in_drag()) { + // The edit we're associated with originated the drag. Do the normal drag + // behavior. + DCHECK((effect & DROPEFFECT_COPY) && (effect & DROPEFFECT_MOVE)); + return (key_state & MK_CONTROL) ? DROPEFFECT_COPY : DROPEFFECT_MOVE; + } + // Our edit didn't originate the drag, only allow link or copy. + return CopyOrLinkDropEffect(effect); + } + + return DROPEFFECT_NONE; +} + +void EditDropTarget::OnDragLeave(IDataObject* data_object) { + ResetDropHighlights(); +} + +DWORD EditDropTarget::OnDrop(IDataObject* data_object, + DWORD key_state, + POINT cursor_position, + DWORD effect) { + OSExchangeData os_data(data_object); + + if (drag_has_url_) { + GURL url; + std::wstring title; + if (os_data.GetURLAndTitle(&url, &title)) { + edit_->SetUserText(UTF8ToWide(url.spec())); + edit_->AcceptInput(CURRENT_TAB, true); + return CopyOrLinkDropEffect(effect); + } + } else if (drag_has_string_) { + int string_drop_position = edit_->drop_highlight_position(); + std::wstring text; + if ((string_drop_position != -1 || !edit_->in_drag()) && + os_data.GetString(&text)) { + DCHECK(string_drop_position == -1 || + ((string_drop_position >= 0) && + (string_drop_position <= edit_->GetTextLength()))); + const DWORD drop_operation = + OnDragOver(data_object, key_state, cursor_position, effect); + if (edit_->in_drag()) { + if (drop_operation == DROPEFFECT_MOVE) + edit_->MoveSelectedText(string_drop_position); + else + edit_->InsertText(string_drop_position, text); + } else { + edit_->PasteAndGo(CollapseWhitespace(text, true)); + } + ResetDropHighlights(); + return drop_operation; + } + } + + ResetDropHighlights(); + + return DROPEFFECT_NONE; +} + +void EditDropTarget::UpdateDropHighlightPosition( + const POINT& cursor_screen_position) { + if (drag_has_string_) { + POINT client_position = cursor_screen_position; + ScreenToClient(edit_->m_hWnd, &client_position); + int drop_position = edit_->CharFromPos(client_position); + if (edit_->in_drag()) { + // Our edit originated the drag, don't allow a drop if over the selected + // region. + LONG sel_start, sel_end; + edit_->GetSel(sel_start, sel_end); + if ((sel_start != sel_end) && (drop_position >= sel_start) && + (drop_position <= sel_end)) + drop_position = -1; + } else { + // A drop from a source other than the edit replaces all the text, so + // we don't show the drop location. See comment in OnDragEnter as to why + // we don't try and select all here. + drop_position = -1; + } + edit_->SetDropHighlightPosition(drop_position); + } +} + +void EditDropTarget::ResetDropHighlights() { + if (drag_has_string_) + edit_->SetDropHighlightPosition(-1); +} diff --git a/chrome/browser/autocomplete/edit_drop_target.h b/chrome/browser/autocomplete/edit_drop_target.h new file mode 100644 index 0000000..d350c6a --- /dev/null +++ b/chrome/browser/autocomplete/edit_drop_target.h @@ -0,0 +1,83 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#ifndef CHROME_BROWSER_AUTOCOMPLETE_EDIT_DROP_TARGET_H__ +#define CHROME_BROWSER_AUTOCOMPLETE_EDIT_DROP_TARGET_H__ + +#include "base/base_drop_target.h" + +class AutocompleteEdit; + +// EditDropTarget is the IDropTarget implementation installed on +// AutocompleteEdit. EditDropTarget prefers URL over plain text. A drop of a URL +// replaces all the text of the edit and navigates immediately to the URL. A +// drop of plain text from the same edit either copies or moves the selected +// text, and a drop of plain text from a source other than the edit does a paste +// and go. +class EditDropTarget : public BaseDropTarget { + public: + explicit EditDropTarget(AutocompleteEdit* edit); + + protected: + virtual DWORD OnDragEnter(IDataObject* data_object, + DWORD key_state, + POINT cursor_position, + DWORD effect); + virtual DWORD OnDragOver(IDataObject* data_object, + DWORD key_state, + POINT cursor_position, + DWORD effect); + virtual void OnDragLeave(IDataObject* data_object); + virtual DWORD OnDrop(IDataObject* data_object, + DWORD key_state, + POINT cursor_position, + DWORD effect); + + private: + // If dragging a string, the drop highlight position of the edit is reset + // based on the mouse position. + void UpdateDropHighlightPosition(const POINT& cursor_screen_position); + + // Resets the visual drop indicates we install on the edit. + void ResetDropHighlights(); + + // The edit we're the drop target for. + AutocompleteEdit* edit_; + + // If true, the drag session contains a URL. + bool drag_has_url_; + + // If true, the drag session contains a string. If drag_has_url_ is true, + // this is false regardless of whether the clipboard has a string. + bool drag_has_string_; + + DISALLOW_EVIL_CONSTRUCTORS(EditDropTarget); +}; + +#endif // CHROME_BROWSER_AUTOCOMPLETE_EDIT_DROP_TARGET_H__ diff --git a/chrome/browser/autocomplete/history_contents_provider.cc b/chrome/browser/autocomplete/history_contents_provider.cc new file mode 100644 index 0000000..c68b17c --- /dev/null +++ b/chrome/browser/autocomplete/history_contents_provider.cc @@ -0,0 +1,264 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "chrome/browser/autocomplete/history_contents_provider.h" + +#include "base/string_util.h" +#include "chrome/browser/history/query_parser.h" +#include "chrome/browser/profile.h" +#include "net/base/net_util.h" + +namespace { + +// Number of days to search for full text results. The longer this is, the more +// time it will take. +const int kDaysToSearch = 30; + +// When processing the results from the history query, this structure points to +// a single result. It allows the results to be sorted and processed without +// modifying the larger and slower results structure. +struct MatchReference { + const history::URLResult* result; + int relevance; // Score of relevance computed by CalculateRelevance. +}; + +// This is a > operator for MatchReference. +bool CompareMatchRelevance(const MatchReference& a, const MatchReference& b) { + if (a.relevance != b.relevance) + return a.relevance > b.relevance; + + // Want results in reverse-chronological order all else being equal. + return a.result->last_visit() > b.result->last_visit(); +} + +} // namespace + +using history::HistoryDatabase; + +void HistoryContentsProvider::Start(const AutocompleteInput& input, + bool minimal_changes, + bool synchronous_only) { + matches_.clear(); + + if (input.text().empty() || (input.type() == AutocompleteInput::INVALID) || + // The history service must exist. + (!history_service_ && + (!profile_ || !profile_->GetHistoryService(Profile::EXPLICIT_ACCESS)))) { + Stop(); + return; + } + + // TODO(pkasting): http://b/888148 We disallow URL input and "URL-like" input + // (REQUESTED_URL or UNKNOWN with dots) because we get poor results for it, + // but we could get better results if we did better tokenizing instead. + if ((input.type() == AutocompleteInput::URL) || + (((input.type() == AutocompleteInput::REQUESTED_URL) || + (input.type() == AutocompleteInput::UNKNOWN)) && + (input.text().find('.') != std::wstring::npos))) { + Stop(); + return; + } + + // Change input type and reset relevance counters, so matches will be marked + // up properly. + input_type_ = input.type(); + star_title_count_ = star_contents_count_ = title_count_ = contents_count_ = 0; + + // Decide what to do about any previous query/results. + if (!minimal_changes) { + // Any in-progress request is irrelevant, cancel it. + Stop(); + } else if (have_results_) { + // We finished the previous query and still have its results. Mark them up + // again for the new input. + ConvertResults(); + return; + } else if (!done_) { + // We're still running the previous query. If we're allowed to keep running + // it, do so, and when it finishes, its results will get marked up for this + // new input. In synchronous_only mode, just cancel. + if (synchronous_only) + Stop(); + return; + } + + if (!synchronous_only) { + HistoryService* history = history_service_ ? history_service_ : + profile_->GetHistoryService(Profile::EXPLICIT_ACCESS); + if (history) { + done_ = false; + + history::QueryOptions options; + options.SetRecentDayRange(kDaysToSearch); + options.most_recent_visit_only = true; + options.max_count = kMaxMatchCount; + history->QueryHistory(input.text(), options, &request_consumer_, + NewCallback(this, &HistoryContentsProvider::QueryComplete)); + } + } +} + +void HistoryContentsProvider::Stop() { + done_ = true; + request_consumer_.CancelAllRequests(); + + // Clear the results. We swap in an empty one as the easy way to clear it. + history::QueryResults empty_results; + results_.Swap(&empty_results); + have_results_ = false; + + db_match_count_ = 0; +} + +void HistoryContentsProvider::QueryComplete(HistoryService::Handle handle, + history::QueryResults* results) { + results_.Swap(results); + have_results_ = true; + ConvertResults(); + + db_match_count_ = static_cast<int>(results_.size()); + done_ = true; + if (listener_) + listener_->OnProviderUpdate(!matches_.empty()); +} + +void HistoryContentsProvider::ConvertResults() { + // Make the result references and score the results. + std::vector<MatchReference> result_refs; + result_refs.reserve(results_.size()); + for (size_t i = 0; i < results_.size(); i++) { + MatchReference ref; + ref.result = &results_[i]; + ref.relevance = CalculateRelevance(*ref.result); + result_refs.push_back(ref); + } + + // Get the top matches and add them. Always do max number of matches the popup + // will show plus one. This ensures that if the other providers provide the + // exact same set of results, and the db only has max_matches + 1 results + // available for this query, we know the last one. + // + // This is done to avoid having the history search shortcut show + // 'See 1 previously viewed ...'. + // + // Note that AutocompleteResult::max_matches() (maximum size of the popup) + // is different than both max_matches (the provider's maximum) and + // kMaxMatchCount (the number of items we want from the history). + size_t max_for_popup = std::min(AutocompleteResult::max_matches() + 1, + result_refs.size()); + size_t max_for_provider = std::min(max_matches(), result_refs.size()); + std::partial_sort(result_refs.begin(), result_refs.begin() + max_for_popup, + result_refs.end(), &CompareMatchRelevance); + matches_.clear(); + for (size_t i = 0; i < max_for_popup; i++) { + matches_.push_back(ResultToMatch(*result_refs[i].result, + result_refs[i].relevance)); + } + + // We made more matches than the autocomplete service requested for this + // provider (see previous comment). We invert the weights for the items + // we want to get removed, but preserve their magnitude which will be used + // to fill them in with our other results. + for (size_t i = max_for_provider; i < max_for_popup; i++) + matches_[i].relevance = -matches_[i].relevance; +} + +AutocompleteMatch HistoryContentsProvider::ResultToMatch( + const history::URLResult& result, + int score) { + // TODO(sky): if matched title highlight matching words in title. + // Also show star in popup. + AutocompleteMatch match(this, score, false); + match.fill_into_edit = StringForURLDisplay(result.url(), true); + match.destination_url = UTF8ToWide(result.url().spec()); + match.contents = match.fill_into_edit; + match.contents_class.push_back( + ACMatchClassification(0, ACMatchClassification::URL)); + match.description = result.title(); + match.starred = result.starred(); + + ClassifyDescription(result, &match); + return match; +} + +void HistoryContentsProvider::ClassifyDescription( + const history::URLResult& result, + AutocompleteMatch* match) const { + const Snippet::MatchPositions& title_matches = result.title_match_positions(); + + size_t offset = 0; + if (!title_matches.empty()) { + // Classify matches in the title. + for (Snippet::MatchPositions::const_iterator i = title_matches.begin(); + i != title_matches.end(); ++i) { + if (i->first != offset) { + match->description_class.push_back( + ACMatchClassification(offset, ACMatchClassification::NONE)); + } + match->description_class.push_back( + ACMatchClassification(i->first, ACMatchClassification::MATCH)); + offset = i->second; + } + } + if (offset != result.title().size()) { + match->description_class.push_back( + ACMatchClassification(offset, ACMatchClassification::NONE)); + } +} + +int HistoryContentsProvider::CalculateRelevance( + const history::URLResult& result) { + bool in_title = !!result.title_match_positions().size(); + + switch (input_type_) { + case AutocompleteInput::UNKNOWN: + case AutocompleteInput::REQUESTED_URL: + if (result.starred()) { + return in_title ? 1000 + star_title_count_++ : + 550 + star_contents_count_++; + } else { + return in_title ? 700 + title_count_++ : + 500 + contents_count_++; + } + + case AutocompleteInput::QUERY: + case AutocompleteInput::FORCED_QUERY: + if (result.starred()) { + return in_title ? 1200 + star_title_count_++ : + 750 + star_contents_count_++; + } else { + return in_title ? 900 + title_count_++ : + 700 + contents_count_++; + } + + default: + NOTREACHED(); + return 0; + } +} diff --git a/chrome/browser/autocomplete/history_contents_provider.h b/chrome/browser/autocomplete/history_contents_provider.h new file mode 100644 index 0000000..ed1b26c --- /dev/null +++ b/chrome/browser/autocomplete/history_contents_provider.h @@ -0,0 +1,127 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#ifndef CHROME_BROWSER_AUTOCOMPLETE_HISTORY_CONTENTS_PROVIDER_H__ +#define CHROME_BROWSER_AUTOCOMPLETE_HISTORY_CONTENTS_PROVIDER_H__ + +#include "chrome/browser/autocomplete/autocomplete.h" +#include "chrome/browser/history/history.h" + +// HistoryContentsProvider is an AutocompleteProvider that provides results from +// the contents (body and/or title) of previously visited pages. Results are +// obtained asynchronously from the history service. +class HistoryContentsProvider : public AutocompleteProvider { + public: + HistoryContentsProvider(ACProviderListener* listener, Profile* profile) + : AutocompleteProvider(listener, profile, "HistoryContents"), + history_service_(NULL), + have_results_(false) { + DCHECK(profile); + } + +#ifdef UNIT_TEST + HistoryContentsProvider(ACProviderListener* listener, + HistoryService* history_service) + : AutocompleteProvider(listener, NULL, "HistoryContents"), + history_service_(history_service), + have_results_(false) { + } +#endif + + // As necessary asks the history service for the relevant results. When + // done SetResults is invoked. + virtual void Start(const AutocompleteInput& input, + bool minimal_changes, + bool synchronous_only); + + virtual void Stop(); + + // Returns the total number of matches available in the database, up to + // kMaxMatchCount, whichever is smaller. + // Return value is only valid if done() returns true. + size_t db_match_count() const { return db_match_count_; } + + // The maximum match count we'll report. If the db_match_count is greater + // than this, it will be clamped to this result. + static const int kMaxMatchCount = 50; + + private: + void QueryComplete(HistoryService::Handle handle, + history::QueryResults* results); + + // Converts each MatchingPageResult in results_ to an AutocompleteMatch and + // adds it to matches_. + void ConvertResults(); + + // Creates and returns an AutocompleteMatch from a MatchingPageResult. + AutocompleteMatch ResultToMatch(const history::URLResult& result, + int score); + + // Adds ACMatchClassifications to match from the offset positions in + // page_result. + void ClassifyDescription(const history::URLResult& result, + AutocompleteMatch* match) const; + + // Calculates and returns the relevance of a particular result. See the + // chart in autocomplete.h for the list of values this returns. + int CalculateRelevance(const history::URLResult& result); + + CancelableRequestConsumerT<int, 0> request_consumer_; + + // This is only non-null for testing, otherwise the HistoryService from the + // Profile is used. + HistoryService* history_service_; + + // The number of times we're returned each different type of result. These are + // used by CalculateRelevance. Initialized in Start. + int star_title_count_; + int star_contents_count_; + int title_count_; + int contents_count_; + + // Current autocomplete input type. + AutocompleteInput::Type input_type_; + + // Results from most recent query. These are cached so we don't have to + // re-issue queries for "minor changes" (which don't affect this provider). + history::QueryResults results_; + + // Whether results_ is valid (so we can tell invalid apart from empty). + bool have_results_; + + // Current query string. + std::wstring query_; + + // Total number of matches available in the database. + int db_match_count_; + + DISALLOW_EVIL_CONSTRUCTORS(HistoryContentsProvider); +}; + +#endif // CHROME_BROWSER_AUTOCOMPLETE_HISTORY_CONTENTS_PROVIDER_H__ diff --git a/chrome/browser/autocomplete/history_contents_provider_unittest.cc b/chrome/browser/autocomplete/history_contents_provider_unittest.cc new file mode 100644 index 0000000..d84ec78 --- /dev/null +++ b/chrome/browser/autocomplete/history_contents_provider_unittest.cc @@ -0,0 +1,179 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "base/file_util.h" +#include "base/path_service.h" +#include "base/string_util.h" +#include "chrome/browser/autocomplete/autocomplete.h" +#include "chrome/browser/autocomplete/history_contents_provider.h" +#include "chrome/browser/history/history.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +struct TestEntry { + const char* url; + const wchar_t* title; + const wchar_t* body; +} test_entries[] = { + {"http://www.google.com/1", L"PAGEONE 1", L"FOO some body text"}, + {"http://www.google.com/2", L"PAGEONE 2", L"FOO some more blah blah"}, + {"http://www.google.com/3", L"PAGETHREE 3", L"BAR some hello world for you"}, +}; + +// For comparing TestEntry.url with wide strings generated by the autocomplete +// code +bool UrlIs(const char* url, const std::wstring& str) { + return WideToUTF8(str) == std::string(url); +} + +class HistoryContentsProviderTest : public testing::Test, + public ACProviderListener { + public: + + void RunQuery(const AutocompleteInput& input, + bool minimal_changes, + bool synchronous_only) { + provider_->Start(input, minimal_changes, synchronous_only); + + // When we're waiting for asynchronous messages, we have to spin the message + // loop. This will be exited in the OnProviderUpdate function when complete. + if (!synchronous_only) + MessageLoop::current()->Run(); + } + + const ACMatches& matches() const { return provider_->matches(); } + + private: + // testing::Test + virtual void SetUp() { + PathService::Get(base::DIR_TEMP, &history_dir_); + file_util::AppendToPath(&history_dir_, L"HistoryContentProviderTest"); + file_util::Delete(history_dir_, true); // Normally won't exist. + file_util::CreateDirectoryW(history_dir_); + + history_service_ = new HistoryService; + history_service_->Init(history_dir_); + + // Populate history. + for (int i = 0; i < arraysize(test_entries); i++) { + // We need the ID scope and page ID so that the visit tracker can find it. + // We just use the index for the page ID below. + const void* id_scope = reinterpret_cast<void*>(1); + GURL url(test_entries[i].url); + + // Add everything in order of time. We don't want to have a time that + // is "right now" or it will nondeterministically appear in the results. + Time t = Time::Now() - TimeDelta::FromDays(arraysize(test_entries) + i); + + history_service_->AddPage(url, t, id_scope, i, GURL(), + PageTransition::LINK, HistoryService::RedirectList()); + history_service_->SetPageTitle(url, test_entries[i].title); + history_service_->SetPageContents(url, test_entries[i].body); + } + + provider_ = new HistoryContentsProvider(this, history_service_); + } + + virtual void TearDown() { + history_service_->SetOnBackendDestroyTask(new MessageLoop::QuitTask); + history_service_->Cleanup(); + provider_ = NULL; + history_service_ = NULL; + + // Wait for history thread to complete (the QuitTask will cause it to exit + // on destruction). Note: if this never terminates, somebody is probably + // leaking a reference to the history backend, so it never calls our + // destroy task. + MessageLoop::current()->Run(); + + file_util::Delete(history_dir_, true); + } + + // ACProviderListener + virtual void OnProviderUpdate(bool updated_matches) { + // When we quit, the test will get back control. + MessageLoop::current()->Quit(); + } + + std::wstring history_dir_; + + scoped_refptr<HistoryContentsProvider> provider_; + scoped_refptr<HistoryService> history_service_; +}; + +} // namespace + +TEST_F(HistoryContentsProviderTest, Body) { + AutocompleteInput input(L"FOO", std::wstring(), true); + RunQuery(input, false, false); + + // The results should be the first two pages, in decreasing order. + const ACMatches& m = matches(); + ASSERT_EQ(2, m.size()); + EXPECT_TRUE(UrlIs(test_entries[1].url, m[0].destination_url)); + EXPECT_STREQ(test_entries[1].title, m[0].description.c_str()); + EXPECT_TRUE(UrlIs(test_entries[0].url, m[1].destination_url)); + EXPECT_STREQ(test_entries[0].title, m[1].description.c_str()); +} + +TEST_F(HistoryContentsProviderTest, Title) { + AutocompleteInput input(L"PAGEONE", std::wstring(), true); + RunQuery(input, false, false); + + // The results should be the first two pages. + const ACMatches& m = matches(); + ASSERT_EQ(2, m.size()); + EXPECT_TRUE(UrlIs(test_entries[1].url, m[0].destination_url)); + EXPECT_STREQ(test_entries[1].title, m[0].description.c_str()); + EXPECT_TRUE(UrlIs(test_entries[0].url, m[1].destination_url)); + EXPECT_STREQ(test_entries[0].title, m[1].description.c_str()); +} + +// The "minimal changes" flag should mean that we don't re-query the DB. +TEST_F(HistoryContentsProviderTest, MinimalChanges) { + AutocompleteInput input(L"PAGEONE", std::wstring(), true); + + // A minimal changes request when there have been no real queries should + // give us no results. + RunQuery(input, true, true); + const ACMatches& m1 = matches(); + EXPECT_EQ(0, m1.size()); + + // Now do a "regular" query to get the results. + RunQuery(input, false, false); + const ACMatches& m2 = matches(); + EXPECT_EQ(2, m2.size()); + + // Now do a minimal one where we want synchronous results, and the results + // should still be there. + RunQuery(input, true, true); + const ACMatches& m3 = matches(); + EXPECT_EQ(2, m3.size()); +}
\ No newline at end of file diff --git a/chrome/browser/autocomplete/history_url_provider.cc b/chrome/browser/autocomplete/history_url_provider.cc new file mode 100644 index 0000000..1db8c00 --- /dev/null +++ b/chrome/browser/autocomplete/history_url_provider.cc @@ -0,0 +1,856 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "chrome/browser/autocomplete/history_url_provider.h" + +#include <algorithm> + +#include "base/histogram.h" +#include "base/message_loop.h" +#include "base/string_util.h" +#include "chrome/browser/history/history.h" +#include "chrome/browser/history/history_backend.h" +#include "chrome/browser/history/history_database.h" +#include "chrome/browser/profile.h" +#include "chrome/browser/url_fixer_upper.h" +#include "chrome/common/gfx/url_elider.h" +#include "chrome/common/pref_names.h" +#include "chrome/common/pref_service.h" +#include "chrome/common/sqlite_utils.h" +#include "googleurl/src/gurl.h" +#include "googleurl/src/url_parse.h" +#include "googleurl/src/url_util.h" +#include "net/base/net_util.h" + +HistoryURLProviderParams::HistoryURLProviderParams( + const AutocompleteInput& input, + bool trim_http, + const ACMatches& matches, + const std::wstring& languages) + : message_loop(MessageLoop::current()), + input(input), + trim_http(trim_http), + cancel(false), + matches(matches), + languages(languages) { +} + +void HistoryURLProvider::Start(const AutocompleteInput& input, + bool minimal_changes, + bool synchronous_only) { + // NOTE: We could try hard to do less work in the |minimal_changes| case + // here; some clever caching would let us reuse the raw matches from the + // history DB without re-querying. However, we'd still have to go back to + // the history thread to mark these up properly, and if pass 2 is currently + // running, we'd need to wait for it to return to the main thread before + // doing this (we can't just write new data for it to read due to thread + // safety issues). At that point it's just as fast, and easier, to simply + // re-run the query from scratch and ignore |minimal_changes|. + + // Cancel any in-progress query. + Stop(); + + RunAutocompletePasses(input, true, !synchronous_only); +} + +void HistoryURLProvider::Stop() { + done_ = true; + + if (params_) + params_->cancel = true; +} + +void HistoryURLProvider::DeleteMatch(const AutocompleteMatch& match) { + // Delete the match from the history DB. + HistoryService* history_service = + profile_ ? profile_->GetHistoryService(Profile::EXPLICIT_ACCESS) : + history_service_; + GURL selected_url(match.destination_url); + if (!history_service || !selected_url.is_valid()) { + NOTREACHED() << "Can't delete requested URL"; + return; + } + history_service->DeleteURL(selected_url); + + // Delete the match from the current set of matches. + bool found = false; + for (ACMatches::iterator i(matches_.begin()); i != matches_.end(); ++i) { + if (i->destination_url == match.destination_url) { + found = true; + if (i->is_history_what_you_typed_match) { + // We can't get rid of the What You Typed match, but we can make it + // look like it has no backing data. + i->deletable = false; + i->description.clear(); + i->description_class.clear(); + } else { + matches_.erase(i); + } + break; + } + } + DCHECK(found) << "Asked to delete a URL that isn't in our set of matches"; + listener_->OnProviderUpdate(true); + + // Cancel any current pass 2 and rerun it, so we get correct history data. + if (!done_) { + // Copy params_->input to avoid a race condition where params_ gets deleted + // out from under us on the other thread after we set params_->cancel here. + AutocompleteInput input(params_->input); + params_->cancel = true; + RunAutocompletePasses(input, false, true); + } +} + +// Called on the history thread. +void HistoryURLProvider::ExecuteWithDB(history::HistoryBackend* backend, + history::URLDatabase* db, + HistoryURLProviderParams* params) { + // We may get called with a NULL database if it couldn't be properly + // initialized. In this case we just say the query is complete. + if (db && !params->cancel) { + TimeTicks beginning_time = TimeTicks::Now(); + + DoAutocomplete(backend, db, params); + + HISTOGRAM_TIMES(L"Autocomplete.HistoryAsyncQueryTime", + TimeTicks::Now() - beginning_time); + } + + // Return the results (if any) to the main thread. + params->message_loop->PostTask(FROM_HERE, NewRunnableMethod( + this, &HistoryURLProvider::QueryComplete, params)); +} + +// Used by both autocomplete passes, and therefore called on multiple different +// threads (though not simultaneously). +void HistoryURLProvider::DoAutocomplete(history::HistoryBackend* backend, + history::URLDatabase* db, + HistoryURLProviderParams* params) { + // Get the matching URLs from the DB + typedef std::vector<history::URLRow> URLRowVector; + URLRowVector url_matches; + HistoryMatches history_matches; + for (Prefixes::const_iterator i(prefixes_.begin()); i != prefixes_.end(); + ++i) { + if (params->cancel) + return; // canceled in the middle of a query, give up + // We only need max_matches results in the end, but before we get there we + // need to promote lower-quality matches that are prefixes of + // higher-quality matches, and remove lower-quality redirects. So we ask + // for more results than we need, of every prefix type, in hopes this will + // give us far more than enough to work with. CullRedirects() will then + // reduce the list to the best max_matches results. + db->AutocompleteForPrefix(i->prefix + params->input.text(), + max_matches() * 2, &url_matches); + for (URLRowVector::const_iterator j(url_matches.begin()); + j != url_matches.end(); ++j) { + const Prefix* best_prefix = BestPrefix(UTF8ToWide(j->url().spec()), + std::wstring()); + DCHECK(best_prefix != NULL); + history_matches.push_back(HistoryMatch(*j, i->prefix.length(), + !i->num_components, + i->num_components >= best_prefix->num_components)); + } + } + + // Create sorted list of suggestions. + CullPoorMatches(&history_matches); + SortMatches(&history_matches); + PromoteOrCreateShorterSuggestion(db, *params, &history_matches); + + // Try to promote a match as an exact/inline autocomplete match. This also + // moves it to the front of |history_matches|, so skip over it when + // converting the rest of the matches. We want to provide up to max_matches + // results plus the What You Typed result. + size_t first_match = 1; + size_t exact_suggestion = 0; + if (!params->matches.empty() && + FixupExactSuggestion(db, params, &history_matches)) + exact_suggestion = 1; + else if (params->input.prevent_inline_autocomplete() || + history_matches.empty() || + !PromoteMatchForInlineAutocomplete(params, history_matches.front())) + first_match = 0; + + // This is the end of the synchronous pass. + if (!backend) + return; + + // Remove redirects and trim list to size. + CullRedirects(backend, &history_matches, max_matches() + exact_suggestion); + + // Convert the history matches to autocomplete matches. + for (size_t i = first_match; i < history_matches.size(); ++i) { + const HistoryMatch& match = history_matches[i]; + DCHECK(!exact_suggestion || + (match.url_info.url() != + GURL(params->matches.front().destination_url))); + params->matches.push_back(HistoryMatchToACMatch(params, match, NORMAL, + history_matches.size() - 1 - i)); + } +} + +// Called on the main thread when the query is complete. +void HistoryURLProvider::QueryComplete( + HistoryURLProviderParams* params_gets_deleted) { + // Ensure |params_gets_deleted| gets deleted on exit. + scoped_ptr<HistoryURLProviderParams> params(params_gets_deleted); + + // If the user hasn't already started another query, clear our member pointer + // so we can't write into deleted memory. + if (params_ == params_gets_deleted) + params_ = NULL; + + // Don't send responses for queries that have been canceled. + if (params->cancel) + return; // Already set done_ when we canceled, no need to set it again. + + done_ = true; + matches_.swap(params->matches); + listener_->OnProviderUpdate(true); +} + +void HistoryURLProvider::SuggestExactInput(const AutocompleteInput& input, + bool trim_http) { + AutocompleteMatch match(this, + CalculateRelevance(input.type(), WHAT_YOU_TYPED, 0), false); + + // Try to canonicalize the URL. If this fails, don't create a What You Typed + // suggestion, since it can't be navigated to. We also need this so other + // history suggestions don't duplicate the same effective URL as this. + // TODO(brettw) make autocomplete use GURL! + GURL canonicalized_url(URLFixerUpper::FixupURL(input.text(), + input.desired_tld())); + if (!canonicalized_url.is_valid() || + (canonicalized_url.IsStandard() && + !canonicalized_url.SchemeIsFile() && canonicalized_url.host().empty())) + return; + match.destination_url = UTF8ToWide(canonicalized_url.spec()); + match.fill_into_edit = StringForURLDisplay(canonicalized_url, false); + // NOTE: Don't set match.input_location (to allow inline autocompletion) + // here, it's surprising and annoying. + // Trim off "http://" if the user didn't type it. + const size_t offset = trim_http ? TrimHttpPrefix(&match.fill_into_edit) : 0; + + // Try to highlight "innermost" match location. If we fix up "w" into + // "www.w.com", we want to highlight the fifth character, not the first. + // This relies on match.destination_url being the non-prefix-trimmed version + // of match.contents. + match.contents = match.fill_into_edit; + const Prefix* best_prefix = BestPrefix(match.destination_url, input.text()); + // Because of the vagaries of GURL, it's possible for match.destination_url + // to not contain the user's input at all. In this case don't mark anything + // as a match. + const size_t match_location = (best_prefix == NULL) ? + std::wstring::npos : best_prefix->prefix.length() - offset; + AutocompleteMatch::ClassifyLocationInString(match_location, + input.text().length(), + match.contents.length(), + ACMatchClassification::URL, + &match.contents_class); + + match.is_history_what_you_typed_match = true; + matches_.push_back(match); +} + +bool HistoryURLProvider::FixupExactSuggestion(history::URLDatabase* db, + HistoryURLProviderParams* params, + HistoryMatches* matches) const { + DCHECK(!params->matches.empty()); + + history::URLRow info; + AutocompleteMatch& match = params->matches.front(); + + // Tricky corner case: The user has visited intranet site "foo", but not + // internet site "www.foo.com". He types in foo (getting an exact match), + // then tries to hit ctrl-enter. When pressing ctrl, the what-you-typed + // match ("www.foo.com") doesn't show up in history, and thus doesn't get a + // promoted relevance, but a different match from the input ("foo") does, and + // gets promoted for inline autocomplete. Thus instead of getting + // "www.foo.com", the user still gets "foo" (and, before hitting enter, + // probably gets an odd-looking inline autocomplete of "/"). + // + // We detect this crazy case as follows: + // * If the what-you-typed match is not in the history DB, + // * and the user has specified a TLD, + // * and the input _without_ the TLD _is_ in the history DB, + // * ...then just before pressing "ctrl" the best match we supplied was the + // what-you-typed match, so stick with it by promoting this. + if (!db->GetRowForURL(GURL(match.destination_url), &info)) { + if (params->input.desired_tld().empty()) + return false; + // This code should match what SuggestExactInput() would do with no + // desired_tld(). + // TODO(brettw) make autocomplete use GURL! + GURL destination_url(URLFixerUpper::FixupURL(params->input.text(), + std::wstring())); + if (!db->GetRowForURL(destination_url, &info)) + return false; + } else { + // We have data for this match, use it. + match.starred = info.starred(); + match.deletable = true; + match.description = info.title(); + AutocompleteMatch::ClassifyMatchInString(params->input.text(), + info.title(), + ACMatchClassification::NONE, + &match.description_class); + } + + // Promote as an exact match. + match.relevance = CalculateRelevance(params->input.type(), + INLINE_AUTOCOMPLETE, 0); + + // Put it on the front of the HistoryMatches for redirect culling. + EnsureMatchPresent(info, std::wstring::npos, false, matches, true); + return true; +} + +bool HistoryURLProvider::PromoteMatchForInlineAutocomplete( + HistoryURLProviderParams* params, + const HistoryMatch& match) { + // Promote the first match if it's been typed at least n times, where n == 1 + // for "simple" (host-only) URLs and n == 2 for others. We set a higher bar + // for these long URLs because it's less likely that users will want to visit + // them again. Even though we don't increment the typed_count for pasted-in + // URLs, if the user manually edits the URL or types some long thing in by + // hand, we wouldn't want to immediately start autocompleting it. + if (!match.url_info.typed_count() || + ((match.url_info.typed_count() == 1) && + !IsHostOnly(match.url_info.url()))) + return false; + + params->matches.push_back(HistoryMatchToACMatch(params, match, + INLINE_AUTOCOMPLETE, 0)); + return true; +} + +// static +std::wstring HistoryURLProvider::FixupUserInput(const std::wstring& input) { + // Fixup and canonicalize user input. + const GURL canonical_gurl(URLFixerUpper::FixupURL(input, std::wstring())); + std::wstring output(UTF8ToWide(canonical_gurl.possibly_invalid_spec())); + if (output.empty()) + return input; // This probably won't happen, but there are no guarantees. + + // Don't prepend a scheme when the user didn't have one. Since the fixer + // upper only prepends the "http" scheme, that's all we need to check for. + url_parse::Component scheme; + if (canonical_gurl.SchemeIs("http") && + !url_util::FindAndCompareScheme(input, "http", &scheme)) + TrimHttpPrefix(&output); + + // Make the number of trailing slashes on the output exactly match the input. + // Examples of why not doing this would matter: + // * The user types "a" and has this fixed up to "a/". Now no other sites + // beginning with "a" will match. + // * The user types "file:" and has this fixed up to "file://". Now inline + // autocomplete will append too few slashes, resulting in e.g. "file:/b..." + // instead of "file:///b..." + // * The user types "http:/" and has this fixed up to "http:". Now inline + // autocomplete will append too many slashes, resulting in e.g. + // "http:///c..." instead of "http://c...". + // NOTE: We do this after calling TrimHttpPrefix() since that can strip + // trailing slashes (if the scheme is the only thing in the input). It's not + // clear that the result of fixup really matters in this case, but there's no + // harm in making sure. + const size_t last_input_nonslash = input.find_last_not_of(L"/\\"); + const size_t num_input_slashes = (last_input_nonslash == std::wstring::npos) ? + input.length() : (input.length() - 1 - last_input_nonslash); + const size_t last_output_nonslash = output.find_last_not_of(L"/\\"); + const size_t num_output_slashes = + (last_output_nonslash == std::wstring::npos) ? + output.length() : (output.length() - 1 - last_output_nonslash); + if (num_output_slashes < num_input_slashes) + output.append(num_input_slashes - num_output_slashes, '/'); + else if (num_output_slashes > num_input_slashes) + output.erase(output.length() - num_output_slashes + num_input_slashes); + + return output; +} + +// static +size_t HistoryURLProvider::TrimHttpPrefix(std::wstring* url) { + url_parse::Component scheme; + if (!url_util::FindAndCompareScheme(*url, "http", &scheme)) + return 0; // Not "http". + + // Erase scheme plus up to two slashes. + size_t prefix_len = scheme.end() + 1; // "http:" + const size_t after_slashes = std::min(url->length(), + static_cast<size_t>(scheme.end() + 3)); + while ((prefix_len < after_slashes) && ((*url)[prefix_len] == L'/')) + ++prefix_len; + if (prefix_len == url->length()) + url->clear(); + else + url->erase(url->begin(), url->begin() + prefix_len); + return prefix_len; +} + +// static +bool HistoryURLProvider::IsHostOnly(const GURL& url) { + DCHECK(url.is_valid()); + return (!url.has_path() || (url.path() == "/")) && !url.has_query() && + !url.has_ref(); +} + +// static +bool HistoryURLProvider::CompareHistoryMatch(const HistoryMatch& a, + const HistoryMatch& b) { + // A URL that has been typed at all is better than one that has never been + // typed. (Note "!"s on each side) + if (!a.url_info.typed_count() != !b.url_info.typed_count()) + return a.url_info.typed_count() > b.url_info.typed_count(); + + // Innermost matches (matches after any scheme or "www.") are better than + // non-innermost matches. + if (a.innermost_match != b.innermost_match) + return a.innermost_match; + + // URLs that have been typed more often are better. + if (a.url_info.typed_count() != b.url_info.typed_count()) + return a.url_info.typed_count() > b.url_info.typed_count(); + + // Starred pages are better than unstarred pages. + if (a.url_info.starred() != b.url_info.starred()) + return a.url_info.starred(); + + // For URLs that have each been typed once, a host (alone) is better than a + // page inside. + if (a.url_info.typed_count() == 1) { + const bool a_is_host_only = IsHostOnly(a.url_info.url()); + if (a_is_host_only != IsHostOnly(b.url_info.url())) + return a_is_host_only; + } + + // URLs that have been visited more often are better. + if (a.url_info.visit_count() != b.url_info.visit_count()) + return a.url_info.visit_count() > b.url_info.visit_count(); + + // URLs that have been visited more recently are better. + return a.url_info.last_visit() > b.url_info.last_visit(); +} + +// static +HistoryURLProvider::Prefixes HistoryURLProvider::GetPrefixes() { + // We'll complete text following these prefixes. + // NOTE: There's no requirement that these be in any particular order. + Prefixes prefixes; + prefixes.push_back(Prefix(L"https://www.", 2)); + prefixes.push_back(Prefix(L"http://www.", 2)); + prefixes.push_back(Prefix(L"ftp://ftp.", 2)); + prefixes.push_back(Prefix(L"ftp://www.", 2)); + prefixes.push_back(Prefix(L"https://", 1)); + prefixes.push_back(Prefix(L"http://", 1)); + prefixes.push_back(Prefix(L"ftp://", 1)); + prefixes.push_back(Prefix(L"", 0)); // Catches within-scheme matches as well + return prefixes; +} + +// static +int HistoryURLProvider::CalculateRelevance(AutocompleteInput::Type input_type, + MatchType match_type, + size_t match_number) { + switch (match_type) { + case INLINE_AUTOCOMPLETE: + return 1400; + + case WHAT_YOU_TYPED: + return (input_type == AutocompleteInput::REQUESTED_URL) ? 1300 : 1200; + + default: + return 900 + static_cast<int>(match_number); + } +} + +// static +GURL HistoryURLProvider::ConvertToHostOnly(const HistoryMatch& match, + const std::wstring& input) { + // See if we should try to do host-only suggestions for this URL. Nonstandard + // schemes means there's no authority section, so suggesting the host name + // is useless. File URLs are standard, but host suggestion is not useful for + // them either. + const GURL& url = match.url_info.url(); + if (!url.is_valid() || !url.IsStandard() || url.SchemeIsFile()) + return GURL(); + + // Transform to a host-only match. Bail if the host no longer matches the + // user input (e.g. because the user typed more than just a host). + GURL host = url.GetWithEmptyPath(); + if ((host.spec().length() < (match.input_location + input.length()))) + return GURL(); // User typing is longer than this host suggestion. + + const std::wstring spec = UTF8ToWide(host.spec()); + if (spec.compare(match.input_location, input.length(), input)) + return GURL(); // User typing is no longer a prefix. + + return host; +} + +// static +void HistoryURLProvider::PromoteOrCreateShorterSuggestion( + history::URLDatabase* db, + const HistoryURLProviderParams& params, + HistoryMatches* matches) { + if (matches->empty()) + return; // No matches, nothing to do. + + // Determine the base URL from which to search, and whether that URL could + // itself be added as a match. We can add the base iff it's not "effectively + // the same" as any "what you typed" match. + const HistoryMatch& match = matches->front(); + GURL search_base = ConvertToHostOnly(match, params.input.text()); + bool can_add_search_base_to_matches = params.matches.empty(); + if (search_base.is_empty()) { + // Search from what the user typed when we couldn't reduce the best match + // to a host. Careful: use a substring of |match| here, rather than the + // first match in |params|, because they might have different prefixes. If + // the user typed "google.com", params.matches will hold + // "http://google.com/", but |match| might begin with + // "http://www.google.com/". + // TODO: this should be cleaned up, and is probably incorrect for IDN. + std::string new_match = match.url_info.url().possibly_invalid_spec(). + substr(0, match.input_location + params.input.text().length()); + search_base = GURL(new_match); + + } else if (!can_add_search_base_to_matches) { + // TODO(brettw) this extra GURL conversion should be unnecessary. + can_add_search_base_to_matches = + (search_base != GURL(params.matches.front().destination_url)); + } + if (search_base == match.url_info.url()) + return; // Couldn't shorten |match|, so no range of URLs to search over. + + // Search the DB for short URLs between our base and |match|. + history::URLRow info(search_base); + bool promote = true; + // A short URL is only worth suggesting if it's been visited at least a third + // as often as the longer URL. + const int min_visit_count = ((match.url_info.visit_count() - 1) / 3) + 1; + // For stability between the in-memory and on-disk autocomplete passes, when + // the long URL has been typed before, only suggest shorter URLs that have + // also been typed. Otherwise, the on-disk pass could suggest a shorter URL + // (which hasn't been typed) that the in-memory pass doesn't know about, + // thereby making the top match, and thus the behavior of inline + // autocomplete, unstable. + const int min_typed_count = match.url_info.typed_count() ? 1 : 0; + if (!db->FindShortestURLFromBase(search_base.possibly_invalid_spec(), + match.url_info.url().possibly_invalid_spec(), min_visit_count, + min_typed_count, can_add_search_base_to_matches, &info)) { + if (!can_add_search_base_to_matches) + return; // Couldn't find anything and can't add the search base, bail. + + // Try to get info on the search base itself. Promote it to the top if the + // original best match isn't good enough to autocomplete. + db->GetRowForURL(search_base, &info); + promote = match.url_info.typed_count() <= 1; + } + + // Promote or add the desired URL to the list of matches. + EnsureMatchPresent(info, match.input_location, match.match_in_scheme, + matches, promote); +} + +// static +void HistoryURLProvider::EnsureMatchPresent( + const history::URLRow& info, + std::wstring::size_type input_location, + bool match_in_scheme, + HistoryMatches* matches, + bool promote) { + // |matches| may already have an entry for this. + for (HistoryMatches::iterator i(matches->begin()); i != matches->end(); + ++i) { + if (i->url_info.url() == info.url()) { + // Rotate it to the front if the caller wishes. + if (promote) + std::rotate(matches->begin(), i, i + 1); + return; + } + } + + // No entry, so create one. + HistoryMatch match(info, input_location, match_in_scheme, true); + if (promote) + matches->push_front(match); + else + matches->push_back(match); +} + +void HistoryURLProvider::RunAutocompletePasses(const AutocompleteInput& input, + bool fixup_input_and_run_pass_1, + bool run_pass_2) { + matches_.clear(); + + if ((input.type() != AutocompleteInput::UNKNOWN) && + (input.type() != AutocompleteInput::REQUESTED_URL) && + (input.type() != AutocompleteInput::URL)) + return; + + // Create a match for exactly what the user typed. This will always be one + // of the top two results we return. + const bool trim_http = !url_util::FindAndCompareScheme(input.text(), + "http", NULL); + SuggestExactInput(input, trim_http); + + // We'll need the history service to run both passes, so try to obtain it. + HistoryService* const history_service = profile_ ? + profile_->GetHistoryService(Profile::EXPLICIT_ACCESS) : history_service_; + if (!history_service) + return; + + // Create the data structure for the autocomplete passes. We'll save this off + // onto the |params_| member for later deletion below if we need to run pass + // 2. + const std::wstring& languages = profile_ ? + profile_->GetPrefs()->GetString(prefs::kAcceptLanguages) : std::wstring(); + scoped_ptr<HistoryURLProviderParams> params( + new HistoryURLProviderParams(input, trim_http, matches_, languages)); + + if (fixup_input_and_run_pass_1) { + // Do some fixup on the user input before matching against it, so we provide + // good results for local file paths, input with spaces, etc. + // NOTE: This purposefully doesn't take input.desired_tld() into account; if + // it did, then holding "ctrl" would change all the results from the + // HistoryURLProvider provider, not just the What You Typed Result. + // However, this means we need to call this _after_ calling + // SuggestExactInput(), since that function does need to take + // input.desired_tld() into account; if it doesn't, it may convert "56" + + // ctrl into "0.0.0.56.com" instead of "56.com" like the user probably + // wanted. It's not a problem to call this after SuggestExactInput(), + // because that function fixes up the user's input in a way that's a + // superset of what FixupUserInput() does. + const std::wstring fixed_text(FixupUserInput(input.text())); + if (fixed_text.empty()) { + // Conceivably fixup could result in an empty string (although I don't + // have cases where this happens offhand). We can't do anything with + // empty input, so just bail; otherwise we'd crash later. + return; + } + params->input.set_text(fixed_text); + + // Pass 1: Get the in-memory URL database, and use it to find and promote + // the inline autocomplete match, if any. + history::URLDatabase* url_db = history_service->in_memory_database(); + // url_db can be NULL if it hasn't finished initializing (or failed to + // initialize). In this case all we can do is fall back on the second + // pass. Ultimately, we should probably try to ensure the history system + // starts properly before we get here, as otherwise this can cause + // inconsistent behavior when the user has just started the browser and + // tries to type immediately. + if (url_db) { + DoAutocomplete(NULL, url_db, params.get()); + // params->matches now has the matches we should expose to the provider. + // Since pass 2 expects a "clean slate" set of matches that only contains + // the not-yet-fixed-up What You Typed match, which is exactly what + // matches_ currently contains, just swap them. + matches_.swap(params->matches); + } + } + + // Pass 2: Ask the history service to call us back on the history thread, + // where we can read the full on-disk DB. + if (run_pass_2) { + done_ = false; + params_ = params.release(); // This object will be destroyed in + // QueryComplete() once we're done with it. + history_service->ScheduleAutocomplete(this, params_); + } +} + +const HistoryURLProvider::Prefix* HistoryURLProvider::BestPrefix( + const std::wstring& text, + const std::wstring& prefix_suffix) const { + const Prefix* best_prefix = NULL; + for (Prefixes::const_iterator i(prefixes_.begin()); i != prefixes_.end(); + ++i) { + if ((best_prefix == NULL) || + (i->num_components > best_prefix->num_components)) { + std::wstring prefix_with_suffix(i->prefix + prefix_suffix); + if ((text.length() >= prefix_with_suffix.length()) && + !text.compare(0, prefix_with_suffix.length(), prefix_with_suffix)) + best_prefix = &(*i); + } + } + return best_prefix; +} + +void HistoryURLProvider::SortMatches(HistoryMatches* matches) const { + // Sort by quality, best first. + std::sort(matches->begin(), matches->end(), &CompareHistoryMatch); + + // Remove duplicate matches (caused by the search string appearing in one of + // the prefixes as well as after it). Consider the following scenario: + // + // User has visited "http://http.com" once and "http://htaccess.com" twice. + // User types "http". The autocomplete search with prefix "http://" returns + // the first host, while the search with prefix "" returns both hosts. Now + // we sort them into rank order: + // http://http.com (innermost_match) + // http://htaccess.com (!innermost_match, url_info.visit_count == 2) + // http://http.com (!innermost_match, url_info.visit_count == 1) + // + // The above scenario tells us we can't use std::unique(), since our + // duplicates are not always sequential. It also tells us we should remove + // the lower-quality duplicate(s), since otherwise the returned results won't + // be ordered correctly. This is easy to do: we just always remove the later + // element of a duplicate pair. + // Be careful! Because the vector contents may change as we remove elements, + // we use an index instead of an iterator in the outer loop, and don't + // precalculate the ending position. + for (size_t i = 0; i < matches->size(); ++i) { + HistoryMatches::iterator j(matches->begin() + i + 1); + while (j != matches->end()) { + if ((*matches)[i].url_info.url() == j->url_info.url()) + j = matches->erase(j); + else + ++j; + } + } +} + +void HistoryURLProvider::CullPoorMatches(HistoryMatches* matches) const { + static const int kLowQualityMatchTypedLimit = 1; + static const int kLowQualityMatchVisitLimit = 3; + static const int kLowQualityMatchAgeLimitInDays = 3; + Time recent_threshold = + Time::Now() - TimeDelta::FromDays(kLowQualityMatchAgeLimitInDays); + for (HistoryMatches::iterator i(matches->begin()); i != matches->end();) { + const history::URLRow& url_info = i->url_info; + if ((url_info.typed_count() <= kLowQualityMatchTypedLimit) && + (url_info.visit_count() <= kLowQualityMatchVisitLimit) && + (url_info.last_visit() < recent_threshold)) { + i = matches->erase(i); + } else { + ++i; + } + } +} + +void HistoryURLProvider::CullRedirects(history::HistoryBackend* backend, + HistoryMatches* matches, + size_t max_results) const { + for (size_t source = 0; + (source < matches->size()) && (source < max_results); ) { + const GURL& url = (*matches)[source].url_info.url(); + // TODO(brettw) this should go away when everything uses GURL. + HistoryService::RedirectList redirects; + backend->GetMostRecentRedirectsFrom(url, &redirects); + if (!redirects.empty()) { + // Remove all but the first occurrence of any of these redirects in the + // search results. We also must add the URL we queried for, since it may + // not be the first match and we'd want to remove it. + // + // For example, when A redirects to B and our matches are [A, X, B], + // we'll get B as the redirects from, and we want to remove the second + // item of that pair, removing B. If A redirects to B and our matches are + // [B, X, A], we'll want to remove A instead. + redirects.push_back(url); + source = RemoveSubsequentMatchesOf(matches, source, redirects); + } else { + // Advance to next item. + source++; + } + } + + if (matches->size() > max_results) + matches->resize(max_results); +} + +size_t HistoryURLProvider::RemoveSubsequentMatchesOf( + HistoryMatches* matches, + size_t source_index, + const std::vector<GURL>& remove) const { + size_t next_index = source_index + 1; // return value = item after source + + // Find the first occurrence of any URL in the redirect chain. We want to + // keep this one since it is rated the highest. + HistoryMatches::iterator first(std::find_first_of( + matches->begin(), matches->end(), remove.begin(), remove.end())); + DCHECK(first != matches->end()) << + "We should have always found at least the original URL."; + + // Find any following occurrences of any URL in the redirect chain, these + // should be deleted. + HistoryMatches::iterator next(first); + next++; // Start searching immediately after the one we found already. + while (next != matches->end() && + (next = std::find_first_of(next, matches->end(), remove.begin(), + remove.end())) != matches->end()) { + // Remove this item. When we remove an item before the source index, we + // need to shift it to the right and remember that so we can return it. + next = matches->erase(next); + if (static_cast<size_t>(next - matches->begin()) < next_index) + next_index--; + } + return next_index; +} + +AutocompleteMatch HistoryURLProvider::HistoryMatchToACMatch( + HistoryURLProviderParams* params, + const HistoryMatch& history_match, + MatchType match_type, + size_t match_number) { + const history::URLRow& info = history_match.url_info; + AutocompleteMatch match(this, + CalculateRelevance(params->input.type(), match_type, match_number), + !!info.visit_count()); + match.destination_url = UTF8ToWide(info.url().possibly_invalid_spec()); + match.fill_into_edit = gfx::ElideUrl(info.url(), ChromeFont(), 0, + match_type == WHAT_YOU_TYPED ? std::wstring() : params->languages); + if (!params->input.prevent_inline_autocomplete()) { + match.inline_autocomplete_offset = + history_match.input_location + params->input.text().length(); + } + size_t offset = 0; + if (params->trim_http && !history_match.match_in_scheme) { + offset = TrimHttpPrefix(&match.fill_into_edit); + if (match.inline_autocomplete_offset != std::wstring::npos) { + DCHECK(match.inline_autocomplete_offset >= offset); + match.inline_autocomplete_offset -= offset; + } + } + DCHECK((match.inline_autocomplete_offset == std::wstring::npos) || + (match.inline_autocomplete_offset <= match.fill_into_edit.length())); + + match.contents = match.fill_into_edit; + AutocompleteMatch::ClassifyLocationInString( + history_match.input_location - offset, params->input.text().length(), + match.contents.length(), ACMatchClassification::URL, + &match.contents_class); + match.description = info.title(); + AutocompleteMatch::ClassifyMatchInString(params->input.text(), info.title(), + ACMatchClassification::NONE, + &match.description_class); + + match.starred = history_match.url_info.starred(); + return match; +} diff --git a/chrome/browser/autocomplete/history_url_provider.h b/chrome/browser/autocomplete/history_url_provider.h new file mode 100644 index 0000000..cafdb5b --- /dev/null +++ b/chrome/browser/autocomplete/history_url_provider.h @@ -0,0 +1,419 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#ifndef CHROME_BROWSER_AUTOCOMPLETE_HISTORY_URL_PROVIDER_H__ +#define CHROME_BROWSER_AUTOCOMPLETE_HISTORY_URL_PROVIDER_H__ + +#include <map> +#include <vector> +#include <deque> + +#include "base/ref_counted.h" +#include "chrome/browser/autocomplete/autocomplete.h" +#include "chrome/browser/history/history_database.h" +#include "chrome/browser/profile.h" + +class HistoryService; +class MessageLoop; + +namespace history { + +class HistoryBackend; + +} // namespace history + + +// How history autocomplete works +// ============================== +// +// Read down this diagram for temporal ordering. +// +// Main thread History thread +// ----------- -------------- +// AutocompleteController::Start +// -> HistoryURLProvider::Start +// -> RunAutocompletePasses +// -> SuggestExactInput +// [params_ allocated] +// -> DoAutocomplete (for inline autocomplete) +// -> URLDatabase::AutocompleteForPrefix (on in-memory DB) +// -> HistoryService::ScheduleAutocomplete +// (return to controller) \ +// HistoryBackend::ScheduleAutocomplete +// -> HistoryURLProvider::ExecuteWithDB +// -> DoAutocomplete +// -> URLDatabase::AutocompleteForPrefix +// / +// HistoryService::QueryComplete +// [params_ destroyed] +// -> AutocompleteProvider::Listener::OnProviderUpdate +// +// The autocomplete controller calls us, and must be called back, on the main +// thread. When called, we run two autocomplete passes. The first pass runs +// synchronously on the main thread and queries the in-memory URL database. +// This pass promotes matches for inline autocomplete if applicable. We do +// this synchronously so that users get consistent behavior when they type +// quickly and hit enter, no matter how loaded the main history database is. +// Doing this synchronously also prevents inline autocomplete from being +// "flickery" in the AutocompleteEdit. Because the in-memory DB does not have +// redirect data, results other than the top match might change between the +// two passes, so we can't just decide to use this pass' matches as the final +// results. +// +// The second autocomplete pass uses the full history database, which must be +// queried on the history thread. Start() asks the history service schedule to +// callback on the history thread with a pointer to the main database. When we +// are done doing queries, we schedule a task on the main thread that notifies +// the AutocompleteController that we're done. +// +// The communication between these threads is done using a +// HistoryURLProviderParams object. This is allocated in the main thread, and +// normally deleted in QueryComplete(). So that both autocomplete passes can +// use the same code, we also use this to hold results during the first +// autocomplete pass. +// +// While the second pass is running, the AutocompleteController may cancel the +// request. This can happen frequently when the user is typing quickly. In +// this case, the main thread sets params_->cancel, which the background thread +// checks periodically. If it finds the flag set, it stops what it's doing +// immediately and calls back to the main thread. (We don't delete the params +// on the history thread, because we should only do that when we can safely +// NULL out params_, and that must be done on the main thread.) + +// Used to communicate autocomplete parameters between threads via the history +// service. +struct HistoryURLProviderParams { + HistoryURLProviderParams(const AutocompleteInput& input, + bool trim_http, + const ACMatches& matches, + const std::wstring& languages); + + MessageLoop* message_loop; + + // A copy of the autocomplete input. We need the copy since this object will + // live beyond the original query while it runs on the history thread. + AutocompleteInput input; + + // Set when "http://" should be trimmed from the beginning of the URLs. + bool trim_http; + + // Set by the main thread to cancel this request. READ ONLY when running in + // ExecuteWithDB() on the history thread to prevent deadlock. If this flag is + // set when the query runs, the query will be abandoned. This allows us to + // avoid running queries that are no longer needed. Since we don't care if + // we run the extra queries, the lack of signaling is not a problem. + bool cancel; + + // List of matches written by the history thread. We keep this separate list + // to avoid having the main thread read the provider's matches while the + // history thread is manipulating them. The provider copies this list back + // to matches_ on the main thread in QueryComplete(). + ACMatches matches; + + // Languages we should pass to gfx::ElideUrl. + std::wstring languages; + + private: + DISALLOW_EVIL_CONSTRUCTORS(HistoryURLProviderParams); +}; + +// This class is an autocomplete provider and is also a pseudo-internal +// component of the history system. See comments above. +// +// Note: This object can get leaked on shutdown if there are pending +// requests on the database (which hold a reference to us). Normally, these +// messages get flushed for each thread. We do a round trip from main, to +// history, back to main while holding a reference. If the main thread +// completes before the history thread, the message to delegate back to the +// main thread will not run and the reference will leak. Therefore, don't do +// anything on destruction. +class HistoryURLProvider : public AutocompleteProvider { + public: + HistoryURLProvider(ACProviderListener* listener, Profile* profile) + : AutocompleteProvider(listener, profile, "HistoryURL"), + history_service_(NULL), + prefixes_(GetPrefixes()), + params_(NULL) { + } + +#ifdef UNIT_TEST + HistoryURLProvider(ACProviderListener* listener, + HistoryService* history_service) + : AutocompleteProvider(listener, NULL, "History"), + history_service_(history_service), + prefixes_(GetPrefixes()), + params_(NULL) { + } +#endif + // no destructor (see note above) + + // AutocompleteProvider + virtual void Start(const AutocompleteInput& input, + bool minimal_changes, + bool synchronous_only); + virtual void Stop(); + virtual void DeleteMatch(const AutocompleteMatch& match); + + // Runs the history query on the history thread, called by the history + // system. The history database MAY BE NULL in which case it is not + // available and we should return no data. Also schedules returning the + // results to the main thread + void ExecuteWithDB(history::HistoryBackend* backend, + history::URLDatabase* db, + HistoryURLProviderParams* params); + + // Actually runs the autocomplete job on the given database, which is + // guaranteed not to be NULL. + void DoAutocomplete(history::HistoryBackend* backend, + history::URLDatabase* db, + HistoryURLProviderParams* params); + + // Dispatches the results to the autocomplete controller. Called on the + // main thread by ExecuteWithDB when the results are available. + // Frees params_gets_deleted on exit. + void QueryComplete(HistoryURLProviderParams* params_gets_deleted); + + private: + struct Prefix { + Prefix(std::wstring prefix, int num_components) + : prefix(prefix), + num_components(num_components) { } + + std::wstring prefix; + + // The number of "components" in the prefix. The scheme is a component, + // and the initial "www." or "ftp." is a component. So "http://foo.com" + // and "www.bar.com" each have one component, "ftp://ftp.ftp.com" has two, + // and "mysite.com" has none. This is used to tell whether the user's + // input is an innermost match or not. See comments in HistoryMatch. + int num_components; + }; + typedef std::vector<Prefix> Prefixes; + + // Used for intermediate history result operations. + struct HistoryMatch { + // Required for STL, we don't use this directly. + HistoryMatch() + : url_info(), + input_location(std::wstring::npos), + match_in_scheme(false), + innermost_match(true) { + } + + HistoryMatch(const history::URLRow& url_info, + size_t input_location, + bool match_in_scheme, + bool innermost_match) + : url_info(url_info), + input_location(input_location), + match_in_scheme(match_in_scheme), + innermost_match(innermost_match) { + } + + bool operator==(const GURL& url) const { + return url_info.url() == url; + } + + history::URLRow url_info; + + // The offset of the user's input within the URL. + size_t input_location; + + // Whether this is a match in the scheme. This determines whether we'll go + // ahead and show a scheme on the URL even if the user didn't type one. + // If our best match was in the scheme, not showing the scheme is both + // confusing and, for inline autocomplete of the fill_into_edit, dangerous. + // (If the user types "h" and we match "http://foo/", we need to inline + // autocomplete that, not "foo/", which won't show anything at all, and + // will mislead the user into thinking the What You Typed match is what's + // selected.) + bool match_in_scheme; + + // A match after any scheme/"www.", if the user input could match at both + // locations. If the user types "w", an innermost match ("website.com") is + // better than a non-innermost match ("www.google.com"). If the user types + // "x", no scheme in our prefix list (or "www.") begins with x, so all + // matches are, vacuously, "innermost matches". + bool innermost_match; + }; + typedef std::deque<HistoryMatch> HistoryMatches; + + enum MatchType { + NORMAL, + WHAT_YOU_TYPED, + INLINE_AUTOCOMPLETE + }; + + // Fixes up user URL input to make it more possible to match against. Among + // many other things, this takes care of the following: + // * Prepending file:// to file URLs + // * Converting drive letters in file URLs to uppercase + // * Converting case-insensitive parts of URLs (like the scheme and domain) + // to lowercase + // * Convert spaces to %20s + // Note that we don't do this in AutocompleteInput's constructor, because if + // e.g. we convert a Unicode hostname to punycode, other providers will show + // output that surprises the user ("Search Google for xn--6ca.com"). + static std::wstring FixupUserInput(const std::wstring& input); + + // Trims "http:" and up to two subsequent slashes from |url|. Returns the + // number of characters that were trimmed. + static size_t TrimHttpPrefix(std::wstring* url); + + // Returns true if |url| is just a host (e.g. "http://www.google.com/") and + // not some other subpage (e.g. "http://www.google.com/foo.html"). + static bool IsHostOnly(const GURL& url); + + // Acts like the > operator for URLInfo classes. + static bool CompareHistoryMatch(const HistoryMatch& a, + const HistoryMatch& b); + + // Returns the set of prefixes to use for prefixes_. + static Prefixes GetPrefixes(); + + // Determines the relevance for some input, given its type and which match it + // is. If |match_type| is NORMAL, |match_number| is a number + // [0, kMaxSuggestions) indicating the relevance of the match (higher == more + // relevant). For other values of |match_type|, |match_number| is ignored. + static int CalculateRelevance(AutocompleteInput::Type input_type, + MatchType match_type, + size_t match_number); + + // Given the user's |input| and a |match| created from it, reduce the + // match's URL to just a host. If this host still matches the user input, + // return it. Returns the empty string on failure. + static GURL ConvertToHostOnly(const HistoryMatch& match, + const std::wstring& input); + + // See if a shorter version of the best match should be created, and if so + // place it at the front of |matches|. This can suggest history URLs that + // are prefixes of the best match (if they've been visited enough, compared + // to the best match), or create host-only suggestions even when they haven't + // been visited before: if the user visited http://example.com/asdf once, + // we'll suggest http://example.com/ even if they've never been to it. See + // the function body for the exact heuristics used. + static void PromoteOrCreateShorterSuggestion( + history::URLDatabase* db, + const HistoryURLProviderParams& params, + HistoryMatches* matches); + + // Ensures that |matches| contains an entry for |info|, which may mean adding + // a new such entry (using |input_location| and |match_in_scheme|). + // + // If |promote| is true, this also ensures the entry is the first element in + // |matches|, moving or adding it to the front as appropriate. When + // |promote| is false, existing matches are left in place, and newly added + // matches are placed at the back. + static void EnsureMatchPresent(const history::URLRow& info, + std::wstring::size_type input_location, + bool match_in_scheme, + HistoryMatches* matches, + bool promote); + + // Helper function that actually launches the two autocomplete passes. + void RunAutocompletePasses(const AutocompleteInput& input, + bool fixup_input_and_run_pass_1, + bool run_pass_2); + + // Returns the best prefix that begins |text|. "Best" means "greatest number + // of components". This may return NULL if no prefix begins |text|. + // + // |prefix_suffix| (which may be empty) is appended to every attempted + // prefix. This is useful when you need to figure out the innermost match + // for some user input in a URL. + const Prefix* BestPrefix(const std::wstring& text, + const std::wstring& prefix_suffix) const; + + // Adds the exact input for what the user has typed as input. This is + // called on the main thread to generate the first match synchronously. + void SuggestExactInput(const AutocompleteInput& input, bool trim_http); + + // Assumes |params| contains the "what you typed" suggestion created by + // SuggestExactInput(). Looks up its info in the DB. If found, fills in the + // title from the DB, promotes the match's priority to that of an inline + // autocomplete match (maybe it should be slightly better?), and places it on + // the front of |params|->matches (so we pick the right matches to throw away + // when culling redirects to/from it). Returns whether a match was promoted. + bool FixupExactSuggestion(history::URLDatabase* db, + HistoryURLProviderParams* params, + HistoryMatches* matches) const; + + // Determines if |match| is suitable for inline autocomplete, and promotes it + // if so. + bool PromoteMatchForInlineAutocomplete(HistoryURLProviderParams* params, + const HistoryMatch& match); + + // Sorts the given list of matches. + void SortMatches(HistoryMatches* matches) const; + + // Removes results that have been rarely typed or visited, and not any time + // recently. The exact parameters for this heuristic can be found in the + // function body. + void CullPoorMatches(HistoryMatches* matches) const; + + // Removes results that redirect to each other, leaving at most |max_results| + // results. + void CullRedirects(history::HistoryBackend* backend, + HistoryMatches* matches, + size_t max_results) const; + + // Helper function for CullRedirects, this removes all but the first + // occurance of [any of the set of strings in |remove|] from the |matches| + // list. + // + // The return value is the index of the item that is after the item in the + // input identified by |source_index|. If |source_index| or an item before + // is removed, the next item will be shifted, and this allows the caller to + // pick up on the next one when this happens. + size_t RemoveSubsequentMatchesOf( + HistoryMatches* matches, + size_t source_index, + const std::vector<GURL>& remove) const; + + // Converts a line from the database into an autocomplete match for display. + AutocompleteMatch HistoryMatchToACMatch(HistoryURLProviderParams* params, + const HistoryMatch& history_match, + MatchType match_type, + size_t match_number); + + // This is only non-null for testing, otherwise the HistoryService from the + // Profile is used. + HistoryService* history_service_; + + // Prefixes to try appending to user input when looking for a match. + const Prefixes prefixes_; + + // Params for the current query. The provider should not free this directly; + // instead, it is passed as a parameter through the history backend, and the + // parameter itself is freed once it's no longer needed. The only reason we + // keep this member is so we can set the cancel bit on it. + HistoryURLProviderParams* params_; +}; + +#endif // CHROME_BROWSER_AUTOCOMPLETE_HISTORY_URL_PROVIDER_H__ diff --git a/chrome/browser/autocomplete/history_url_provider_unittest.cc b/chrome/browser/autocomplete/history_url_provider_unittest.cc new file mode 100644 index 0000000..44486e7 --- /dev/null +++ b/chrome/browser/autocomplete/history_url_provider_unittest.cc @@ -0,0 +1,382 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "base/file_util.h" +#include "base/message_loop.h" +#include "base/path_service.h" +#include "chrome/browser/autocomplete/history_url_provider.h" +#include "chrome/browser/history/history.h" +#include "testing/gtest/include/gtest/gtest.h" + +struct TestURLInfo { + std::wstring url; + std::wstring title; + int visit_count; + int typed_count; + bool starred; +}; + +// Contents of the test database. +static TestURLInfo test_db[] = { + {L"http://www.google.com/", L"Google", 3, 3, false}, + + // High-quality pages should get a host synthesized as a lower-quality match. + {L"http://slashdot.org/favorite_page.html", L"Favorite page", 200, 100, + false}, + + // Less popular pages should have hosts synthesized as higher-quality + // matches. + {L"http://kerneltrap.org/not_very_popular.html", L"Less popular", 4, 0, + false}, + + // Unpopular pages should not appear in the results at all. + {L"http://freshmeat.net/unpopular.html", L"Unpopular", 1, 1, false}, + + // If a host has a match, we should pick it up during host synthesis. + {L"http://news.google.com/?ned=us&topic=n", L"Google News - U.S.", 2, 2, + false}, + {L"http://news.google.com/", L"Google News", 1, 1, false}, + + // Suggested short URLs must be "good enough" and must match user input. + {L"http://foo.com/", L"Dir", 5, 5, false}, + {L"http://foo.com/dir/", L"Dir", 2, 2, false}, + {L"http://foo.com/dir/another/", L"Dir", 5, 1, false}, + {L"http://foo.com/dir/another/again/", L"Dir", 10, 0, false}, + {L"http://foo.com/dir/another/again/myfile.html", L"File", 10, 2, false}, + + // Starred state is more important than visit count (but less important than + // typed count) when sorting URLs. The order in which the URLs were starred + // shouldn't matter. + // We throw in a lot of extra URLs here to make sure we're testing the + // history database's query, not just the autocomplete provider. + {L"http://startest.com/y/a", L"A", 2, 2, true}, + {L"http://startest.com/y/b", L"B", 5, 2, false}, + {L"http://startest.com/x/c", L"C", 5, 2, true}, + {L"http://startest.com/x/d", L"D", 5, 5, false}, + {L"http://startest.com/y/e", L"E", 4, 2, false}, + {L"http://startest.com/y/f", L"F", 3, 2, false}, + {L"http://startest.com/y/g", L"G", 3, 2, false}, + {L"http://startest.com/y/h", L"H", 3, 2, false}, + {L"http://startest.com/y/i", L"I", 3, 2, false}, + {L"http://startest.com/y/j", L"J", 3, 2, false}, + {L"http://startest.com/y/k", L"K", 3, 2, false}, + {L"http://startest.com/y/l", L"L", 3, 2, false}, + {L"http://startest.com/y/m", L"M", 3, 2, false}, + + // A file: URL is useful for testing that fixup does the right thing w.r.t. + // the number of trailing slashes on the user's input. + {L"file:///C:/foo.txt", L"", 2, 2, false}, + + // Results with absurdly high typed_counts so that very generic queries like + // "http" will give consistent results even if more data is added above. + {L"http://bogussite.com/a", L"Bogus A", 10002, 10000, false}, + {L"http://bogussite.com/b", L"Bogus B", 10001, 10000, false}, + {L"http://bogussite.com/c", L"Bogus C", 10000, 10000, false}, +}; + +class HistoryURLProviderTest : public testing::Test, + public ACProviderListener { + // ACProviderListener + virtual void OnProviderUpdate(bool updated_matches); + + protected: + // testing::Test + virtual void SetUp(); + virtual void TearDown(); + + // Fills test data into the history system + void FillData(); + + // Runs an autocomplete query on |text| and checks to see that the returned + // results' destination URLs match those provided. + void RunTest(const std::wstring text, + const std::wstring& desired_tld, + bool prevent_inline_autocomplete, + const std::wstring* expected_urls, + int num_results); + + ACMatches matches_; + scoped_refptr<HistoryService> history_service_; + + private: + std::wstring history_dir_; + scoped_refptr<HistoryURLProvider> autocomplete_; +}; + +void HistoryURLProviderTest::OnProviderUpdate(bool updated_matches) { + if (autocomplete_->done()) + MessageLoop::current()->Quit(); +} + +void HistoryURLProviderTest::SetUp() { + PathService::Get(base::DIR_TEMP, &history_dir_); + file_util::AppendToPath(&history_dir_, L"HistoryURLProviderTest"); + file_util::Delete(history_dir_, true); // Normally won't exist. + file_util::CreateDirectoryW(history_dir_); + + history_service_ = new HistoryService; + history_service_->Init(history_dir_); + + autocomplete_ = new HistoryURLProvider(this, history_service_); + + FillData(); +} + +void HistoryURLProviderTest::TearDown() { + history_service_->SetOnBackendDestroyTask(new MessageLoop::QuitTask); + history_service_->Cleanup(); + autocomplete_ = NULL; + history_service_ = NULL; + + // Wait for history thread to complete (the QuitTask will cause it to exit + // on destruction). Note: if this never terminates, somebody is probably + // leaking a reference to the history backend, so it never calls our + // destroy task. + MessageLoop::current()->Run(); + + file_util::Delete(history_dir_, true); +} + +void HistoryURLProviderTest::FillData() { + // All visits are a long time ago (some tests require this since we do some + // special logic for things visited very recently). Note that this time must + // be more recent than the "archived history" threshold for the data to go + // into the main database. + // + // TODO(brettw) It would be nice if we could test this behavior, in which + // case the time would be specifed in the test_db structure. + Time visit_time = Time::Now() - TimeDelta::FromDays(80); + + for (int i = 0; i < arraysize(test_db); ++i) { + const TestURLInfo& cur = test_db[i]; + const GURL current_url(cur.url); + history_service_->AddPageWithDetails(current_url, cur.title, + cur.visit_count, cur.typed_count, + visit_time, false); + if (cur.starred) { + history::StarredEntry star_entry; + star_entry.type = history::StarredEntry::URL; + star_entry.parent_group_id = HistoryService::kBookmarkBarID; + star_entry.url = current_url; + history_service_->CreateStarredEntry(star_entry, NULL, NULL); + } + } +} + +void HistoryURLProviderTest::RunTest(const std::wstring text, + const std::wstring& desired_tld, + bool prevent_inline_autocomplete, + const std::wstring* expected_urls, + int num_results) { + AutocompleteInput input(text, desired_tld, prevent_inline_autocomplete); + autocomplete_->Start(input, false, false); + if (!autocomplete_->done()) + MessageLoop::current()->Run(); + + matches_ = autocomplete_->matches(); + ASSERT_EQ(num_results, matches_.size()); + for (int i = 0; i < num_results; ++i) + EXPECT_EQ(expected_urls[i], matches_[i].destination_url); +} + +TEST_F(HistoryURLProviderTest, PromoteShorterURLs) { + // Test that hosts get synthesized below popular pages. + const std::wstring expected_nonsynth[] = { + L"http://slash/", + L"http://slashdot.org/favorite_page.html", + L"http://slashdot.org/", + }; + RunTest(L"slash", std::wstring(), true, expected_nonsynth, + arraysize(expected_nonsynth)); + + // Test that hosts get synthesized above less popular pages. + const std::wstring expected_synth[] = { + L"http://kernel/", + L"http://kerneltrap.org/", + L"http://kerneltrap.org/not_very_popular.html", + }; + RunTest(L"kernel", std::wstring(), true, expected_synth, + arraysize(expected_synth)); + + // Test that unpopular pages are ignored completely. + const std::wstring expected_what_you_typed_only[] = { + L"http://fresh/", + }; + RunTest(L"fresh", std::wstring(), true, expected_what_you_typed_only, + arraysize(expected_what_you_typed_only)); + + // Test that if we have a synthesized host that matches a suggestion, they + // get combined into one. + const std::wstring expected_combine[] = { + L"http://news/", + L"http://news.google.com/", + L"http://news.google.com/?ned=us&topic=n", + }; + RunTest(L"news", std::wstring(), true, expected_combine, + arraysize(expected_combine)); + // The title should also have gotten set properly on the host for the + // synthesized one, since it was also in the results. + EXPECT_EQ(std::wstring(L"Google News"), matches_[1].description); + + // Test that short URL matching works correctly as the user types more + // (several tests): + // The entry for foo.com is the best of all five foo.com* entries. + const std::wstring short_1[] = { + L"http://foo/", + L"http://foo.com/", + L"http://foo.com/dir/another/again/myfile.html", + L"http://foo.com/dir/", + }; + RunTest(L"foo", std::wstring(), true, short_1, arraysize(short_1)); + + // When the user types the whole host, make sure we don't get two results for + // it. + const std::wstring short_2[] = { + L"http://foo.com/", + L"http://foo.com/dir/another/again/myfile.html", + L"http://foo.com/dir/", + L"http://foo.com/dir/another/", + }; + RunTest(L"foo.com", std::wstring(), true, short_2, arraysize(short_2)); + RunTest(L"foo.com/", std::wstring(), true, short_2, arraysize(short_2)); + + // The filename is the second best of the foo.com* entries, but there is a + // shorter URL that's "good enough". The host doesn't match the user input + // and so should not appear. + const std::wstring short_3[] = { + L"http://foo.com/d", + L"http://foo.com/dir/another/", + L"http://foo.com/dir/another/again/myfile.html", + L"http://foo.com/dir/", + }; + RunTest(L"foo.com/d", std::wstring(), true, short_3, arraysize(short_3)); + + // We shouldn't promote shorter URLs than the best if they're not good + // enough. + const std::wstring short_4[] = { + L"http://foo.com/dir/another/a", + L"http://foo.com/dir/another/again/myfile.html", + L"http://foo.com/dir/another/again/", + }; + RunTest(L"foo.com/dir/another/a", std::wstring(), true, short_4, + arraysize(short_4)); +} + +TEST_F(HistoryURLProviderTest, Starred) { + // Test that starred pages sort properly. + const std::wstring star_1[] = { + L"http://startest/", + L"http://startest.com/x/d", + L"http://startest.com/x/c", + L"http://startest.com/y/a", + }; + RunTest(L"startest", std::wstring(), true, star_1, arraysize(star_1)); + const std::wstring star_2[] = { + L"http://startest.com/y", + L"http://startest.com/y/a", + L"http://startest.com/y/b", + L"http://startest.com/y/e", + }; + RunTest(L"startest.com/y", std::wstring(), true, star_2, arraysize(star_2)); +} + +TEST_F(HistoryURLProviderTest, CullRedirects) { + // URLs we will be using, plus the visit counts they will initially get + // (the redirect set below will also increment the visit counts). We want + // the results to be in A,B,C order. Note also that our visit counts are + // all high enough so that domain synthesizing won't get triggered. + struct RedirectCase { + const wchar_t* url; + int count; + }; + static const RedirectCase redirect[] = { + {L"http://redirects/A", 30}, + {L"http://redirects/B", 20}, + {L"http://redirects/C", 10} + }; + for (int i = 0; i < arraysize(redirect); i++) { + history_service_->AddPageWithDetails(GURL(redirect[i].url), L"Title", + redirect[i].count, redirect[i].count, + Time::Now(), false); + } + + // Create a B->C->A redirect chain, but set the visit counts such that they + // will appear in A,B,C order in the results. The autocomplete query will + // search for the most recent visit when looking for redirects, so this will + // be found even though the previous visits had no redirects. + HistoryService::RedirectList redirects_to_a; + redirects_to_a.push_back(GURL(redirect[1].url)); + redirects_to_a.push_back(GURL(redirect[2].url)); + redirects_to_a.push_back(GURL(redirect[0].url)); + history_service_->AddPage(GURL(redirect[0].url), NULL, 0, GURL(), + PageTransition::TYPED, redirects_to_a); + + // Because all the results are part of a redirect chain with other results, + // all but the first one (A) should be culled. We should get the default + // "what you typed" result, plus this one. + const std::wstring typing(L"http://redirects/"); + const std::wstring expected_results[] = { + typing, + redirect[0].url}; + RunTest(typing, std::wstring(), true, expected_results, + arraysize(expected_results)); +} + +TEST_F(HistoryURLProviderTest, Fixup) { + // Test for various past crashes we've had. + RunTest(L"\\", std::wstring(), false, NULL, 0); + + RunTest(L"#", std::wstring(), false, NULL, 0); + + const std::wstring crash_1[] = {L"http://%20/"}; + RunTest(L"%20", std::wstring(), false, crash_1, arraysize(crash_1)); + + // Fixing up "file:" should result in an inline autocomplete offset of just + // after "file:", not just after "file://". + const std::wstring input_1(L"file:"); + const std::wstring fixup_1[] = {L"file:///", L"file:///C:/foo.txt"}; + RunTest(input_1, std::wstring(), false, fixup_1, arraysize(fixup_1)); + EXPECT_EQ(input_1.length(), matches_[1].inline_autocomplete_offset); + + // Fixing up "http:/" should result in an inline autocomplete offset of just + // after "http:/", not just after "http:". + const std::wstring input_2(L"http:/"); + const std::wstring fixup_2[] = { + L"http://bogussite.com/a", + L"http://bogussite.com/b", + L"http://bogussite.com/c", + }; + RunTest(input_2, std::wstring(), false, fixup_2, arraysize(fixup_2)); + EXPECT_EQ(input_2.length(), matches_[0].inline_autocomplete_offset); + + // Adding a TLD to a small number like "56" should result in "www.56.com" + // rather than "0.0.0.56.com". + std::wstring fixup_3[] = {L"http://www.56.com/"}; + RunTest(L"56", L"com", true, fixup_3, arraysize(fixup_3)); +}
\ No newline at end of file diff --git a/chrome/browser/autocomplete/keyword_provider.cc b/chrome/browser/autocomplete/keyword_provider.cc new file mode 100644 index 0000000..cc56029 --- /dev/null +++ b/chrome/browser/autocomplete/keyword_provider.cc @@ -0,0 +1,304 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "chrome/browser/autocomplete/keyword_provider.h" + +#include <algorithm> + +#include "base/string_util.h" +#include "chrome/browser/profile.h" +#include "chrome/browser/template_url.h" +#include "chrome/browser/template_url_model.h" +#include "chrome/common/l10n_util.h" +#include "net/base/escape.h" +#include "net/base/net_util.h" + +#include "generated_resources.h" + +static const wchar_t kSearchDescriptionParameter[](L"%1"); +static const wchar_t kSearchValueParameter[](L"%2"); + +// static +std::wstring KeywordProvider::SplitReplacementStringFromInput( + const std::wstring& input) { + // The input may contain leading whitespace, strip it. + std::wstring trimmed_input; + TrimWhitespace(input, TRIM_LEADING, &trimmed_input); + + // And extract the replacement string. + std::wstring remaining_input; + SplitKeywordFromInput(trimmed_input, &remaining_input); + return remaining_input; +} + +KeywordProvider::KeywordProvider(ACProviderListener* listener, Profile* profile) + : AutocompleteProvider(listener, profile, "Keyword"), + model_(NULL) { +} + +KeywordProvider::KeywordProvider(ACProviderListener* listener, + TemplateURLModel* model) + : AutocompleteProvider(listener, NULL, "Keyword"), + model_(model) { +} + + +class KeywordProvider::CompareQuality { + public: + // A keyword is of higher quality when a greater fraction of it has been + // typed, that is, when it is shorter. + // + // TODO(pkasting): http://b/740691 Most recent and most frequent keywords are + // probably better rankings than the fraction of the keyword typed. We should + // always put any exact matches first no matter what, since the code in + // Start() assumes this (and it makes sense). + bool operator()(const std::wstring& keyword1, + const std::wstring& keyword2) const { + return keyword1.length() < keyword2.length(); + } +}; + +void KeywordProvider::Start(const AutocompleteInput& input, + bool minimal_changes, + bool synchronous_only) { + matches_.clear(); + + if ((input.type() == AutocompleteInput::INVALID) || + (input.type() == AutocompleteInput::FORCED_QUERY)) + return; + + // Split user input into a keyword and some query input. + // + // We want to suggest keywords even when users have started typing URLs, on + // the assumption that they might not realize they no longer need to go to a + // site to be able to search it. So we call CleanUserInputKeyword() to strip + // any initial scheme and/or "www.". NOTE: Any heuristics or UI used to + // automatically/manually create keywords will need to be in sync with + // whatever we do here! + // + // TODO(pkasting): http://b/1112681 If someday we remember usage frequency for + // keywords, we might suggest keywords that haven't even been partially typed, + // if the user uses them enough and isn't obviously typing something else. In + // this case we'd consider all input here to be query input. + std::wstring remaining_input; + std::wstring keyword(TemplateURLModel::CleanUserInputKeyword( + SplitKeywordFromInput(input.text(), &remaining_input))); + if (keyword.empty()) + return; + + // Make sure the model is loaded. This is cheap and quickly bails out if + // the model is already loaded. + TemplateURLModel* model = profile_ ? profile_->GetTemplateURLModel() : model_; + DCHECK(model); + model->Load(); + + // Get the best matches for this keyword. + // + // NOTE: We could cache the previous keywords and reuse them here in the + // |minimal_changes| case, but since we'd still have to recalculate their + // relevances and we can just recreate the results synchronously anyway, we + // don't bother. + // + // TODO(pkasting): http://b/893701 We should remember the user's use of a + // search query both from the autocomplete popup and from web pages + // themselves. + std::vector<std::wstring> keyword_matches; + model->FindMatchingKeywords(keyword, !remaining_input.empty(), + &keyword_matches); + if (keyword_matches.empty()) + return; + std::sort(keyword_matches.begin(), keyword_matches.end(), CompareQuality()); + + // Limit to one exact or three inexact matches, and mark them up for display + // in the autocomplete popup. + // Any exact match is going to be the highest quality match, and thus at the + // front of our vector. + if (keyword_matches.front() == keyword) { + matches_.push_back(CreateAutocompleteMatch(model, keyword, input, + keyword.length(), + remaining_input)); + } else { + if (keyword_matches.size() > max_matches()) { + keyword_matches.erase(keyword_matches.begin() + max_matches(), + keyword_matches.end()); + } + for (std::vector<std::wstring>::const_iterator i(keyword_matches.begin()); + i != keyword_matches.end(); ++i) { + matches_.push_back(CreateAutocompleteMatch(model, *i, input, + keyword.length(), + remaining_input)); + } + } +} + +// static +std::wstring KeywordProvider::SplitKeywordFromInput( + const std::wstring& input, + std::wstring* remaining_input) { + // Find end of first token. The AutocompleteController has trimmed leading + // whitespace, so we need not skip over that. + const size_t first_white(input.find_first_of(kWhitespaceWide)); + DCHECK(first_white != 0); + if (first_white == std::wstring::npos) + return input; // Only one token provided. + + // Set |remaining_input| to everything after the first token. + DCHECK(remaining_input != NULL); + const size_t first_nonwhite(input.find_first_not_of(kWhitespaceWide, + first_white)); + if (first_nonwhite != std::wstring::npos) + remaining_input->assign(input.begin() + first_nonwhite, input.end()); + + // Return first token as keyword. + return input.substr(0, first_white); +} + +// static +void KeywordProvider::FillInURLAndContents( + const std::wstring& remaining_input, + const TemplateURL* element, + AutocompleteMatch* match) { + DCHECK(!element->short_name().empty()); + DCHECK(element->url()); + DCHECK(element->url()->IsValid()); + if (remaining_input.empty()) { + if (element->url()->SupportsReplacement()) { + // No query input; return a generic, no-destination placeholder. + match->contents.assign(l10n_util::GetStringF(IDS_KEYWORD_SEARCH, + element->short_name(), + l10n_util::GetString(IDS_EMPTY_KEYWORD_VALUE))); + match->contents_class.push_back( + ACMatchClassification(0, ACMatchClassification::DIM)); + } else { + // Keyword that has no replacement text (aka a shorthand for a URL). + match->destination_url.assign(element->url()->url()); + match->contents.assign(element->short_name()); + AutocompleteMatch::ClassifyLocationInString(0, match->contents.length(), + match->contents.length(), ACMatchClassification::NONE, + &match->contents_class); + } + } else { + // Create destination URL by escaping user input and substituting into + // keyword template URL. The escaping here handles whitespace in user + // input, but we rely on later canonicalization functions to do more + // fixup to make the URL valid if necessary. + DCHECK(element->url()->SupportsReplacement()); + match->destination_url.assign(element->url()->ReplaceSearchTerms( + *element, remaining_input, TemplateURLRef::NO_SUGGESTIONS_AVAILABLE, + std::wstring())); + std::vector<size_t> content_param_offsets; + match->contents.assign(l10n_util::GetStringF(IDS_KEYWORD_SEARCH, + element->short_name(), + remaining_input, + &content_param_offsets)); + if (content_param_offsets.size() == 2) { + AutocompleteMatch::ClassifyLocationInString(content_param_offsets[1], + remaining_input.length(), match->contents.length(), + ACMatchClassification::NONE, &match->contents_class); + } else { + // See comments on an identical NOTREACHED() in search_provider.cc. + NOTREACHED(); + } + } +} + +// static +int KeywordProvider::CalculateRelevance(AutocompleteInput::Type type, + bool complete, + bool is_bookmark_keyword) { + if (complete && is_bookmark_keyword) + return 1500; + + switch (type) { + case AutocompleteInput::UNKNOWN: + case AutocompleteInput::REQUESTED_URL: + return complete ? 1100 : 450; + + case AutocompleteInput::URL: + return complete ? 1100 : 700; + + case AutocompleteInput::QUERY: + return complete ? 1400 : 650; + + default: + NOTREACHED(); + return 0; + } +} + +AutocompleteMatch KeywordProvider::CreateAutocompleteMatch( + TemplateURLModel *model, + const std::wstring keyword, + const AutocompleteInput& input, + size_t prefix_length, + const std::wstring& remaining_input) { + DCHECK(model); + // Get keyword data from data store. + const TemplateURL* element(model->GetTemplateURLForKeyword(keyword)); + DCHECK(element && element->url()); + const bool supports_replacement = element->url()->SupportsReplacement(); + + // Create an edit entry of "[keyword] [remaining input]". This is helpful + // even when [remaining input] is empty, as the user can select the popup + // choice and immediately begin typing in query input. + const bool keyword_complete = (prefix_length == keyword.length()); + AutocompleteMatch result(this, + CalculateRelevance(input.type(), keyword_complete, !supports_replacement), + false); + result.type = AutocompleteMatch::KEYWORD; + result.fill_into_edit.assign(keyword); + if (!remaining_input.empty() || !keyword_complete || supports_replacement) + result.fill_into_edit.push_back(L' '); + result.fill_into_edit.append(remaining_input); + if (!input.prevent_inline_autocomplete() && + (keyword_complete || remaining_input.empty())) + result.inline_autocomplete_offset = input.text().length(); + + // Create destination URL and popup entry content by substituting user input + // into keyword templates. + FillInURLAndContents(remaining_input, element, &result); + + // Create popup entry description based on the keyword name. + result.description.assign(l10n_util::GetStringF( + IDS_AUTOCOMPLETE_KEYWORD_DESCRIPTION, keyword)); + if (supports_replacement) + result.template_url = element; + static const std::wstring kKeywordDesc(l10n_util::GetString( + IDS_AUTOCOMPLETE_KEYWORD_DESCRIPTION)); + AutocompleteMatch::ClassifyLocationInString(kKeywordDesc.find(L"%s"), + prefix_length, + result.description.length(), + ACMatchClassification::DIM, + &result.description_class); + + // Keyword searches don't look like URLs. + result.transition = PageTransition::GENERATED; + + return result; +} diff --git a/chrome/browser/autocomplete/keyword_provider.h b/chrome/browser/autocomplete/keyword_provider.h new file mode 100644 index 0000000..1964006 --- /dev/null +++ b/chrome/browser/autocomplete/keyword_provider.h @@ -0,0 +1,132 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// This file contains the keyword autocomplete provider. The keyword provider is +// responsible for remembering/suggesting user "search keyword queries" (e.g. +// "imdb Godzilla") and then fixing them up into valid URLs. An instance of it +// gets created and managed by the autocomplete controller. KeywordProvider +// uses a TemplateURLModel to find the set of keywords. +// +// For more information on the autocomplete system in general, including how +// the autocomplete controller and autocomplete providers work, see +// chrome/browser/autocomplete.h. + +#ifndef CHROME_BROWSER_AUTOCOMPLETE_KEYWORD_PROVIDER_H__ +#define CHROME_BROWSER_AUTOCOMPLETE_KEYWORD_PROVIDER_H__ + +#include <map> +#include <string> +#include <vector> +#include "chrome/browser/autocomplete/autocomplete.h" + +class Profile; +class TemplateURL; +class TemplateURLModel; + +/****************************** KeywordProvider ******************************/ + +// Autocomplete provider for keyword input. +// +// After construction, the autocomplete controller repeatedly calls Start() +// with some user input, each time expecting to receive a small set of the best +// matches (either synchronously or asynchronously). +// +// To construct these matches, the provider treats user input as a series of +// whitespace-delimited tokens and tries to match the first token as the prefix +// of a known "keyword". A keyword is some string that maps to a search query +// URL; the rest of the user's input is taken as the input to the query. For +// example, the keyword "bug" might map to the URL "http://b/issue?id=%s", so +// input like "bug 123" would become "http://b/issue?id=123". +// +// Because we do prefix matching, user input could match more than one keyword +// at once. (Example: the input "f jazz" matches all keywords starting with +// "f".) We return the best matches, up to three. +// +// The resulting matches are shown with content specified by the keyword +// (usually "Search [name] for %s"), description "(Keyword: [keyword])", and +// action "[keyword] %s". If the user has typed a (possibly partial) keyword +// but no search terms, the suggested result is shown greyed out, with +// "<enter term(s)>" as the substituted input, and does nothing when selected. +class KeywordProvider : public AutocompleteProvider { + public: + KeywordProvider(ACProviderListener* listener, Profile* profile); + // For testing. + KeywordProvider(ACProviderListener* listener, TemplateURLModel* model); + + // Returns the replacement string from the user input. The replacement + // string is the portion of the input that does not contain the keyword. + // For example, the replacement string for "b blah" is blah. + static std::wstring SplitReplacementStringFromInput( + const std::wstring& input); + + // AutocompleteProvider + virtual void Start(const AutocompleteInput& input, + bool minimal_changes, + bool synchronous_only); + + private: + // Helper functor for Start(), for sorting keyword matches by quality. + class CompareQuality; + + // Extracts the next whitespace-delimited token from input and returns it. + // Sets |remaining_input| to everything after the first token (skipping over + // intervening whitespace). + static std::wstring SplitKeywordFromInput(const std::wstring& input, + std::wstring* remaining_input); + + // Fills in the "destination_url" and "contents" fields of |match| with the + // provided user input and keyword data. + static void FillInURLAndContents( + const std::wstring& remaining_input, + const TemplateURL* element, + AutocompleteMatch* match); + + // Determines the relevance for some input, given its type, whether the user + // typed the complete keyword, and whether the keyword is a bookmark keyword + // (i.e. one that does not support replacement). + static int CalculateRelevance(AutocompleteInput::Type type, + bool complete, + bool is_bookmark_keyword); + + // Creates a fully marked-up AutocompleteMatch from the user's input. + AutocompleteMatch CreateAutocompleteMatch( + TemplateURLModel* model, + const std::wstring keyword, + const AutocompleteInput& input, + size_t prefix_length, + const std::wstring& remaining_input); + + // Model for the keywords. This is only non-null when testing, otherwise the + // TemplateURLModel from the Profile is used. + TemplateURLModel* model_; + + DISALLOW_EVIL_CONSTRUCTORS(KeywordProvider); +}; + +#endif // CHROME_BROWSER_AUTOCOMPLETE_KEYWORD_PROVIDER_H__ diff --git a/chrome/browser/autocomplete/keyword_provider_unittest.cc b/chrome/browser/autocomplete/keyword_provider_unittest.cc new file mode 100644 index 0000000..71cab7d --- /dev/null +++ b/chrome/browser/autocomplete/keyword_provider_unittest.cc @@ -0,0 +1,214 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "base/message_loop.h" +#include "chrome/browser/autocomplete/keyword_provider.h" +#include "chrome/browser/template_url.h" +#include "chrome/browser/template_url_model.h" +#include "googleurl/src/gurl.h" +#include "testing/gtest/include/gtest/gtest.h" + +class KeywordProviderTest : public testing::Test { + protected: + struct test_data { + const std::wstring input; + const int num_results; + const std::wstring output[3]; + }; + + KeywordProviderTest() : kw_provider_(NULL) { } + virtual ~KeywordProviderTest() { } + + virtual void SetUp(); + virtual void TearDown(); + + void RunTest(test_data* keyword_cases, + int num_cases, + std::wstring AutocompleteMatch::* member); + + protected: + scoped_refptr<KeywordProvider> kw_provider_; + scoped_ptr<TemplateURLModel> model_; +}; + +void KeywordProviderTest::SetUp() { + static const TemplateURLModel::Initializer kTestKeywordData[] = { + { L"aa", L"aa.com?foo=%s", L"aa" }, + { L"aaaa", L"http://aaaa/?aaaa=1&b=%s&c", L"aaaa" }, + { L"aaaaa", L"%s", L"aaaaa" }, + { L"ab", L"bogus URL %s", L"ab" }, + { L"weasel", L"weasel%sweasel", L"weasel" }, + { L"www", L" +%2B?=%sfoo ", L"www" }, + { L"z", L"%s=z", L"z" }, + }; + + model_.reset(new TemplateURLModel(kTestKeywordData, arraysize(kTestKeywordData))); + kw_provider_ = new KeywordProvider(NULL, model_.get()); +} + +void KeywordProviderTest::TearDown() { + model_.reset(); + kw_provider_ = NULL; +} + +void KeywordProviderTest::RunTest( + test_data* keyword_cases, + int num_cases, + std::wstring AutocompleteMatch::* member) { + ACMatches matches; + for (int i = 0; i < num_cases; ++i) { + AutocompleteInput input(keyword_cases[i].input, std::wstring(), true); + kw_provider_->Start(input, false, false); + EXPECT_TRUE(kw_provider_->done()); + matches = kw_provider_->matches(); + EXPECT_EQ(keyword_cases[i].num_results, matches.size()) << + L"Input was: " + keyword_cases[i].input; + if (matches.size() == keyword_cases[i].num_results) { + for (int j = 0; j < keyword_cases[i].num_results; ++j) { + EXPECT_EQ(keyword_cases[i].output[j], matches[j].*member); + } + } + } +} + +TEST_F(KeywordProviderTest, Edit) { + test_data edit_cases[] = { + // Searching for a nonexistent prefix should give nothing. + {L"Not Found", 0, {}}, + {L"aaaaaNot Found", 0, {}}, + + // Check that tokenization only collapses whitespace between first tokens, + // no-query-input cases have a space appended, and action is not escaped. + {L"z foo", 1, {L"z foo"}}, + {L"z", 1, {L"z "}}, + {L"z \t", 1, {L"z "}}, + {L"z a b c++", 1, {L"z a b c++"}}, + + // Matches should be limited to three, and sorted in quality order, not + // alphabetical. + {L"aaa", 2, {L"aaaa ", L"aaaaa "}}, + {L"a 1 2 3", 3, {L"aa 1 2 3", L"ab 1 2 3", L"aaaa 1 2 3"}}, + {L"www.a", 3, {L"aa ", L"ab ", L"aaaa "}}, + // Exact matches should prevent returning inexact matches. + {L"aaaa foo", 1, {L"aaaa foo"}}, + {L"www.aaaa foo", 1, {L"aaaa foo"}}, + + // Clean up keyword input properly. + {L"www", 1, {L"www "}}, + {L"www.", 0, {}}, + {L"www.w w", 2, {L"www w", L"weasel w"}}, + {L"http://www", 1, {L"www "}}, + {L"http://www.", 0, {}}, + {L"ftp: blah", 0, {}}, + {L"mailto:z", 1, {L"z "}}, + }; + + RunTest(edit_cases, arraysize(edit_cases), + &AutocompleteMatch::fill_into_edit); +} + +TEST_F(KeywordProviderTest, URL) { + test_data url_cases[] = { + // No query input -> empty destination URL. + {L"z", 1, {L""}}, + {L"z \t", 1, {L""}}, + + // Check that tokenization only collapses whitespace between first tokens + // and query input, but not rest of URL, is escaped. + {L"z a b c++", 1, {L"a+++b+++c%2B%2B=z"}}, + {L"www.www www", 1, {L" +%2B?=wwwfoo "}}, + + // Substitution should work with various locations of the "%s". + {L"aaa 1a2b", 2, {L"http://aaaa/?aaaa=1&b=1a2b&c", L"1a2b"}}, + {L"a 1 2 3", 3, {L"aa.com?foo=1+2+3", L"bogus URL 1+2+3", + L"http://aaaa/?aaaa=1&b=1+2+3&c"}}, + {L"www.w w", 2, {L" +%2B?=wfoo ", L"weaselwweasel"}}, + }; + + RunTest(url_cases, arraysize(url_cases), + &AutocompleteMatch::destination_url); +} + +TEST_F(KeywordProviderTest, Contents) { + test_data contents_cases[] = { + // No query input -> substitute "<enter query>" into contents. + {L"z", 1, {L"Search z for <enter query>"}}, + {L"z \t", 1, {L"Search z for <enter query>"}}, + + // Check that tokenization only collapses whitespace between first tokens + // and contents are not escaped or unescaped. + {L"z a b c++", 1, {L"Search z for a b c++"}}, + {L"www.www www", 1, {L"Search www for www"}}, + + // Substitution should work with various locations of the "%s". + {L"aaa", 2, {L"Search aaaa for <enter query>", + L"Search aaaaa for <enter query>"}}, + {L"a 1 2 3", 3, {L"Search aa for 1 2 3", L"Search ab for 1 2 3", + L"Search aaaa for 1 2 3"}}, + {L"www.w w", 2, {L"Search www for w", L"Search weasel for w"}}, + }; + + RunTest(contents_cases, arraysize(contents_cases), + &AutocompleteMatch::contents); +} + +TEST_F(KeywordProviderTest, Description) { + test_data description_cases[] = { + // Whole keyword should be returned for both exact and inexact matches. + {L"z foo", 1, {L"(Keyword: z)"}}, + {L"a foo", 3, {L"(Keyword: aa)", L"(Keyword: ab)", + L"(Keyword: aaaa)"}}, + {L"ftp://www.www w", 1, {L"(Keyword: www)"}}, + + // Keyword should be returned regardless of query input. + {L"z", 1, {L"(Keyword: z)"}}, + {L"z \t", 1, {L"(Keyword: z)"}}, + {L"z a b c++", 1, {L"(Keyword: z)"}}, + }; + + RunTest(description_cases, arraysize(description_cases), + &AutocompleteMatch::description); +} + +TEST_F(KeywordProviderTest, AddKeyword) { + TemplateURL* template_url = new TemplateURL(); + std::wstring keyword(L"foo"); + std::wstring url(L"http://www.google.com/foo?q={searchTerms}"); + template_url->SetURL(url, 0, 0); + template_url->set_keyword(keyword); + template_url->set_short_name(L"Test"); + model_->Add(template_url); + ASSERT_TRUE(template_url == model_->GetTemplateURLForKeyword(keyword)); +} + +TEST_F(KeywordProviderTest, RemoveKeyword) { + std::wstring url(L"http://aaaa/?aaaa=1&b={searchTerms}&c"); + model_->Remove(model_->GetTemplateURLForKeyword(L"aaaa")); + ASSERT_TRUE(model_->GetTemplateURLForKeyword(L"aaaa") == NULL); +} diff --git a/chrome/browser/autocomplete/search_provider.cc b/chrome/browser/autocomplete/search_provider.cc new file mode 100644 index 0000000..40ba841 --- /dev/null +++ b/chrome/browser/autocomplete/search_provider.cc @@ -0,0 +1,622 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "chrome/browser/autocomplete/search_provider.h" + +#include "base/message_loop.h" +#include "base/string_util.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/google_util.h" +#include "chrome/browser/profile.h" +#include "chrome/browser/template_url_model.h" +#include "chrome/common/json_value_serializer.h" +#include "chrome/common/l10n_util.h" +#include "chrome/common/pref_names.h" +#include "chrome/common/pref_service.h" +#include "googleurl/src/url_util.h" +#include "net/base/escape.h" + +#include "generated_resources.h" + +const int SearchProvider::kQueryDelayMs = 200; + +void SearchProvider::Start(const AutocompleteInput& input, + bool minimal_changes, + bool synchronous_only) { + matches_.clear(); + + // Can't return search/suggest results for bogus input or if there is no + // profile. + if (!profile_ || (input.type() == AutocompleteInput::INVALID)) { + Stop(); + return; + } + + // Can't search with no default provider. + const TemplateURL* const current_default_provider = + profile_->GetTemplateURLModel()->GetDefaultSearchProvider(); + // TODO(pkasting): http://b/1155786 Eventually we should not need all these + // checks. + if (!current_default_provider || !current_default_provider->url() || + !current_default_provider->url()->SupportsReplacement()) { + Stop(); + return; + } + + // If we're still running an old query but have since changed the query text + // or the default provider, abort the query. + if (!done_ && (!minimal_changes || + (last_default_provider_ != current_default_provider))) + Stop(); + + // TODO(pkasting): http://b/1162970 We shouldn't need to structure-copy this. + // Nor should we need |last_default_provider_| just to know whether the + // provider changed. + default_provider_ = *current_default_provider; + last_default_provider_ = current_default_provider; + + if (input.text().empty()) { + // User typed "?" alone. Give them a placeholder result indicating what + // this syntax does. + AutocompleteMatch match; + static const std::wstring kNoQueryInput( + l10n_util::GetString(IDS_AUTOCOMPLETE_NO_QUERY)); + match.contents.assign(l10n_util::GetStringF( + IDS_AUTOCOMPLETE_SEARCH_CONTENTS, default_provider_.short_name(), + kNoQueryInput)); + match.contents_class.push_back( + ACMatchClassification(0, ACMatchClassification::DIM)); + match.type = AutocompleteMatch::SEARCH; + matches_.push_back(match); + Stop(); + return; + } + + input_ = input; + + StartOrStopHistoryQuery(minimal_changes, synchronous_only); + StartOrStopSuggestQuery(minimal_changes, synchronous_only); + ConvertResultsToAutocompleteMatches(); +} + +void SearchProvider::Run() { + // Start a new request with the current input. + DCHECK(!done_); + const TemplateURLRef* const suggestions_url = + default_provider_.suggestions_url(); + DCHECK(suggestions_url->SupportsReplacement()); + fetcher_.reset(new URLFetcher(GURL(suggestions_url->ReplaceSearchTerms( + default_provider_, input_.text(), + TemplateURLRef::NO_SUGGESTIONS_AVAILABLE, std::wstring())), + URLFetcher::GET, this)); + fetcher_->set_request_context(profile_->GetRequestContext()); + fetcher_->Start(); +} + +void SearchProvider::Stop() { + StopHistory(); + StopSuggest(); + done_ = true; +} + +void SearchProvider::OnURLFetchComplete(const URLFetcher* source, + const GURL& url, + const URLRequestStatus& status, + int response_code, + const ResponseCookies& cookie, + const std::string& data) { + DCHECK(!done_); + suggest_results_pending_ = false; + suggest_results_.clear(); + navigation_results_.clear(); + JSONStringValueSerializer deserializer(data); + Value* root_val = NULL; + have_suggest_results_ = status.is_success() && (response_code == 200) && + deserializer.Deserialize(&root_val) && ParseSuggestResults(root_val); + delete root_val; + ConvertResultsToAutocompleteMatches(); + listener_->OnProviderUpdate(!suggest_results_.empty()); +} + +void SearchProvider::StartOrStopHistoryQuery(bool minimal_changes, + bool synchronous_only) { + // For the minimal_changes case, if we finished the previous query and still + // have its results, or are allowed to keep running it, just do that, rather + // than starting a new query. + if (minimal_changes && + (have_history_results_ || (!done_ && !synchronous_only))) + return; + + // We can't keep running any previous query, so halt it. + StopHistory(); + + // We can't start a new query if we're only allowed synchronous results. + if (synchronous_only) + return; + + // Start the history query. + HistoryService* const history_service = + profile_->GetHistoryService(Profile::EXPLICIT_ACCESS); + history_service->GetMostRecentKeywordSearchTerms(default_provider_.id(), + input_.text(), static_cast<int>(max_matches()), + &history_request_consumer_, + NewCallback(this, &SearchProvider::OnGotMostRecentKeywordSearchTerms)); + history_request_pending_ = true; +} + +void SearchProvider::StartOrStopSuggestQuery(bool minimal_changes, + bool synchronous_only) { + // Don't run Suggest when off the record, the engine doesn't support it, or + // the user has disabled it. Also don't query the server for URLs that aren't + // http/https/ftp. Sending things like file: and data: is both a waste of + // time and a disclosure of potentially private, local data. + if (profile_->IsOffTheRecord() || + !default_provider_.suggestions_url() || + !profile_->GetPrefs()->GetBoolean(prefs::kSearchSuggestEnabled) || + ((input_.type() == AutocompleteInput::URL) && + (input_.scheme() != L"http") && (input_.scheme() != L"https") && + (input_.scheme() != L"ftp"))) { + StopSuggest(); + return; + } + + // For the minimal_changes case, if we finished the previous query and still + // have its results, or are allowed to keep running it, just do that, rather + // than starting a new query. + if (minimal_changes && + (have_suggest_results_ || (!done_ && !synchronous_only))) + return; + + // We can't keep running any previous query, so halt it. + StopSuggest(); + + // We can't start a new query if we're only allowed synchronous results. + if (synchronous_only) + return; + + // Kick off a timer that will start the URL fetch if it completes before + // the user types another character. + suggest_results_pending_ = true; + MessageLoop::current()->timer_manager()->ResetTimer(timer_.get()); +} + +void SearchProvider::StopHistory() { + history_request_consumer_.CancelAllRequests(); + history_request_pending_ = false; + history_results_.clear(); + have_history_results_ = false; +} + +void SearchProvider::StopSuggest() { + suggest_results_pending_ = false; + MessageLoop::current()->timer_manager()->StopTimer(timer_.get()); + fetcher_.reset(); // Stop any in-progress URL fetch. + suggest_results_.clear(); + have_suggest_results_ = false; + star_request_consumer_.CancelAllRequests(); + star_requests_pending_ = false; +} + +void SearchProvider::OnGotMostRecentKeywordSearchTerms( + CancelableRequestProvider::Handle handle, + HistoryResults* results) { + history_request_pending_ = false; + have_history_results_ = true; + history_results_ = *results; + ConvertResultsToAutocompleteMatches(); + listener_->OnProviderUpdate(!history_results_.empty()); +} + +void SearchProvider::OnQueryURLComplete(HistoryService::Handle handle, + bool success, + const history::URLRow* url_row, + history::VisitVector* unused) { + bool is_starred = success ? url_row->starred() : false; + star_requests_pending_ = false; + // We can't just use star_request_consumer_.HasPendingRequests() here; + // see comment in ConvertResultsToAutocompleteMatches(). + for (NavigationResults::iterator i(navigation_results_.begin()); + i != navigation_results_.end(); ++i) { + if (i->star_request_handle == handle) { + i->star_request_handle = 0; + i->starred = is_starred; + } else if (i->star_request_handle) { + star_requests_pending_ = true; + } + } + if (!star_requests_pending_) { + // No more requests. Notify the observer. + ConvertResultsToAutocompleteMatches(); + listener_->OnProviderUpdate(true); + } +} + +bool SearchProvider::ParseSuggestResults(Value* root_val) { + if (!root_val->IsType(Value::TYPE_LIST)) + return false; + ListValue* root_list = static_cast<ListValue*>(root_val); + + Value* query_val; + std::wstring query_str; + Value* result_val; + if ((root_list->GetSize() < 2) || !root_list->Get(0, &query_val) || + !query_val->GetAsString(&query_str) || (query_str != input_.text()) || + !root_list->Get(1, &result_val) || !result_val->IsType(Value::TYPE_LIST)) + return false; + + ListValue* description_list = NULL; + if (root_list->GetSize() > 2) { + // 3rd element: Description list. + Value* description_val; + if (root_list->Get(2, &description_val) && + description_val->IsType(Value::TYPE_LIST)) + description_list = static_cast<ListValue*>(description_val); + } + + // We don't care about the query URL list (the fourth element in the + // response) for now. + + // Parse optional data in the results from the Suggest server if any. + ListValue* type_list = NULL; + // 5th argument: Optional key-value pairs. + // TODO: We may iterate the 5th+ arguments of the root_list if any other + // optional data are defined. + if (root_list->GetSize() > 4) { + Value* optional_val; + if (root_list->Get(4, &optional_val) && + optional_val->IsType(Value::TYPE_DICTIONARY)) { + DictionaryValue* dict_val = static_cast<DictionaryValue*>(optional_val); + + // Parse Google Suggest specific type extension. + static const std::wstring kGoogleSuggestType(L"google:suggesttype"); + if (dict_val->HasKey(kGoogleSuggestType)) + dict_val->GetList(kGoogleSuggestType, &type_list); + } + } + + ListValue* result_list = static_cast<ListValue*>(result_val); + for (size_t i = 0; i < result_list->GetSize(); ++i) { + Value* suggestion_val; + std::wstring suggestion_str; + if (!result_list->Get(i, &suggestion_val) || + !suggestion_val->GetAsString(&suggestion_str)) + return false; + + Value* type_val; + std::wstring type_str; + if (type_list && type_list->Get(i, &type_val) && + type_val->GetAsString(&type_str) && (type_str == L"NAVIGATION")) { + Value* site_val; + std::wstring site_name; + if (navigation_results_.size() < max_matches() && + description_list && description_list->Get(i, &site_val) && + site_val->IsType(Value::TYPE_STRING) && + site_val->GetAsString(&site_name)) { + navigation_results_.push_back(NavigationResult(suggestion_str, + site_name)); + } + } else { + // TODO(kochi): Currently we treat a calculator result as a query, but it + // is better to have better presentation for caluculator results. + if (suggest_results_.size() < max_matches()) + suggest_results_.push_back(suggestion_str); + } + } + + // Request the star state for all URLs from the history service. + HistoryService* hs = profile_->GetHistoryService(Profile::EXPLICIT_ACCESS); + if (!hs) + return true; + + for (NavigationResults::iterator i(navigation_results_.begin()); + i != navigation_results_.end(); ++i) { + i->star_request_handle = hs->QueryURL(GURL(i->url), false, + &star_request_consumer_, + NewCallback(this, &SearchProvider::OnQueryURLComplete)); + } + star_requests_pending_ = !navigation_results_.empty(); + + return true; +} + +void SearchProvider::ConvertResultsToAutocompleteMatches() { + // Convert all the results to matches and add them to a map, so we can keep + // the most relevant match for each result. + MatchMap map; + const int did_not_accept_suggestion = suggest_results_.empty() ? + TemplateURLRef::NO_SUGGESTIONS_AVAILABLE : + TemplateURLRef::NO_SUGGESTION_CHOSEN; + const Time no_time; + AddMatchToMap(input_.text(), CalculateRelevanceForWhatYouTyped(), + did_not_accept_suggestion, &map); + + for (HistoryResults::const_iterator i(history_results_.begin()); + i != history_results_.end(); ++i) { + AddMatchToMap(i->term, CalculateRelevanceForHistory(i->time), + did_not_accept_suggestion, &map); + } + + for (size_t i = 0; i < suggest_results_.size(); ++i) { + AddMatchToMap(suggest_results_[i], CalculateRelevanceForSuggestion(i), + static_cast<int>(i), &map); + } + + // Now add the most relevant matches from the map to |matches_|. + matches_.clear(); + for (MatchMap::const_iterator i(map.begin()); i != map.end(); ++i) + matches_.push_back(i->second); + + if (navigation_results_.size()) { + // TODO(kochi): http://b/1170574 We add only one results for navigational + // suggestions. If we can get more useful information about the score, + // consider adding more results. + matches_.push_back(NavigationToMatch(navigation_results_[0], + CalculateRelevanceForNavigation(0), + navigation_results_[0].starred)); + } + + const size_t max_total_matches = max_matches() + 1; // 1 for "what you typed" + std::partial_sort(matches_.begin(), + matches_.begin() + std::min(max_total_matches, matches_.size()), + matches_.end(), &AutocompleteMatch::MoreRelevant); + if (matches_.size() > max_total_matches) + matches_.resize(max_total_matches); + + // We're done when both asynchronous subcomponents have finished. + // We can't use CancelableRequestConsumer.HasPendingRequests() for + // history and star requests here. A pending request is not cleared + // until after the completion callback has returned, but we've + // reached here from inside that callback. HasPendingRequests() + // would therefore return true, and if this is the last thing left + // to calculate for this query, we'll never mark the query "done". + done_ = !history_request_pending_ && + !suggest_results_pending_ && + !star_requests_pending_; +} + +int SearchProvider::CalculateRelevanceForWhatYouTyped() const { + switch (input_.type()) { + case AutocompleteInput::UNKNOWN: + return 1300; + + case AutocompleteInput::REQUESTED_URL: + return 1200; + + case AutocompleteInput::URL: + return 850; + + case AutocompleteInput::QUERY: + return 1300; + + case AutocompleteInput::FORCED_QUERY: + return 1500; + + default: + NOTREACHED(); + return 0; + } +} + +int SearchProvider::CalculateRelevanceForHistory(const Time& time) const { + // The relevance of past searches falls off over time. This curve is chosen + // so that the relevance of a search 15 minutes ago is discounted about 50 + // points, while the relevance of a search two weeks ago is discounted about + // 450 points. + const double elapsed_time = std::max((Time::Now() - time).InSecondsF(), 0.); + const int score_discount = static_cast<int>(6.5 * pow(elapsed_time, 0.3)); + + // Don't let scores go below 0. Negative relevance scores are meaningful in a + // different way. + int base_score; + switch (input_.type()) { + case AutocompleteInput::UNKNOWN: + case AutocompleteInput::REQUESTED_URL: + base_score = 1050; + break; + + case AutocompleteInput::URL: + base_score = 750; + break; + + case AutocompleteInput::QUERY: + case AutocompleteInput::FORCED_QUERY: + base_score = 1250; + break; + + default: + NOTREACHED(); + base_score = 0; + break; + } + return std::max(0, base_score - score_discount); +} + +int SearchProvider::CalculateRelevanceForSuggestion( + size_t suggestion_number) const { + DCHECK(suggestion_number < suggest_results_.size()); + const int suggestion_value = + static_cast<int>(suggest_results_.size() - 1 - suggestion_number); + switch (input_.type()) { + case AutocompleteInput::UNKNOWN: + case AutocompleteInput::REQUESTED_URL: + return 600 + suggestion_value; + + case AutocompleteInput::URL: + return 300 + suggestion_value; + + case AutocompleteInput::QUERY: + case AutocompleteInput::FORCED_QUERY: + return 800 + suggestion_value; + + default: + NOTREACHED(); + return 0; + } +} + +int SearchProvider::CalculateRelevanceForNavigation( + size_t suggestion_number) const { + DCHECK(suggestion_number < navigation_results_.size()); + // TODO(kochi): http://b/784900 Use relevance score from the NavSuggest + // server if possible. + switch (input_.type()) { + case AutocompleteInput::QUERY: + case AutocompleteInput::FORCED_QUERY: + return 1000 + static_cast<int>(suggestion_number); + + default: + return 800 + static_cast<int>(suggestion_number); + } +} + +void SearchProvider::AddMatchToMap(const std::wstring& query_string, + int relevance, + int accepted_suggestion, + MatchMap* map) { + AutocompleteMatch match(this, relevance, false); + match.type = AutocompleteMatch::SEARCH; + std::vector<size_t> content_param_offsets; + match.contents.assign(l10n_util::GetStringF(IDS_AUTOCOMPLETE_SEARCH_CONTENTS, + default_provider_.short_name(), + query_string, + &content_param_offsets)); + if (content_param_offsets.size() == 2) { + AutocompleteMatch::ClassifyLocationInString(content_param_offsets[1], + query_string.length(), + match.contents.length(), + ACMatchClassification::NONE, + &match.contents_class); + } else { + // |content_param_offsets| should only not be 2 if: + // (a) A translator screws up + // (b) The strings have been changed and we haven't been rebuilt properly + // (c) Some sort of crazy installer error/DLL version mismatch problem that + // gets the wrong data out of the locale DLL? + // While none of these are supposed to happen, we've seen this get hit in + // the wild, so avoid the vector access in the conditional arm above, which + // will crash. + NOTREACHED(); + } + + // When the user forced a query, we need to make sure all the fill_into_edit + // values preserve that property. Otherwise, if the user starts editing a + // suggestion, non-Search results will suddenly appear. + size_t search_start = 0; + if (input_.type() == AutocompleteInput::FORCED_QUERY) { + match.fill_into_edit.assign(L"?"); + ++search_start; + } + match.fill_into_edit.append(query_string); + // NOTE: All Google suggestions currently start with the original input, but + // not all Yahoo! suggestions do. + if (!input_.prevent_inline_autocomplete() && + !match.fill_into_edit.compare(search_start, input_.text().length(), + input_.text())) + match.inline_autocomplete_offset = search_start + input_.text().length(); + + const TemplateURLRef* const search_url = default_provider_.url(); + DCHECK(search_url->SupportsReplacement()); + match.destination_url = search_url->ReplaceSearchTerms(default_provider_, + query_string, + accepted_suggestion, + input_.text()); + + // Search results don't look like URLs. + match.transition = PageTransition::GENERATED; + + // Try to add |match| to |map|. If a match for |query_string| is already in + // |map|, replace it if |match| is more relevant. + // NOTE: Keep this ToLower() call in sync with url_database.cc. + const std::pair<MatchMap::iterator, bool> i = map->insert( + std::pair<std::wstring, AutocompleteMatch>( + l10n_util::ToLower(query_string), match)); + // NOTE: We purposefully do a direct relevance comparison here instead of + // using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items added + // first" rather than "items alphabetically first" when the scores are equal. + // The only case this matters is when a user has results with the same score + // that differ only by capitalization; because the history system returns + // results sorted by recency, this means we'll pick the most recent such + // result even if the precision of our relevance score is too low to + // distinguish the two. + if (!i.second && (match.relevance > i.first->second.relevance)) + i.first->second = match; +} + +AutocompleteMatch SearchProvider::NavigationToMatch( + const NavigationResult& navigation, + int relevance, + bool starred) { + AutocompleteMatch match(this, relevance, false); + match.destination_url = navigation.url; + match.contents = StringForURLDisplay(GURL(navigation.url), true); + // TODO(kochi): Consider moving HistoryURLProvider::TrimHttpPrefix() to some + // public utility function. + if (!url_util::FindAndCompareScheme(input_.text(), "http", NULL)) + TrimHttpPrefix(&match.contents); + AutocompleteMatch::ClassifyMatchInString(input_.text(), match.contents, + ACMatchClassification::URL, + &match.contents_class); + + match.description = navigation.site_name; + AutocompleteMatch::ClassifyMatchInString(input_.text(), navigation.site_name, + ACMatchClassification::NONE, + &match.description_class); + + match.starred = starred; + // When the user forced a query, we need to make sure all the fill_into_edit + // values preserve that property. Otherwise, if the user starts editing a + // suggestion, non-Search results will suddenly appear. + if (input_.type() == AutocompleteInput::FORCED_QUERY) + match.fill_into_edit.assign(L"?"); + match.fill_into_edit.append(match.contents); + // TODO(pkasting): http://b/1112879 These should perhaps be + // inline-autocompletable? + + return match; +} + +// TODO(kochi): This is duplicate from HistoryURLProvider. +// static +size_t SearchProvider::TrimHttpPrefix(std::wstring* url) { + url_parse::Component scheme; + if (!url_util::FindAndCompareScheme(*url, "http", &scheme)) + return 0; // Not "http". + + // Erase scheme plus up to two slashes. + size_t prefix_len = scheme.end() + 1; // "http:" + const size_t after_slashes = std::min(url->length(), + static_cast<size_t>(scheme.end() + 3)); + while ((prefix_len < after_slashes) && ((*url)[prefix_len] == L'/')) + ++prefix_len; + if (prefix_len == url->length()) + url->clear(); + else + url->erase(url->begin(), url->begin() + prefix_len); + return prefix_len; +} diff --git a/chrome/browser/autocomplete/search_provider.h b/chrome/browser/autocomplete/search_provider.h new file mode 100644 index 0000000..ab4b2ba --- /dev/null +++ b/chrome/browser/autocomplete/search_provider.h @@ -0,0 +1,244 @@ +// Copyright 2008, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// This file contains the Search autocomplete provider. This provider is +// responsible for all non-keyword autocomplete entries that start with +// "Search <engine> for ...", including searching for the current input string, +// search history, and search suggestions. An instance of it gets created and +// managed by the autocomplete controller. +// +// For more information on the autocomplete system in general, including how +// the autocomplete controller and autocomplete providers work, see +// chrome/browser/autocomplete.h. + +#ifndef CHROME_BROWSER_AUTOCOMPLETE_SEARCH_PROVIDER_H__ +#define CHROME_BROWSER_AUTOCOMPLETE_SEARCH_PROVIDER_H__ + +#include "chrome/browser/autocomplete/autocomplete.h" +#include "chrome/browser/history/history.h" +#include "chrome/browser/template_url.h" +#include "chrome/browser/url_fetcher.h" + +class Profile; +class Value; + +// Autocomplete provider for searches and suggestions from a search engine. +// +// After construction, the autocomplete controller repeatedly calls Start() +// with some user input, each time expecting to receive a small set of the best +// matches (either synchronously or asynchronously). +// +// Initially the provider creates a match that searches for the current input +// text. It also starts a task to query the Suggest servers. When that data +// comes back, the provider creates and returns matches for the best +// suggestions. +// +// TODO(pkasting): http://b/893701 This should eventually remember the user's +// search history and use that to create/rank suggestions as well. +class SearchProvider : public AutocompleteProvider, + public URLFetcher::Delegate, + public Task { + public: + SearchProvider(ACProviderListener* listener, Profile* profile) + : AutocompleteProvider(listener, profile, "Search"), + last_default_provider_(NULL), +#pragma warning(suppress: 4355) // Okay to pass "this" here. + timer_(new Timer(kQueryDelayMs, this, false)), + fetcher_(NULL), + star_requests_pending_(false), + history_request_pending_(false), + have_history_results_(false), + suggest_results_pending_(false), + have_suggest_results_(false) { + } + + // AutocompleteProvider + virtual void Start(const AutocompleteInput& input, + bool minimal_changes, + bool synchronous_only); + virtual void Stop(); + + // URLFetcher::Delegate + virtual void OnURLFetchComplete(const URLFetcher* source, + const GURL& url, + const URLRequestStatus& status, + int response_code, + const ResponseCookies& cookies, + const std::string& data); + + // Task + void Run(); + + private: + struct NavigationResult { + NavigationResult(const std::wstring& url, const std::wstring& site_name) + : url(url), + site_name(site_name), + star_request_handle(0), + starred(false) { + } + + // The URL. + std::wstring url; + + // Name for the site. + std::wstring site_name; + + // If non-zero, there is a pending request to the history service to + // obtain the starred state. + HistoryService::Handle star_request_handle; + + // Whether the URL has been starred. + bool starred; + }; + + typedef std::vector<std::wstring> SuggestResults; + typedef std::vector<NavigationResult> NavigationResults; + typedef std::vector<history::KeywordSearchTermVisit> HistoryResults; + typedef std::map<std::wstring, AutocompleteMatch> MatchMap; + + // Determines whether an asynchronous subcomponent query should run for the + // current input. If so, starts it if necessary; otherwise stops it. + // NOTE: These functions do not update |done_|. Callers must do so. + void StartOrStopHistoryQuery(bool minimal_changes, bool synchronous_only); + void StartOrStopSuggestQuery(bool minimal_changes, bool synchronous_only); + + // Functions to stop the separate asynchronous subcomponents. + // NOTE: These functions do not update |done_|. Callers must do so. + void StopHistory(); + void StopSuggest(); + + // Called back by the history system to return searches that begin with the + // input text. + void OnGotMostRecentKeywordSearchTerms( + CancelableRequestProvider::Handle handle, + HistoryResults* results); + + // Notification from the history service that the star state for the URL + // is available. If this is the last url's star state that is being requested + // the listener is notified. + void OnQueryURLComplete(HistoryService::Handle handle, + bool success, + const history::URLRow* url_row, + history::VisitVector* unused); + + // Parses the results from the Suggest server and stores up to kMaxMatches of + // them in server_results_. Returns whether parsing succeeded. + bool ParseSuggestResults(Value* root_val); + + // Converts the parsed server results in server_results_ to a set of + // AutocompleteMatches and adds them to |matches_|. This also sets |done_| + // correctly. + void ConvertResultsToAutocompleteMatches(); + + // Determines the relevance for a particular match. We use different scoring + // algorithms for the different types of matches. + int CalculateRelevanceForWhatYouTyped() const; + // |time| is the time at which this query was last seen. + int CalculateRelevanceForHistory(const Time& time) const; + // |suggestion_value| is which suggestion this is in the list returned from + // the server; the best suggestion is suggestion number 0. + int CalculateRelevanceForSuggestion(size_t suggestion_value) const; + // |suggestion_value| is same as above. + int CalculateRelevanceForNavigation(size_t suggestion_value) const; + + // Creates an AutocompleteMatch for "Search <engine> for |query_string|" with + // the supplied relevance. Adds this match to |map|; if such a match already + // exists, whichever one has lower relevance is eliminated. + void AddMatchToMap(const std::wstring& query_string, + int relevance, + int accepted_suggestion, + MatchMap* map); + // Returns an AutocompleteMatch for a navigational suggestion. + AutocompleteMatch NavigationToMatch(const NavigationResult& query_string, + int relevance, + bool starred); + + // Trims "http:" and up to two subsequent slashes from |url|. Returns the + // number of characters that were trimmed. + // TODO(kochi): this is duplicate from history_autocomplete + static size_t TrimHttpPrefix(std::wstring* url); + + // Don't send any queries to the server until some time has elapsed after + // the last keypress, to avoid flooding the server with requests we are + // likely to end up throwing away anyway. + static const int kQueryDelayMs; + + // The user's input. + AutocompleteInput input_; + + TemplateURL default_provider_; // Cached across the life of a query so we + // behave consistently even if the user + // changes their default while the query is + // running. + const TemplateURL* last_default_provider_; + // TODO(pkasting): http://b/1162970 We + // shouldn't need this. + + // An object we can use to cancel history and star requests. + CancelableRequestConsumer history_request_consumer_; + CancelableRequestConsumerT<int, 0> star_request_consumer_; + + // Whether we are waiting for star requests to finish. + bool star_requests_pending_; + + // Searches in the user's history that begin with the input text. + HistoryResults history_results_; + + // Whether history_results_ is valid (so we can tell invalid apart from + // empty). + bool have_history_results_; + + // Whether we are waiting for a history request to finish. + bool history_request_pending_; + + // True if we're expecting suggest results that haven't yet arrived. This + // could be because either |timer_| or |fetcher| is still running (see below). + bool suggest_results_pending_; + + // A timer to start a query to the suggest server after the user has stopped + // typing for long enough. + scoped_ptr<Timer> timer_; + + // The fetcher that retrieves suggest results from the server. + scoped_ptr<URLFetcher> fetcher_; + + // Suggestions returned by the Suggest server for the input text. + SuggestResults suggest_results_; + + // Navigational suggestions returned by the server. + NavigationResults navigation_results_; + + // Whether suggest_results_ is valid. + bool have_suggest_results_; + + DISALLOW_EVIL_CONSTRUCTORS(SearchProvider); +}; + +#endif // CHROME_BROWSER_AUTOCOMPLETE_SEARCH_PROVIDER_H__ |