diff options
author | mpcomplete@chromium.org <mpcomplete@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-05-28 17:45:33 +0000 |
---|---|---|
committer | mpcomplete@chromium.org <mpcomplete@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-05-28 17:45:33 +0000 |
commit | 56ad3797dc8ea7d757dc2b12d606c14cedd564e1 (patch) | |
tree | 26fab7c6d4cbb0358b404ca3eb488aae75ebe1d3 /chrome | |
parent | 79b663c6e6ddf89e85cdc566b5d4f368465bb858 (diff) | |
download | chromium_src-56ad3797dc8ea7d757dc2b12d606c14cedd564e1.zip chromium_src-56ad3797dc8ea7d757dc2b12d606c14cedd564e1.tar.gz chromium_src-56ad3797dc8ea7d757dc2b12d606c14cedd564e1.tar.bz2 |
First pass at experimental omnibox API. There are plenty of rough edges and features to work on, but it's in a usable state.
When an extension is installed that specifies an omnibox keyword in its manifest, we add that keyword to the user's list of Search Engines. The user can then edit this keyword later.
I'm leveraging most of the original search engine keyword code. An extension keyword has a special URL that identifies it as an extension keyword. There is some special case code to treat these keywords slightly differently throughout
the omnibox code.
BUG=38884
Review URL: http://codereview.chromium.org/2078021
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@48503 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome')
40 files changed, 801 insertions, 99 deletions
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd index c23b9a3..9122834 100644 --- a/chrome/app/generated_resources.grd +++ b/chrome/app/generated_resources.grd @@ -1046,6 +1046,9 @@ each locale. --> <message name="IDS_KEYWORD_SEARCH" desc="The description for a chrome keyword search match in the Omnibox dropdown"> Search <ph name="SITE_NAME">$1<ex>www.google.com</ex></ph> for <ph name="SEARCH_TERMS">$2<ex>flowers</ex></ph> </message> + <message name="IDS_EXTENSION_KEYWORD_COMMAND" desc="The description for an extension keyword command match in the Omnibox dropdown"> + Run <ph name="EXTENSION_NAME">$1<ex>Google Talk</ex></ph> command <ph name="SEARCH_TERMS">$2<ex>mpcomplete@chromium.org</ex></ph> + </message> <message name="IDS_EMPTY_KEYWORD_VALUE" desc="Shown in the location bar drop down when the user enters a string that matches a chrome keyword, but they haven't entered any text following the chrome keyword"> <enter query> @@ -1053,6 +1056,12 @@ each locale. --> <message name="IDS_AUTOCOMPLETE_KEYWORD_DESCRIPTION" desc="Description for a keyword match."> (Keyword: <ph name="KEYWORD">$1</ph>) </message> + <message name="IDS_AUTOCOMPLETE_EXTENSION_KEYWORD_DESCRIPTION" desc="Description for an extension keyword match."> + (Keyword: <ph name="KEYWORD">$1</ph>) + </message> + <message name="IDS_AUTOCOMPLETE_EXTENSION_KEYWORD_CONTENT" desc="Extra text describing usage of the suggested extension keyword command."> + <ph name="EXTENSION_COMMAND">$1<ex>send-mail-to</ex></ph> <ph name="EXTENSION_DESCRIPTION">$2<ex>(username@example.com)</ex></ph> + </message> <if expr="not pp_ifdef('use_titlecase')"> <message name="IDS_EDIT_SEARCH_ENGINES" desc="Title of the popup menu item for editing search engines"> @@ -3773,12 +3782,18 @@ Keep your key file in a safe place. You will need it to create new versions of y <message name="IDS_OMNIBOX_KEYWORD_HINT" desc="Shown to the user when the url in the omnibox has a keyword associated with it. $1 is replaced with an image showing the tab key and is labelled with IDS_OMNIBOX_KEYWORD_HINT_KEY. $2 is replaced with the description of the keyword."> Press <ph name="SEARCH_KEY">$1<ex>Tab</ex></ph> to search <ph name="SITE_NAME">$2<ex>google.com</ex></ph> </message> + <message name="IDS_OMNIBOX_EXTENSION_KEYWORD_HINT" desc="Shown to the user when the word(s) in the omnibox has an extension keyword associated with it. $1 is replaced with an image showing the tab key and is labelled with IDS_OMNIBOX_KEYWORD_HINT_KEY. $2 is replaced with the description of the keyword."> + Press <ph name="SEARCH_KEY">$1<ex>Tab</ex></ph> to send commands to <ph name="EXTENSION_NAME">$2<ex>Google Talk</ex></ph> + </message> <message name="IDS_OMNIBOX_KEYWORD_HINT_KEY"> Tab </message> <message name="IDS_OMNIBOX_KEYWORD_TEXT" desc="Text shown in the search button at the front of the omnibox when the user has selected a keyword"> Search <ph name="SITE_NAME">$1<ex>google.com</ex></ph>: </message> + <message name="IDS_OMNIBOX_EXTENSION_KEYWORD_TEXT" desc="Text shown in the search button at the front of the omnibox when the user has selected an extension keyword"> + <ph name="EXTENSION_NAME">$1<ex>Google Talk</ex></ph> command: + </message> <!-- TODO(tc): This doesn't handle singular/plural properly. It needs to be reworded. --> <message name="IDS_OMNIBOX_RECENT_HISTORY" desc="Text shown in the omnibox that shows how many recent matches there are and allows the user to navigate to destinations->history with the selected text."> See <ph name="NUM_MATCHES">$1<ex>2,123</ex></ph> recent pages in history containing <ph name="SEARCH_TERMS">$2<ex>flowers</ex></ph> diff --git a/chrome/browser/autocomplete/autocomplete_edit.cc b/chrome/browser/autocomplete/autocomplete_edit.cc index a650e1d..de74891 100644 --- a/chrome/browser/autocomplete/autocomplete_edit.cc +++ b/chrome/browser/autocomplete/autocomplete_edit.cc @@ -15,6 +15,7 @@ #include "chrome/browser/autocomplete/autocomplete_popup_model.h" #include "chrome/browser/autocomplete/keyword_provider.h" #include "chrome/browser/command_updater.h" +#include "chrome/browser/extensions/extension_omnibox_api.h" #include "chrome/browser/metrics/user_metrics.h" #include "chrome/browser/net/dns_global.h" #include "chrome/browser/net/url_fixer_upper.h" @@ -265,6 +266,17 @@ void AutocompleteEditModel::AcceptInput(WindowOpenDisposition disposition, AutocompleteMatch match; GURL alternate_nav_url; GetInfoForCurrentText(&match, &alternate_nav_url); + + if (match.template_url && match.template_url->IsExtensionKeyword()) { + // Strip the keyword + leading space off the input. + size_t prefix_length = match.template_url->keyword().size() + 1; + ExtensionOmniboxEventRouter::OnInputEntered( + profile_, match.template_url->GetExtensionId(), + WideToUTF8(match.fill_into_edit.substr(prefix_length))); + view_->RevertAll(); + return; + } + if (!match.destination_url.is_valid()) return; @@ -606,9 +618,13 @@ void AutocompleteEditModel::Observe(NotificationType type, inline_autocomplete_text = match->fill_into_edit.substr(match->inline_autocomplete_offset); } - // Warm up DNS Prefetch Cache. - chrome_browser_net::DnsPrefetchUrl(match->destination_url, - IsPreconnectable(match->type)); + + if (!match->destination_url.SchemeIs(chrome::kExtensionScheme)) { + // Warm up DNS Prefetch Cache. + chrome_browser_net::DnsPrefetchUrl(match->destination_url, + IsPreconnectable(match->type)); + } + // 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. diff --git a/chrome/browser/autocomplete/keyword_provider.cc b/chrome/browser/autocomplete/keyword_provider.cc index 9f841cc..a0130b7 100644 --- a/chrome/browser/autocomplete/keyword_provider.cc +++ b/chrome/browser/autocomplete/keyword_provider.cc @@ -8,10 +8,13 @@ #include <vector> #include "app/l10n_util.h" +#include "base/string16.h" #include "base/utf_string_conversions.h" +#include "chrome/browser/extensions/extension_omnibox_api.h" #include "chrome/browser/profile.h" #include "chrome/browser/search_engines/template_url.h" #include "chrome/browser/search_engines/template_url_model.h" +#include "chrome/common/notification_service.h" #include "grit/generated_resources.h" #include "net/base/escape.h" #include "net/base/net_util.h" @@ -31,13 +34,17 @@ std::wstring KeywordProvider::SplitReplacementStringFromInput( KeywordProvider::KeywordProvider(ACProviderListener* listener, Profile* profile) : AutocompleteProvider(listener, profile, "Keyword"), - model_(NULL) { + model_(NULL), + current_input_id_(0) { + registrar_.Add(this, NotificationType::EXTENSION_OMNIBOX_SUGGESTIONS_READY, + Source<Profile>(profile)); } KeywordProvider::KeywordProvider(ACProviderListener* listener, TemplateURLModel* model) : AutocompleteProvider(listener, NULL, "Keyword"), - model_(model) { + model_(model), + current_input_id_(0) { } @@ -84,6 +91,14 @@ void KeywordProvider::Start(const AutocompleteInput& input, bool minimal_changes) { matches_.clear(); + if (!minimal_changes) { + done_ = true; + + // Input has changed. Increment the input ID so that we can discard any + // stale extension suggestions that may be incoming. + ++current_input_id_; + } + // Split user input into a keyword and some query input. // // We want to suggest keywords even when users have started typing URLs, on @@ -131,7 +146,33 @@ void KeywordProvider::Start(const AutocompleteInput& input, if (keyword_matches.front() == keyword) { matches_.push_back(CreateAutocompleteMatch(model, keyword, input, keyword.length(), - remaining_input)); + remaining_input, -1)); + + const TemplateURL* template_url(model->GetTemplateURLForKeyword(keyword)); + if (profile_ && + !input.synchronous_only() && template_url->IsExtensionKeyword()) { + if (minimal_changes) { + // If the input hasn't significantly changed, we can just use the + // suggestions from last time. We need to readjust the relevance to + // ensure it is less than the main match's relevance. + for (size_t i = 0; i < extension_suggest_matches_.size(); ++i) { + matches_.push_back(extension_suggest_matches_[i]); + matches_.back().relevance = matches_[0].relevance - (i + 1); + } + } else { + extension_suggest_last_input_ = input; + extension_suggest_matches_.clear(); + + bool have_listeners = ExtensionOmniboxEventRouter::OnInputChanged( + profile_, template_url->GetExtensionId(), + WideToUTF8(remaining_input), current_input_id_); + + // We only have to wait for suggest results if there are actually + // extensions listening for input changes. + if (have_listeners) + done_ = false; + } + } } else { if (keyword_matches.size() > kMaxMatches) { keyword_matches.erase(keyword_matches.begin() + kMaxMatches, @@ -141,7 +182,7 @@ void KeywordProvider::Start(const AutocompleteInput& input, i != keyword_matches.end(); ++i) { matches_.push_back(CreateAutocompleteMatch(model, *i, input, keyword.length(), - remaining_input)); + remaining_input, -1)); } } } @@ -189,10 +230,12 @@ void KeywordProvider::FillInURLAndContents( DCHECK(!element->short_name().empty()); DCHECK(element->url()); DCHECK(element->url()->IsValid()); + int message_id = element->IsExtensionKeyword() ? + IDS_EXTENSION_KEYWORD_COMMAND : IDS_KEYWORD_SEARCH; 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, + match->contents.assign(l10n_util::GetStringF(message_id, element->AdjustedShortNameForLocaleDirection(), l10n_util::GetString(IDS_EMPTY_KEYWORD_VALUE))); match->contents_class.push_back( @@ -215,7 +258,7 @@ void KeywordProvider::FillInURLAndContents( *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, + match->contents.assign(l10n_util::GetStringF(message_id, element->short_name(), remaining_input, &content_param_offsets)); @@ -246,7 +289,8 @@ AutocompleteMatch KeywordProvider::CreateAutocompleteMatch( const std::wstring keyword, const AutocompleteInput& input, size_t prefix_length, - const std::wstring& remaining_input) { + const std::wstring& remaining_input, + int relevance) { DCHECK(model); // Get keyword data from data store. const TemplateURL* element(model->GetTemplateURLForKeyword(keyword)); @@ -257,14 +301,17 @@ AutocompleteMatch KeywordProvider::CreateAutocompleteMatch( // 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, - // When the user wants keyword matches to take - // preference, score them highly regardless of whether - // the input provides query text. - input.prefer_keyword() || !supports_replacement), - false, supports_replacement ? AutocompleteMatch::SEARCH_OTHER_ENGINE : - AutocompleteMatch::HISTORY_KEYWORD); + if (relevance < 0) { + relevance = + CalculateRelevance(input.type(), keyword_complete, + // When the user wants keyword matches to take + // preference, score them highly regardless of + // whether the input provides query text. + input.prefer_keyword() || !supports_replacement); + } + AutocompleteMatch result(this, relevance, false, + supports_replacement ? AutocompleteMatch::SEARCH_OTHER_ENGINE : + AutocompleteMatch::HISTORY_KEYWORD); result.fill_into_edit.assign(keyword); if (!remaining_input.empty() || !keyword_complete || supports_replacement) result.fill_into_edit.push_back(L' '); @@ -278,12 +325,13 @@ AutocompleteMatch KeywordProvider::CreateAutocompleteMatch( 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)); + int message_id = element->IsExtensionKeyword() ? + IDS_AUTOCOMPLETE_EXTENSION_KEYWORD_DESCRIPTION : + IDS_AUTOCOMPLETE_KEYWORD_DESCRIPTION; + result.description.assign(l10n_util::GetStringF(message_id, keyword)); if (supports_replacement) result.template_url = element; - static const std::wstring kKeywordDesc(l10n_util::GetString( - IDS_AUTOCOMPLETE_KEYWORD_DESCRIPTION)); + static const std::wstring kKeywordDesc(l10n_util::GetString(message_id)); AutocompleteMatch::ClassifyLocationInString(kKeywordDesc.find(L"%s"), prefix_length, result.description.length(), @@ -294,3 +342,64 @@ AutocompleteMatch KeywordProvider::CreateAutocompleteMatch( return result; } + +void KeywordProvider::Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details) { + // TODO(mpcomplete): consider clamping the number of suggestions to + // AutocompleteProvider::kMaxMatches. + DCHECK(type == NotificationType::EXTENSION_OMNIBOX_SUGGESTIONS_READY); + + int suggest_id = Details<ExtensionOmniboxSuggestions>(details).ptr()->first; + if (suggest_id != current_input_id_) + return; // This is an old result. Just ignore. + + const AutocompleteInput& input = extension_suggest_last_input_; + std::wstring keyword, remaining_input; + if (!ExtractKeywordFromInput(input, &keyword, &remaining_input)) { + NOTREACHED(); + return; + } + + TemplateURLModel* model = + profile_ ? profile_->GetTemplateURLModel() : model_; + + ListValue* suggestions = + Details<ExtensionOmniboxSuggestions>(details).ptr()->second; + for (size_t i = 0; i < suggestions->GetSize(); ++i) { + DictionaryValue* suggestion; + string16 content, description; + if (!suggestions->GetDictionary(i, &suggestion) || + !suggestion->GetString("content", &content) || + !suggestion->GetString("description", &description)) + break; + + // We want to order these suggestions in descending order, so start with + // the relevance of the first result (added synchronously in Start()), + // and subtract 1 for each subsequent suggestion from the extension. + // We know that |complete| is true, because we wouldn't get results from + // the extension unless the full keyword had been typed. + int first_relevance = + CalculateRelevance(input.type(), true, input.prefer_keyword()); + extension_suggest_matches_.push_back(CreateAutocompleteMatch( + model, keyword, input, keyword.length(), UTF16ToWide(content), + first_relevance - (i + 1))); + + if (!description.empty()) { + AutocompleteMatch* match = &extension_suggest_matches_.back(); + std::vector<size_t> offsets; + match->contents.assign(l10n_util::GetStringF( + IDS_AUTOCOMPLETE_EXTENSION_KEYWORD_CONTENT, + match->contents, UTF16ToWide(description), &offsets)); + CHECK_EQ(2U, offsets.size()) << + "Expected 2 params for IDS_AUTOCOMPLETE_EXTENSION_KEYWORD_CONTENT"; + match->contents_class.push_back( + ACMatchClassification(offsets[1], ACMatchClassification::NONE)); + } + } + + done_ = true; + matches_.insert(matches_.end(), extension_suggest_matches_.begin(), + extension_suggest_matches_.end()); + listener_->OnProviderUpdate(!extension_suggest_matches_.empty()); +} diff --git a/chrome/browser/autocomplete/keyword_provider.h b/chrome/browser/autocomplete/keyword_provider.h index adf81d8..6fb744b 100644 --- a/chrome/browser/autocomplete/keyword_provider.h +++ b/chrome/browser/autocomplete/keyword_provider.h @@ -1,4 +1,4 @@ -// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Copyright (c) 2010 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // @@ -18,6 +18,7 @@ #include <string> #include "chrome/browser/autocomplete/autocomplete.h" +#include "chrome/common/notification_registrar.h" class Profile; class TemplateURL; @@ -45,7 +46,9 @@ class TemplateURLModel; // 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 { +class KeywordProvider : + public AutocompleteProvider, + public NotificationObserver { public: KeywordProvider(ACProviderListener* listener, Profile* profile); // For testing. @@ -103,17 +106,39 @@ class KeywordProvider : public AutocompleteProvider { bool no_query_text_needed); // Creates a fully marked-up AutocompleteMatch from the user's input. + // If |relevance| is negative, calculate a relevance based on heuristics. AutocompleteMatch CreateAutocompleteMatch( TemplateURLModel* model, const std::wstring keyword, const AutocompleteInput& input, size_t prefix_length, - const std::wstring& remaining_input); + const std::wstring& remaining_input, + int relevance); + + // NotificationObserver interface. + void Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details); // Model for the keywords. This is only non-null when testing, otherwise the // TemplateURLModel from the Profile is used. TemplateURLModel* model_; + // Identifies the current input state. This is incremented each time the + // autocomplete edit's input changes in any way. It is used to tell whether + // suggest results from the extension are current. + int current_input_id_; + + // The input state at the time we last asked the extension for suggest + // results. + AutocompleteInput extension_suggest_last_input_; + + // We remember the last suggestions we've received from the extension in case + // we need to reset our matches without asking the extension again. + std::vector<AutocompleteMatch> extension_suggest_matches_; + + NotificationRegistrar registrar_; + DISALLOW_EVIL_CONSTRUCTORS(KeywordProvider); }; diff --git a/chrome/browser/cocoa/location_bar_view_mac.h b/chrome/browser/cocoa/location_bar_view_mac.h index 3f13bbd..2d8d57e 100644 --- a/chrome/browser/cocoa/location_bar_view_mac.h +++ b/chrome/browser/cocoa/location_bar_view_mac.h @@ -134,6 +134,7 @@ class LocationBarViewMac : public AutocompleteEditController, const std::wstring& keyword, const std::wstring& short_name, const bool is_keyword_hint, + const bool is_extension_keyword, NSImage* image); // Overridden from NotificationObserver. diff --git a/chrome/browser/cocoa/location_bar_view_mac.mm b/chrome/browser/cocoa/location_bar_view_mac.mm index 5bca3f1..f339d146 100644 --- a/chrome/browser/cocoa/location_bar_view_mac.mm +++ b/chrome/browser/cocoa/location_bar_view_mac.mm @@ -255,6 +255,7 @@ void LocationBarViewMac::OnChangedImpl(AutocompleteTextField* field, const std::wstring& keyword, const std::wstring& short_name, const bool is_keyword_hint, + const bool is_extension_keyword, NSImage* image) { AutocompleteTextFieldCell* cell = [field autocompleteTextFieldCell]; const CGFloat availableWidth([field availableDecorationWidth]); @@ -266,15 +267,15 @@ void LocationBarViewMac::OnChangedImpl(AutocompleteTextField* field, const std::wstring min_name(CalculateMinString(short_name)); NSString* partial_string = nil; + int message_id = is_extension_keyword ? + IDS_OMNIBOX_EXTENSION_KEYWORD_TEXT : IDS_OMNIBOX_KEYWORD_TEXT; if (!min_name.empty()) { partial_string = - l10n_util::GetNSStringF(IDS_OMNIBOX_KEYWORD_TEXT, - WideToUTF16(min_name)); + l10n_util::GetNSStringF(message_id, WideToUTF16(min_name)); } NSString* keyword_string = - l10n_util::GetNSStringF(IDS_OMNIBOX_KEYWORD_TEXT, - WideToUTF16(short_name)); + l10n_util::GetNSStringF(message_id, WideToUTF16(short_name)); [cell setKeywordString:keyword_string partialString:partial_string availableWidth:availableWidth]; @@ -283,8 +284,10 @@ void LocationBarViewMac::OnChangedImpl(AutocompleteTextField* field, // is a parameter to be replaced by an image. "Engine" is a // parameter to be replaced by text based on the keyword. std::vector<size_t> content_param_offsets; + int message_id = is_extension_keyword ? + IDS_OMNIBOX_EXTENSION_KEYWORD_HINT : IDS_OMNIBOX_KEYWORD_HINT; const std::wstring keyword_hint( - l10n_util::GetStringF(IDS_OMNIBOX_KEYWORD_HINT, + l10n_util::GetStringF(message_id, std::wstring(), short_name, &content_param_offsets)); @@ -318,8 +321,10 @@ void LocationBarViewMac::OnChanged() { // here where we have a Profile and pass it into OnChangedImpl(). const std::wstring keyword(edit_view_->model()->keyword()); std::wstring short_name; + bool is_extension_keyword = false; if (!keyword.empty()) { - short_name = GetKeywordName(profile_, keyword); + short_name = profile_->GetTemplateURLModel()-> + GetKeywordShortName(keyword, &is_extension_keyword); } // TODO(shess): Implementation exported to a static so that it can @@ -329,6 +334,7 @@ void LocationBarViewMac::OnChanged() { keyword, short_name, edit_view_->model()->is_keyword_hint(), + is_extension_keyword, GetTabButtonImage()); } diff --git a/chrome/browser/cocoa/location_bar_view_mac_unittest.mm b/chrome/browser/cocoa/location_bar_view_mac_unittest.mm index ecb2a7b..c3b1f9a6 100644 --- a/chrome/browser/cocoa/location_bar_view_mac_unittest.mm +++ b/chrome/browser/cocoa/location_bar_view_mac_unittest.mm @@ -1,4 +1,4 @@ -// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Copyright (c) 2010 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -89,19 +89,21 @@ TEST_F(LocationBarViewMacTest, OnChangedImpl) { [NSString stringWithFormat:@"Search Go%C:", 0x2026]; // With no special hints requested, none set. - LocationBarViewMac::OnChangedImpl(field_, std::wstring(), std::wstring(), false, image); + LocationBarViewMac::OnChangedImpl(field_, std::wstring(), std::wstring(), + false, false, image); EXPECT_FALSE([cell keywordString]); EXPECT_FALSE([cell hintString]); // Request a keyword hint. - LocationBarViewMac::OnChangedImpl(field_, kKeyword, kKeyword, true, image); + LocationBarViewMac::OnChangedImpl(field_, kKeyword, kKeyword, + true, false, image); EXPECT_FALSE([cell keywordString]); EXPECT_TRUE([[[cell hintString] string] hasPrefix:kKeywordPrefix]); EXPECT_TRUE([[[cell hintString] string] hasSuffix:kKeywordSuffix]); // Request keyword-search mode. LocationBarViewMac::OnChangedImpl( - field_, kKeyword, kKeyword, false, image); + field_, kKeyword, kKeyword, false, false, image); EXPECT_TRUE([[[cell keywordString] string] hasSuffix:kKeywordString]); EXPECT_FALSE([cell hintString]); @@ -112,13 +114,14 @@ TEST_F(LocationBarViewMacTest, OnChangedImpl) { NSRect frame([field_ frame]); frame.size.width = 10.0; [field_ setFrame:frame]; - LocationBarViewMac::OnChangedImpl(field_, kKeyword, kKeyword, false, image); + LocationBarViewMac::OnChangedImpl(field_, kKeyword, kKeyword, false, + false, image); EXPECT_TRUE([[[cell keywordString] string] isEqualToString:kPartialString]); EXPECT_FALSE([cell hintString]); // Transition back to baseline. LocationBarViewMac::OnChangedImpl( - field_, std::wstring(), std::wstring(), false, image); + field_, std::wstring(), std::wstring(), false, false, image); EXPECT_FALSE([cell keywordString]); EXPECT_FALSE([cell hintString]); } diff --git a/chrome/browser/extensions/extension_function_dispatcher.cc b/chrome/browser/extensions/extension_function_dispatcher.cc index 40dbae5..31d6984 100644 --- a/chrome/browser/extensions/extension_function_dispatcher.cc +++ b/chrome/browser/extensions/extension_function_dispatcher.cc @@ -30,6 +30,7 @@ #include "chrome/browser/extensions/extension_infobar_module.h" #include "chrome/browser/extensions/extension_message_service.h" #include "chrome/browser/extensions/extension_metrics_module.h" +#include "chrome/browser/extensions/extension_omnibox_api.h" #include "chrome/browser/extensions/extension_page_actions_module.h" #include "chrome/browser/extensions/extension_page_actions_module_constants.h" #include "chrome/browser/extensions/extension_popup_api.h" @@ -226,6 +227,9 @@ void FactoryRegistry::ResetFunctions() { RegisterFunction<UpdateContextMenuFunction>(); RegisterFunction<RemoveContextMenuFunction>(); RegisterFunction<RemoveAllContextMenusFunction>(); + + // Omnibox. + RegisterFunction<OmniboxSendSuggestionsFunction>(); } void FactoryRegistry::GetAllNames(std::vector<std::string>* names) { diff --git a/chrome/browser/extensions/extension_message_service.cc b/chrome/browser/extensions/extension_message_service.cc index f3fb6a0..2c7f693 100644 --- a/chrome/browser/extensions/extension_message_service.cc +++ b/chrome/browser/extensions/extension_message_service.cc @@ -177,6 +177,12 @@ void ExtensionMessageService::RemoveEventListener(const std::string& event_name, } } +bool ExtensionMessageService::HasEventListener( + const std::string& event_name) { + return (listeners_.find(event_name) != listeners_.end() && + !listeners_[event_name].empty()); +} + void ExtensionMessageService::AllocatePortIdPair(int* port1, int* port2) { AutoLock lock(next_port_id_lock_); diff --git a/chrome/browser/extensions/extension_message_service.h b/chrome/browser/extensions/extension_message_service.h index c205bb9..bf49948 100644 --- a/chrome/browser/extensions/extension_message_service.h +++ b/chrome/browser/extensions/extension_message_service.h @@ -74,6 +74,9 @@ class ExtensionMessageService void RemoveEventListener(const std::string& event_name, int render_process_id); + // Returns true if there is at least one listener for the given event. + bool HasEventListener(const std::string& event_name); + // Closes the message channel associated with the given port, and notifies // the other side. void CloseChannel(int port_id); diff --git a/chrome/browser/extensions/extension_omnibox_api.cc b/chrome/browser/extensions/extension_omnibox_api.cc new file mode 100644 index 0000000..7b526d7 --- /dev/null +++ b/chrome/browser/extensions/extension_omnibox_api.cc @@ -0,0 +1,67 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/extensions/extension_omnibox_api.h" + +#include "base/json/json_writer.h" +#include "base/string_util.h" +#include "base/values.h" +#include "chrome/browser/extensions/extension_message_service.h" +#include "chrome/browser/profile.h" +#include "chrome/common/notification_service.h" + +namespace events { +const char kOnInputChanged[] = "experimental.omnibox.onInputChanged/"; +const char kOnInputEntered[] = "experimental.omnibox.onInputEntered/"; +}; // namespace events + +// static +bool ExtensionOmniboxEventRouter::OnInputChanged( + Profile* profile, const std::string& extension_id, + const std::string& input, int suggest_id) { + std::string event_name = events::kOnInputChanged + extension_id; + if (!profile->GetExtensionMessageService()->HasEventListener(event_name)) + return false; + + ListValue args; + args.Set(0, Value::CreateStringValue(input)); + args.Set(1, Value::CreateIntegerValue(suggest_id)); + std::string json_args; + base::JSONWriter::Write(&args, false, &json_args); + + profile->GetExtensionMessageService()->DispatchEventToRenderers( + event_name, json_args, profile->IsOffTheRecord(), GURL()); + return true; +} + +// static +void ExtensionOmniboxEventRouter::OnInputEntered( + Profile* profile, const std::string& extension_id, + const std::string& input) { + std::string event_name = events::kOnInputEntered + extension_id; + + ListValue args; + args.Set(0, Value::CreateStringValue(input)); + std::string json_args; + base::JSONWriter::Write(&args, false, &json_args); + + profile->GetExtensionMessageService()->DispatchEventToRenderers( + event_name, json_args, profile->IsOffTheRecord(), GURL()); +} + +bool OmniboxSendSuggestionsFunction::RunImpl() { + int request_id; + ListValue* suggestions_value; + EXTENSION_FUNCTION_VALIDATE(args_->GetInteger(0, &request_id)); + EXTENSION_FUNCTION_VALIDATE(args_->GetList(1, &suggestions_value)); + + ExtensionOmniboxSuggestions details(request_id, suggestions_value); + NotificationService::current()->Notify( + NotificationType::EXTENSION_OMNIBOX_SUGGESTIONS_READY, + Source<Profile>(profile_), + Details<ExtensionOmniboxSuggestions>(&details)); + + return true; +} + diff --git a/chrome/browser/extensions/extension_omnibox_api.h b/chrome/browser/extensions/extension_omnibox_api.h new file mode 100644 index 0000000..f77e904 --- /dev/null +++ b/chrome/browser/extensions/extension_omnibox_api.h @@ -0,0 +1,39 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_EXTENSIONS_EXTENSION_OMNIBOX_API_H_ +#define CHROME_BROWSER_EXTENSIONS_EXTENSION_OMNIBOX_API_H_ + +#include "chrome/browser/extensions/extension_function.h" + +class ListValue; + +// Event router class for events related to the omnibox API. +class ExtensionOmniboxEventRouter { + public: + // The user has changed what is typed into the omnibox while in an extension + // keyword session. Returns true if someone is listening to this event, and + // thus we have some degree of confidence we'll get a response. + static bool OnInputChanged( + Profile* profile, const std::string& extension_id, + const std::string& input, int suggest_id); + + // The user has accepted the omnibox input. + static void OnInputEntered( + Profile* profile, const std::string& extension_id, + const std::string& input); + + private: + DISALLOW_COPY_AND_ASSIGN(ExtensionOmniboxEventRouter); +}; + +class OmniboxSendSuggestionsFunction : public SyncExtensionFunction { + public: + virtual bool RunImpl(); + DECLARE_EXTENSION_FUNCTION_NAME("experimental.omnibox.sendSuggestions"); +}; + +typedef std::pair<int, ListValue*> ExtensionOmniboxSuggestions; + +#endif // CHROME_BROWSER_EXTENSIONS_EXTENSION_OMNIBOX_API_H_ diff --git a/chrome/browser/extensions/extension_omnibox_apitest.cc b/chrome/browser/extensions/extension_omnibox_apitest.cc new file mode 100644 index 0000000..15b3d73 --- /dev/null +++ b/chrome/browser/extensions/extension_omnibox_apitest.cc @@ -0,0 +1,134 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/autocomplete/autocomplete.h" +#include "chrome/browser/autocomplete/autocomplete_edit.h" +#include "chrome/browser/autocomplete/autocomplete_edit_view.h" +#include "chrome/browser/autocomplete/autocomplete_popup_model.h" +#include "chrome/browser/browser.h" +#include "chrome/browser/browser_window.h" +#include "chrome/browser/extensions/extension_apitest.h" +#include "chrome/browser/history/history.h" +#include "chrome/browser/location_bar.h" +#include "chrome/browser/profile.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/common/notification_type.h" +#include "chrome/common/url_constants.h" +#include "chrome/test/ui_test_utils.h" + +namespace { + +std::wstring AutocompleteResultAsString(const AutocompleteResult& result) { + std::wstring output(StringPrintf(L"{%d} ", result.size())); + for (size_t i = 0; i < result.size(); ++i) { + AutocompleteMatch match = result.match_at(i); + std::wstring provider_name(ASCIIToWide(match.provider->name())); + output.append(StringPrintf(L"[\"%ls\" by \"%ls\"] ", + match.contents.c_str(), + provider_name.c_str())); + } + return output; +} + +} // namespace + +class OmniboxApiTest : public ExtensionApiTest { + protected: + LocationBar* GetLocationBar() const { + return browser()->window()->GetLocationBar(); + } + + AutocompleteController* GetAutocompleteController() const { + return GetLocationBar()->location_entry()->model()->popup_model()-> + autocomplete_controller(); + } + + void WaitForHistoryBackendToLoad() { + HistoryService* history_service = + browser()->profile()->GetHistoryService(Profile::EXPLICIT_ACCESS); + if (!history_service->BackendLoaded()) + ui_test_utils::WaitForNotification(NotificationType::HISTORY_LOADED); + } + + void WaitForAutocompleteDone(AutocompleteController* controller) { + while (!controller->done()) { + ui_test_utils::WaitForNotification( + NotificationType::AUTOCOMPLETE_CONTROLLER_DEFAULT_MATCH_UPDATED); + } + } +}; + +IN_PROC_BROWSER_TEST_F(OmniboxApiTest, Basic) { + CommandLine::ForCurrentProcess()->AppendSwitch( + switches::kEnableExperimentalExtensionApis); + + ASSERT_TRUE(StartHTTPServer()); + ASSERT_TRUE(RunExtensionTest("omnibox")) << message_; + + // The results depend on the history backend being loaded. Make sure it is + // loaded so that the autocomplete results are consistent. + WaitForHistoryBackendToLoad(); + + LocationBar* location_bar = GetLocationBar(); + AutocompleteController* autocomplete_controller = GetAutocompleteController(); + + // Test that our extension's keyword is suggested to us when we partially type + // it. + { + autocomplete_controller->Start(L"keywor", std::wstring(), + true, false, false); + + WaitForAutocompleteDone(autocomplete_controller); + EXPECT_TRUE(autocomplete_controller->done()); + EXPECT_EQ(std::wstring(), location_bar->GetInputString()); + EXPECT_EQ(std::wstring(), location_bar->location_entry()->GetText()); + EXPECT_TRUE(location_bar->location_entry()->IsSelectAll()); + + // First result should be to search for what was typed, second should be to + // enter "extension keyword" mode. + const AutocompleteResult& result = autocomplete_controller->result(); + ASSERT_EQ(2U, result.size()) << AutocompleteResultAsString(result); + AutocompleteMatch match = result.match_at(0); + EXPECT_EQ(AutocompleteMatch::SEARCH_WHAT_YOU_TYPED, match.type); + EXPECT_FALSE(match.deletable); + + match = result.match_at(1); + ASSERT_TRUE(match.template_url); + EXPECT_TRUE(match.template_url->IsExtensionKeyword()); + EXPECT_EQ(L"keyword", match.template_url->keyword()); + } + + // Test that our extension can send suggestions back to us. + { + autocomplete_controller->Start(L"keyword suggestio", std::wstring(), + true, false, false); + + WaitForAutocompleteDone(autocomplete_controller); + EXPECT_TRUE(autocomplete_controller->done()); + + // First result should be to invoke the keyword with what we typed, 2-4 + // should be to invoke with suggestions from the extension, and the last + // should be to search for what we typed. + const AutocompleteResult& result = autocomplete_controller->result(); + ASSERT_EQ(5U, result.size()) << AutocompleteResultAsString(result); + + ASSERT_TRUE(result.match_at(0).template_url); + EXPECT_EQ(L"keyword suggestio", result.match_at(0).fill_into_edit); + EXPECT_EQ(L"keyword suggestion1", result.match_at(1).fill_into_edit); + EXPECT_EQ(L"keyword suggestion2", result.match_at(2).fill_into_edit); + EXPECT_EQ(L"keyword suggestion3", result.match_at(3).fill_into_edit); + + AutocompleteMatch match = result.match_at(4); + EXPECT_EQ(AutocompleteMatch::SEARCH_WHAT_YOU_TYPED, match.type); + EXPECT_FALSE(match.deletable); + } + + { + ResultCatcher catcher; + autocomplete_controller->Start(L"keyword command", std::wstring(), + true, false, false); + location_bar->AcceptInput(); + EXPECT_TRUE(catcher.GetNextResult()) << catcher.message(); + } +} diff --git a/chrome/browser/extensions/extensions_service.cc b/chrome/browser/extensions/extensions_service.cc index 849243f..ed6e6d3 100644 --- a/chrome/browser/extensions/extensions_service.cc +++ b/chrome/browser/extensions/extensions_service.cc @@ -29,9 +29,10 @@ #include "chrome/browser/extensions/extension_updater.h" #include "chrome/browser/extensions/external_extension_provider.h" #include "chrome/browser/extensions/external_pref_extension_provider.h" +#include "chrome/browser/net/chrome_url_request_context.h" #include "chrome/browser/pref_service.h" #include "chrome/browser/profile.h" -#include "chrome/browser/net/chrome_url_request_context.h" +#include "chrome/browser/search_engines/template_url_model.h" #include "chrome/common/child_process_logging.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/extensions/extension.h" @@ -324,6 +325,9 @@ void ExtensionsService::UninstallExtension(const std::string& extension_id, // obtained via Extension::id(). std::string extension_id_copy(extension_id); + if (profile_->GetTemplateURLModel()) + profile_->GetTemplateURLModel()->UnregisterExtensionKeyword(extension); + // Unload before doing more cleanup to ensure that nothing is hanging on to // any of these resources. UnloadExtension(extension_id); @@ -763,7 +767,7 @@ void ExtensionsService::OnLoadedInstalledExtensions() { NotificationService::NoDetails()); } -void ExtensionsService::OnExtensionLoaded(Extension* extension, +bool ExtensionsService::OnExtensionLoaded(Extension* extension, bool allow_privilege_increase) { // Ensure extension is deleted unless we transfer ownership. scoped_ptr<Extension> scoped_extension(extension); @@ -831,6 +835,7 @@ void ExtensionsService::OnExtensionLoaded(Extension* extension, extension->set_being_upgraded(false); UpdateActiveExtensionsInCrashReporter(); + return true; } void ExtensionsService::UpdateActiveExtensionsInCrashReporter() { @@ -878,12 +883,17 @@ void ExtensionsService::OnExtensionInstalled(Extension* extension, } // Also load the extension. - OnExtensionLoaded(extension, allow_privilege_increase); + bool success = OnExtensionLoaded(extension, allow_privilege_increase); + if (!success) + extension = NULL; // extension is deleted on failure. // Erase any pending extension. if (it != pending_extensions_.end()) { pending_extensions_.erase(it); } + + if (success && profile_->GetTemplateURLModel()) + profile_->GetTemplateURLModel()->RegisterExtensionKeyword(extension); } Extension* ExtensionsService::GetExtensionByIdInternal(const std::string& id, diff --git a/chrome/browser/extensions/extensions_service.h b/chrome/browser/extensions/extensions_service.h index 25b27d2..89f167d 100644 --- a/chrome/browser/extensions/extensions_service.h +++ b/chrome/browser/extensions/extensions_service.h @@ -263,8 +263,9 @@ class ExtensionsService // Called when the initial extensions load has completed. virtual void OnLoadedInstalledExtensions(); - // Called when an extension has been loaded. - void OnExtensionLoaded(Extension* extension, + // Called when an extension has been loaded. Returns false if the extension + // failed to load (for example, if it has already been loaded). + bool OnExtensionLoaded(Extension* extension, bool allow_privilege_increase); // Called by the backend when an extension has been installed. diff --git a/chrome/browser/gtk/location_bar_view_gtk.cc b/chrome/browser/gtk/location_bar_view_gtk.cc index 2a2e571..9eb2257 100644 --- a/chrome/browser/gtk/location_bar_view_gtk.cc +++ b/chrome/browser/gtk/location_bar_view_gtk.cc @@ -94,19 +94,6 @@ const GdkColor kHintTextColor = GDK_COLOR_RGB(0x75, 0x75, 0x75); // Size of the rounding of the "Search site for:" box. const int kCornerSize = 3; -// Returns the short name for a keyword. -std::wstring GetKeywordName(Profile* profile, - const std::wstring& keyword) { - // Make sure the TemplateURL still exists. - // TODO(sky): Once LocationBarView adds a listener to the TemplateURLModel - // to track changes to the model, this should become a DCHECK. - const TemplateURL* template_url = - profile->GetTemplateURLModel()->GetTemplateURLForKeyword(keyword); - if (template_url) - return template_url->AdjustedShortNameForLocaleDirection(); - return std::wstring(); -} - // If widget is visible, increment the int pointed to by count. // Suitible for use with gtk_container_foreach. void CountVisibleWidgets(GtkWidget* widget, gpointer count) { @@ -905,11 +892,14 @@ void LocationBarViewGtk::SetKeywordLabel(const std::wstring& keyword) { if (!profile_->GetTemplateURLModel()) return; - const std::wstring short_name = GetKeywordName(profile_, keyword); - std::wstring full_name(l10n_util::GetStringF( - IDS_OMNIBOX_KEYWORD_TEXT, short_name)); + bool is_extension_keyword; + const std::wstring short_name = profile_->GetTemplateURLModel()-> + GetKeywordShortName(keyword, &is_extension_keyword); + int message_id = is_extension_keyword ? + IDS_OMNIBOX_EXTENSION_KEYWORD_TEXT : IDS_OMNIBOX_KEYWORD_TEXT; + std::wstring full_name(l10n_util::GetStringF(message_id, short_name)); std::wstring partial_name(l10n_util::GetStringF( - IDS_OMNIBOX_KEYWORD_TEXT, CalculateMinString(short_name))); + message_id, CalculateMinString(short_name))); gtk_label_set_text(GTK_LABEL(tab_to_search_full_label_), WideToUTF8(full_name).c_str()); gtk_label_set_text(GTK_LABEL(tab_to_search_partial_label_), @@ -924,10 +914,14 @@ void LocationBarViewGtk::SetKeywordHintLabel(const std::wstring& keyword) { if (!profile_->GetTemplateURLModel()) return; + bool is_extension_keyword; + const std::wstring short_name = profile_->GetTemplateURLModel()-> + GetKeywordShortName(keyword, &is_extension_keyword); + int message_id = is_extension_keyword ? + IDS_OMNIBOX_EXTENSION_KEYWORD_HINT : IDS_OMNIBOX_KEYWORD_HINT; std::vector<size_t> content_param_offsets; const std::wstring keyword_hint(l10n_util::GetStringF( - IDS_OMNIBOX_KEYWORD_HINT, std::wstring(), - GetKeywordName(profile_, keyword), &content_param_offsets)); + message_id, std::wstring(), short_name, &content_param_offsets)); if (content_param_offsets.size() != 2) { // See comments on an identical NOTREACHED() in search_provider.cc. diff --git a/chrome/browser/search_engines/template_url.cc b/chrome/browser/search_engines/template_url.cc index d91bce9..f069a8e 100644 --- a/chrome/browser/search_engines/template_url.cc +++ b/chrome/browser/search_engines/template_url.cc @@ -1,4 +1,4 @@ -// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Copyright (c) 2010 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -12,6 +12,7 @@ #include "chrome/browser/browser_process.h" #include "chrome/browser/google_url_tracker.h" #include "chrome/browser/search_engines/template_url_model.h" +#include "chrome/common/url_constants.h" #include "gfx/favicon_size.h" #include "net/base/escape.h" @@ -621,3 +622,12 @@ void TemplateURL::InvalidateCachedValues() const { keyword_generated_ = false; } } + +std::string TemplateURL::GetExtensionId() const { + DCHECK(IsExtensionKeyword()); + return GURL(WideToUTF8(url_.url())).host(); +} + +bool TemplateURL::IsExtensionKeyword() const { + return GURL(WideToUTF8(url_.url())).SchemeIs(chrome::kExtensionScheme); +} diff --git a/chrome/browser/search_engines/template_url.h b/chrome/browser/search_engines/template_url.h index 1a7d779..40561ac 100644 --- a/chrome/browser/search_engines/template_url.h +++ b/chrome/browser/search_engines/template_url.h @@ -1,4 +1,4 @@ -// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Copyright (c) 2010 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -421,6 +421,9 @@ class TemplateURL { void set_prepopulate_id(int id) { prepopulate_id_ = id; } int prepopulate_id() const { return prepopulate_id_; } + std::string GetExtensionId() const; + bool IsExtensionKeyword() const; + private: friend class WebDatabaseTest; friend class WebDatabase; diff --git a/chrome/browser/search_engines/template_url_model.cc b/chrome/browser/search_engines/template_url_model.cc index 8ad9cfe..c30a3f9 100644 --- a/chrome/browser/search_engines/template_url_model.cc +++ b/chrome/browser/search_engines/template_url_model.cc @@ -1,13 +1,13 @@ -// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Copyright (c) 2010 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/search_engines/template_url_model.h" - #include "app/l10n_util.h" #include "base/stl_util-inl.h" #include "base/utf_string_conversions.h" +#include "chrome/browser/extensions/extensions_service.h" #include "chrome/browser/google_url_tracker.h" #include "chrome/browser/history/history.h" #include "chrome/browser/history/history_notifications.h" @@ -16,6 +16,7 @@ #include "chrome/browser/profile.h" #include "chrome/browser/rlz/rlz.h" #include "chrome/browser/search_engines/template_url_prepopulate_data.h" +#include "chrome/common/extensions/extension.h" #include "chrome/common/notification_service.h" #include "chrome/common/pref_names.h" #include "chrome/common/url_constants.h" @@ -184,7 +185,8 @@ std::wstring TemplateURLModel::CleanUserInputKeyword( GURL TemplateURLModel::GenerateSearchURL(const TemplateURL* t_url) { DCHECK(t_url); const TemplateURLRef* search_ref = t_url->url(); - if (!search_ref || !search_ref->IsValid()) + // Extension keywords don't have host-based search URLs. + if (!search_ref || !search_ref->IsValid() || t_url->IsExtensionKeyword()) return GURL(); if (!search_ref->SupportsReplacement()) @@ -433,17 +435,13 @@ void TemplateURLModel::RemoveFromMapsByPointer( void TemplateURLModel::SetTemplateURLs( const std::vector<const TemplateURL*>& urls) { - DCHECK(template_urls_.empty()); // This should only be called on load, - // when we have no TemplateURLs. - // Add mappings for the new items. for (TemplateURLVector::const_iterator i = urls.begin(); i != urls.end(); ++i) { next_id_ = std::max(next_id_, (*i)->id()); AddToMaps(*i); + template_urls_.push_back(*i); } - - template_urls_ = urls; } std::vector<const TemplateURL*> TemplateURLModel::GetTemplateURLs() const { @@ -674,6 +672,20 @@ void TemplateURLModel::RemoveDuplicatePrepopulateIDs( } } +std::wstring TemplateURLModel::GetKeywordShortName(const std::wstring& keyword, + bool* is_extension_keyword) { + const TemplateURL* template_url = GetTemplateURLForKeyword(keyword); + + // TODO(sky): Once LocationBarView adds a listener to the TemplateURLModel + // to track changes to the model, this should become a DCHECK. + if (template_url) { + *is_extension_keyword = template_url->IsExtensionKeyword(); + return template_url->AdjustedShortNameForLocaleDirection(); + } + *is_extension_keyword = false; + return std::wstring(); +} + void TemplateURLModel::Observe(NotificationType type, const NotificationSource& source, const NotificationDetails& details) { @@ -711,6 +723,14 @@ void TemplateURLModel::NotifyLoaded() { NotificationType::TEMPLATE_URL_MODEL_LOADED, Source<TemplateURLModel>(this), NotificationService::NoDetails()); + + for (size_t i = 0; i < pending_extension_ids_.size(); ++i) { + Extension* extension = profile_->GetExtensionsService()-> + GetExtensionById(pending_extension_ids_[i], true); + if (extension) + RegisterExtensionKeyword(extension); + } + pending_extension_ids_.clear(); } void TemplateURLModel::MergeEnginesFromPrepopulateData() { @@ -1036,3 +1056,49 @@ void TemplateURLModel::GoogleBaseURLChanged() { OnTemplateURLModelChanged()); } } + +void TemplateURLModel::RegisterExtensionKeyword(Extension* extension) { + // TODO(mpcomplete): disable the keyword when the extension is disabled. + if (extension->omnibox_keyword().empty()) + return; + + Load(); + if (!loaded_) { + pending_extension_ids_.push_back(extension->id()); + return; + } + + if (GetTemplateURLForExtension(extension)) + return; // Already have this one registered (might be an upgrade). + + std::wstring keyword = UTF8ToWide(extension->omnibox_keyword()); + + TemplateURL* template_url = new TemplateURL; + template_url->set_short_name(UTF8ToWide(extension->name())); + template_url->set_keyword(keyword); + // This URL is not actually used for navigation. It holds the extension's + // ID, as well as forcing the TemplateURL to be treated as a search keyword. + template_url->SetURL( + UTF8ToWide(chrome::kExtensionScheme) + L"://" + + UTF8ToWide(extension->id()) + L"/?q={searchTerms}", 0, 0); + template_url->set_safe_for_autoreplace(false); + + Add(template_url); +} + +void TemplateURLModel::UnregisterExtensionKeyword(Extension* extension) { + const TemplateURL* url = GetTemplateURLForExtension(extension); + if (url) + Remove(url); +} + +const TemplateURL* TemplateURLModel::GetTemplateURLForExtension( + Extension* extension) const { + for (TemplateURLVector::const_iterator i = template_urls_.begin(); + i != template_urls_.end(); ++i) { + if ((*i)->IsExtensionKeyword() && (*i)->url()->GetHost() == extension->id()) + return *i; + } + + return NULL; +} diff --git a/chrome/browser/search_engines/template_url_model.h b/chrome/browser/search_engines/template_url_model.h index 5b76234..8bab7cb 100644 --- a/chrome/browser/search_engines/template_url_model.h +++ b/chrome/browser/search_engines/template_url_model.h @@ -1,4 +1,4 @@ -// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Copyright (c) 2010 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -13,6 +13,7 @@ #include "chrome/common/notification_registrar.h" #include "testing/gtest/include/gtest/gtest_prod.h" +class Extension; class PrefService; class Profile; @@ -136,6 +137,20 @@ class TemplateURLModel : public WebDataServiceConsumer, // date passed in. void RemoveAutoGeneratedSince(base::Time created_after); + // If the given extension has an omnibox keyword, adds a TemplateURL for that + // keyword. Only 1 keyword is allowed for a given extension. If the keyword + // already exists for this extension, does nothing. + void RegisterExtensionKeyword(Extension* extension); + + // Removes the TemplateURL containing the keyword for the given extension, + // if any. + void UnregisterExtensionKeyword(Extension* extension); + + // Returns the TemplateURL associated with the keyword for this extension. + // This works by checking the extension ID, not the keyword, so it will work + // even if the user changed the keyword. + const TemplateURL* GetTemplateURLForExtension(Extension* extension) const; + // Returns the set of URLs describing the keywords. The elements are owned // by TemplateURLModel and should not be deleted. std::vector<const TemplateURL*> GetTemplateURLs() const; @@ -186,6 +201,12 @@ class TemplateURLModel : public WebDataServiceConsumer, // number changes. void RemoveDuplicatePrepopulateIDs(std::vector<const TemplateURL*>* urls); + // Returns the locale-direction-adjusted short name for the given keyword. + // Also sets the out param to indicate whether the keyword belongs to an + // extension. + std::wstring GetKeywordShortName(const std::wstring& keyword, + bool* is_extension_keyword); + // NotificationObserver method. TemplateURLModel listens for three // notification types: // . NOTIFY_HISTORY_URL_VISITED: adds keyword search terms if the visit @@ -351,6 +372,9 @@ class TemplateURLModel : public WebDataServiceConsumer, // increasing integer that is initialized from the database. TemplateURL::IDType next_id_; + // List of extension IDs waiting for Load to have keywords registered. + std::vector<std::string> pending_extension_ids_; + DISALLOW_EVIL_CONSTRUCTORS(TemplateURLModel); }; diff --git a/chrome/browser/search_engines/template_url_table_model.cc b/chrome/browser/search_engines/template_url_table_model.cc index dcad47e..653cf7e 100644 --- a/chrome/browser/search_engines/template_url_table_model.cc +++ b/chrome/browser/search_engines/template_url_table_model.cc @@ -159,7 +159,8 @@ void TemplateURLTableModel::Reload() { const TemplateURL* template_url = *i; // NOTE: we don't use ShowInDefaultList here to avoid things bouncing // the lists while editing. - if (!template_url->show_in_default_list()) + if (!template_url->show_in_default_list() && + !template_url->IsExtensionKeyword()) entries_.push_back(new ModelEntry(this, *template_url)); } diff --git a/chrome/browser/views/location_bar/keyword_hint_view.cc b/chrome/browser/views/location_bar/keyword_hint_view.cc index 9f35e3b..14974c8 100644 --- a/chrome/browser/views/location_bar/keyword_hint_view.cc +++ b/chrome/browser/views/location_bar/keyword_hint_view.cc @@ -56,9 +56,13 @@ void KeywordHintView::SetKeyword(const std::wstring& keyword) { return; std::vector<size_t> content_param_offsets; + bool is_extension_keyword; + std::wstring short_name = profile_->GetTemplateURLModel()-> + GetKeywordShortName(keyword, &is_extension_keyword); + int message_id = is_extension_keyword ? + IDS_OMNIBOX_EXTENSION_KEYWORD_HINT : IDS_OMNIBOX_KEYWORD_HINT; const std::wstring keyword_hint(l10n_util::GetStringF( - IDS_OMNIBOX_KEYWORD_HINT, std::wstring(), - GetKeywordName(profile_, keyword), &content_param_offsets)); + message_id, std::wstring(), short_name, &content_param_offsets)); if (content_param_offsets.size() == 2) { leading_label_->SetText( keyword_hint.substr(0, content_param_offsets.front())); @@ -121,17 +125,3 @@ void KeywordHintView::Layout() { trailing_label_->SetBounds(x, 0, pref.width(), height()); } } - - -// static -std::wstring KeywordHintView::GetKeywordName(Profile* profile, - const std::wstring& keyword) { - // Make sure the TemplateURL still exists. - // TODO(sky): Once LocationBarView adds a listener to the TemplateURLModel - // to track changes to the model, this should become a DCHECK. - const TemplateURL* template_url = - profile->GetTemplateURLModel()->GetTemplateURLForKeyword(keyword); - if (template_url) - return template_url->AdjustedShortNameForLocaleDirection(); - return std::wstring(); -} diff --git a/chrome/browser/views/location_bar/keyword_hint_view.h b/chrome/browser/views/location_bar/keyword_hint_view.h index 500216c..75388df 100644 --- a/chrome/browser/views/location_bar/keyword_hint_view.h +++ b/chrome/browser/views/location_bar/keyword_hint_view.h @@ -46,10 +46,6 @@ class KeywordHintView : public views::View { void set_profile(Profile* profile) { profile_ = profile; } - // Returns the short name for a keyword. - static std::wstring GetKeywordName(Profile* profile, - const std::wstring& keyword); - private: views::Label* leading_label_; views::Label* trailing_label_; diff --git a/chrome/browser/views/location_bar/selected_keyword_view.cc b/chrome/browser/views/location_bar/selected_keyword_view.cc index 47779bc..4bcb827 100644 --- a/chrome/browser/views/location_bar/selected_keyword_view.cc +++ b/chrome/browser/views/location_bar/selected_keyword_view.cc @@ -7,6 +7,7 @@ #include "app/l10n_util.h" #include "base/i18n/rtl.h" #include "base/logging.h" +#include "chrome/browser/search_engines/template_url_model.h" #include "chrome/browser/profile.h" #include "chrome/browser/views/location_bar/keyword_hint_view.h" #include "grit/generated_resources.h" @@ -56,14 +57,16 @@ void SelectedKeywordView::SetKeyword(const std::wstring& keyword) { if (!profile_->GetTemplateURLModel()) return; - const std::wstring short_name = - KeywordHintView::GetKeywordName(profile_, keyword); - full_label_.SetText(l10n_util::GetStringF(IDS_OMNIBOX_KEYWORD_TEXT, - short_name)); + bool is_extension_keyword; + const std::wstring short_name = profile_->GetTemplateURLModel()-> + GetKeywordShortName(keyword, &is_extension_keyword); + int message_id = is_extension_keyword ? + IDS_OMNIBOX_EXTENSION_KEYWORD_TEXT : IDS_OMNIBOX_KEYWORD_TEXT; + full_label_.SetText(l10n_util::GetStringF(message_id, short_name)); const std::wstring min_string = CalculateMinString(short_name); partial_label_.SetText(min_string.empty() ? full_label_.GetText() : - l10n_util::GetStringF(IDS_OMNIBOX_KEYWORD_TEXT, min_string)); + l10n_util::GetStringF(message_id, min_string)); } std::wstring SelectedKeywordView::CalculateMinString( diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index 8478b87..b441a88 100644 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -1089,6 +1089,8 @@ 'browser/extensions/extension_message_service.h', 'browser/extensions/extension_metrics_module.cc', 'browser/extensions/extension_metrics_module.h', + 'browser/extensions/extension_omnibox_api.cc', + 'browser/extensions/extension_omnibox_api.h', 'browser/extensions/extension_page_actions_module.cc', 'browser/extensions/extension_page_actions_module.h', 'browser/extensions/extension_page_actions_module_constants.cc', diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index fda62892..e87f53e 100644 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -1341,6 +1341,7 @@ 'browser/extensions/extension_management_browsertest.cc', 'browser/extensions/extension_messages_apitest.cc', 'browser/extensions/extension_metrics_apitest.cc', + 'browser/extensions/extension_omnibox_apitest.cc', 'browser/extensions/extension_override_apitest.cc', 'browser/extensions/extension_processes_apitest.cc', 'browser/extensions/extension_startup_browsertest.cc', diff --git a/chrome/common/extensions/api/extension_api.json b/chrome/common/extensions/api/extension_api.json index c387aaaf..c6094b6 100644 --- a/chrome/common/extensions/api/extension_api.json +++ b/chrome/common/extensions/api/extension_api.json @@ -3015,5 +3015,72 @@ } ], "events": [] + }, + { + "namespace": "experimental.omnibox", + "types": [], + "functions": [ + { + "name": "sendSuggestions", + "nodoc": true, + "type": "function", + "description": "", + "parameters": [ + {"type": "integer", "name": "requestId"}, + { + "type": "array", + "description": "Array of suggest results", + "items": { + "type": "object", + "properties": { + "content": {"type": "string"}, + "description": {"type": "string"} + } + } + } + ] + } + ], + "events": [ + { + "name": "onInputChanged", + "type": "function", + "description": "User has changed what is typed into the omnibox.", + "parameters": [ + { + "type": "string", + "name": "text" + }, + { + "type": "function", + "name": "suggest", + "parameters": [ + { + "type": "array", + "description": "Array of suggest results", + "items": { + "type": "object", + "properties": { + "content": {"type": "string"}, + "description": {"type": "string"} + } + } + } + ] + } + ] + }, + { + "name": "onInputEntered", + "type": "function", + "description": "User has accepted what is typed into the omnibox.", + "parameters": [ + { + "type": "string", + "name": "text" + } + ] + } + ] } ] diff --git a/chrome/common/extensions/docs/examples/api/omnibox/background.html b/chrome/common/extensions/docs/examples/api/omnibox/background.html new file mode 100644 index 0000000..ff83ff2 --- /dev/null +++ b/chrome/common/extensions/docs/examples/api/omnibox/background.html @@ -0,0 +1,19 @@ +<script> +// This event is fired each time the user updates the text in the omnibox, +// as long as the extension's keyword mode is still active. +chrome.experimental.omnibox.onInputChanged.addListener( + function(text, suggest) { + console.log('inputChanged: ' + text); + suggest([ + {content: text + " one", description: "the first one"}, + {content: text + " number two", description: "the second entry"} + ]); + }); + +// This event is fired with the user accepts the input in the omnibox. +chrome.experimental.omnibox.onInputEntered.addListener( + function(text) { + console.log('inputEntered: ' + text); + alert('You just typed "' + text + '"'); + }); +</script> diff --git a/chrome/common/extensions/docs/examples/api/omnibox/manifest.json b/chrome/common/extensions/docs/examples/api/omnibox/manifest.json new file mode 100644 index 0000000..2b63ca2 --- /dev/null +++ b/chrome/common/extensions/docs/examples/api/omnibox/manifest.json @@ -0,0 +1,7 @@ +{ + "name": "Omnibox Example", + "version": "1.0", + "permissions": [ "experimental" ], + "background_page": "background.html", + "omnibox_keyword": "omnix" +} diff --git a/chrome/common/extensions/docs/experimental.html b/chrome/common/extensions/docs/experimental.html index 8dd43e7..49a7aa4 100644 --- a/chrome/common/extensions/docs/experimental.html +++ b/chrome/common/extensions/docs/experimental.html @@ -265,6 +265,7 @@ on the following experimental APIs: <a href="experimental.contextMenu.html">experimental.contextMenu</a></li><li> <a href="experimental.cookies.html">experimental.cookies</a></li><li> <a href="experimental.infobars.html">experimental.infobars</a></li><li> + <a href="experimental.omnibox.html">experimental.omnibox</a></li><li> <a href="experimental.processes.html">experimental.processes</a></li> </ul> diff --git a/chrome/common/extensions/extension.cc b/chrome/common/extensions/extension.cc index 89d7095..f68445e 100644 --- a/chrome/common/extensions/extension.cc +++ b/chrome/common/extensions/extension.cc @@ -1456,6 +1456,18 @@ bool Extension::InitFromValue(const DictionaryValue& source, bool require_key, } } + if (source.HasKey(keys::kOmniboxKeyword)) { + if (!source.GetString(keys::kOmniboxKeyword, &omnibox_keyword_) || + omnibox_keyword_.empty()) { + *error = errors::kInvalidOmniboxKeyword; + return false; + } + if (!HasApiPermission(Extension::kExperimentalPermission)) { + *error = errors::kOmniboxExperimental; + return false; + } + } + if (!CheckAppsAreEnabled(manifest_value_.get(), error) || !LoadWebContentEnabled(manifest_value_.get(), error) || !LoadWebOrigin(manifest_value_.get(), error) || diff --git a/chrome/common/extensions/extension.h b/chrome/common/extensions/extension.h index a135df5..49d6288 100644 --- a/chrome/common/extensions/extension.h +++ b/chrome/common/extensions/extension.h @@ -327,6 +327,8 @@ class Extension { return chrome_url_overrides_; } + const std::string omnibox_keyword() const { return omnibox_keyword_; } + bool web_content_enabled() const { return web_content_enabled_; } const ExtensionExtent& web_extent() const { return web_extent_; } @@ -528,6 +530,9 @@ class Extension { // resource to the cached image. ImageCache image_cache_; + // The omnibox keyword for this extension, or empty if there is none. + std::string omnibox_keyword_; + // Runtime data: // True if the background page is ready. diff --git a/chrome/common/extensions/extension_constants.cc b/chrome/common/extensions/extension_constants.cc index 174701c..d0a64e1 100644 --- a/chrome/common/extensions/extension_constants.cc +++ b/chrome/common/extensions/extension_constants.cc @@ -62,6 +62,7 @@ const wchar_t* kWebContent = L"web_content"; const wchar_t* kWebContentEnabled = L"web_content.enabled"; const wchar_t* kWebOrigin = L"web_content.origin"; const wchar_t* kWebPaths = L"web_content.paths"; +const wchar_t* kOmniboxKeyword = L"omnibox_keyword"; } // namespace extension_manifest_keys namespace extension_manifest_values { @@ -244,6 +245,11 @@ const char* kCannotAccessPage = "Cannot access contents of url \"*\". " const char* kCannotScriptGallery = "The extensions gallery cannot be scripted."; const char* kWebContentMustBeEnabled = "The 'web_content.enabled' property " "must be set to true in order to use any other web content features."; +const char* kInvalidOmniboxKeyword = + "Invalid value for 'omnibox_keyword'."; +const char* kOmniboxExperimental = + "You must request the 'experimental' permission in order to use the" + " omnibox API."; } // namespace extension_manifest_errors namespace extension_urls { diff --git a/chrome/common/extensions/extension_constants.h b/chrome/common/extensions/extension_constants.h index 2353b35..35c4555 100644 --- a/chrome/common/extensions/extension_constants.h +++ b/chrome/common/extensions/extension_constants.h @@ -64,6 +64,7 @@ namespace extension_manifest_keys { extern const wchar_t* kWebLaunchUrl; extern const wchar_t* kWebOrigin; extern const wchar_t* kWebPaths; + extern const wchar_t* kOmniboxKeyword; } // namespace extension_manifest_keys // Some values expected in manifests. @@ -161,6 +162,8 @@ namespace extension_manifest_errors { extern const char* kCannotAccessPage; extern const char* kCannotScriptGallery; extern const char* kWebContentMustBeEnabled; + extern const char* kInvalidOmniboxKeyword; + extern const char* kOmniboxExperimental; } // namespace extension_manifest_errors namespace extension_urls { diff --git a/chrome/common/notification_type.h b/chrome/common/notification_type.h index de9efcb..68a64b1 100644 --- a/chrome/common/notification_type.h +++ b/chrome/common/notification_type.h @@ -835,6 +835,11 @@ class NotificationType { // details are a pointer to the const BookmarksFunction in question. EXTENSION_BOOKMARKS_API_INVOKED, + // Sent when an omnibox extension has sent back omnibox suggestions. The + // source is the profile, and the details are a + // std::pair<int suggest_id, ListValue suggestions_array> + EXTENSION_OMNIBOX_SUGGESTIONS_READY, + // Privacy Blacklist ------------------------------------------------------- // Sent on the IO thread when a non-visual resource (like a cookie) diff --git a/chrome/renderer/resources/extension_apitest.js b/chrome/renderer/resources/extension_apitest.js index d587f72..62c5ab9 100644 --- a/chrome/renderer/resources/extension_apitest.js +++ b/chrome/renderer/resources/extension_apitest.js @@ -22,7 +22,7 @@ var chrome = chrome || {}; // Helper function to get around the fact that function names in javascript // are read-only, and you can't assign one to anonymous functions. function testName(test) { - return test.name || test.generatedName; + return test ? (test.name || test.generatedName) : "(no test)"; } chrome.test.fail = function(message) { diff --git a/chrome/renderer/resources/extension_process_bindings.js b/chrome/renderer/resources/extension_process_bindings.js index c3d6a33..fe393ba 100644 --- a/chrome/renderer/resources/extension_process_bindings.js +++ b/chrome/renderer/resources/extension_process_bindings.js @@ -305,6 +305,21 @@ var chrome = chrome || {}; }); } + function setupOmniboxEvents(extensionId) { + chrome.experimental.omnibox.onInputEntered = + new chrome.Event("experimental.omnibox.onInputEntered/" + extensionId); + + chrome.experimental.omnibox.onInputChanged = + new chrome.Event("experimental.omnibox.onInputChanged/" + extensionId); + chrome.experimental.omnibox.onInputChanged.dispatch = + function(text, requestId) { + var suggestCallback = function(suggestions) { + chrome.experimental.omnibox.sendSuggestions(requestId, suggestions); + } + chrome.Event.prototype.dispatch.apply(this, [text, suggestCallback]); + }; + } + chromeHidden.onLoad.addListener(function (extensionId) { chrome.initExtension(extensionId, false); @@ -636,6 +651,7 @@ var chrome = chrome || {}; setupToolstripEvents(GetRenderViewId()); setupPopupEvents(GetRenderViewId()); setupHiddenContextMenuEvent(extensionId); + setupOmniboxEvents(extensionId); }); if (!chrome.experimental) diff --git a/chrome/renderer/resources/renderer_extension_bindings.js b/chrome/renderer/resources/renderer_extension_bindings.js index b4ff23f..dc53cd7 100644 --- a/chrome/renderer/resources/renderer_extension_bindings.js +++ b/chrome/renderer/resources/renderer_extension_bindings.js @@ -253,6 +253,7 @@ var chrome = chrome || {}; "experimental.idle", "experimental.infobars", "experimental.metrics", + "experimental.omnibox", "experimental.popup", "experimental.processes", "history", diff --git a/chrome/test/data/extensions/api_test/omnibox/manifest.json b/chrome/test/data/extensions/api_test/omnibox/manifest.json new file mode 100644 index 0000000..8d3a577 --- /dev/null +++ b/chrome/test/data/extensions/api_test/omnibox/manifest.json @@ -0,0 +1,8 @@ +{ + "name": "chrome.extension.omnibox", + "version": "0.1", + "description": "end-to-end browser test for chrome.omnibox API", + "background_page": "test.html", + "omnibox_keyword": "keyword", + "permissions": ["experimental"] +} diff --git a/chrome/test/data/extensions/api_test/omnibox/test.html b/chrome/test/data/extensions/api_test/omnibox/test.html new file mode 100644 index 0000000..3955962 --- /dev/null +++ b/chrome/test/data/extensions/api_test/omnibox/test.html @@ -0,0 +1,23 @@ +<script> +if (!chrome.omnibox) { + chrome.omnibox = chrome.experimental.omnibox; +} + +chrome.experimental.omnibox.onInputChanged.addListener( + function(text, suggest) { + suggest([ + {content: text + "n1", description: "description1"}, + {content: text + "n2", description: "description2"}, + {content: text + "n3", description: "description3"} + ]); + }); + +chrome.experimental.omnibox.onInputEntered.addListener( + function(text) { + chrome.test.assertEq("command", text); + chrome.test.notifyPass(); + }); + +// Now we wait for the input events to fire. +chrome.test.notifyPass(); +</script> |