diff options
| author | avi@chromium.org <avi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-02-23 19:45:42 +0000 |
|---|---|---|
| committer | avi@chromium.org <avi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-02-23 19:45:42 +0000 |
| commit | 62f7777cb7c9dba41286c17bab10e30152a5a841 (patch) | |
| tree | 9eebf968b2c840b1536d4321ed0f086d96135eaf /chrome/browser/ui/search_engines | |
| parent | 4d8af92fa3a43f203679bbbd37968c127d056a4d (diff) | |
| download | chromium_src-62f7777cb7c9dba41286c17bab10e30152a5a841.zip chromium_src-62f7777cb7c9dba41286c17bab10e30152a5a841.tar.gz chromium_src-62f7777cb7c9dba41286c17bab10e30152a5a841.tar.bz2 | |
Move the UI portions of search engine management to ui/search_engines.
BUG=none
TEST=no visible change
Review URL: http://codereview.chromium.org/6573008
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@75774 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/ui/search_engines')
7 files changed, 1190 insertions, 0 deletions
diff --git a/chrome/browser/ui/search_engines/edit_search_engine_controller.cc b/chrome/browser/ui/search_engines/edit_search_engine_controller.cc new file mode 100644 index 0000000..bd397f2 --- /dev/null +++ b/chrome/browser/ui/search_engines/edit_search_engine_controller.cc @@ -0,0 +1,149 @@ +// Copyright (c) 2011 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/ui/search_engines/edit_search_engine_controller.h" + +#include "base/string_util.h" +#include "base/utf_string_conversions.h" +#include "chrome/browser/metrics/user_metrics.h" +#include "chrome/browser/net/url_fixer_upper.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/search_engines/template_url.h" +#include "chrome/browser/search_engines/template_url_model.h" +#include "googleurl/src/gurl.h" + +EditSearchEngineController::EditSearchEngineController( + const TemplateURL* template_url, + EditSearchEngineControllerDelegate* edit_keyword_delegate, + Profile* profile) + : template_url_(template_url), + edit_keyword_delegate_(edit_keyword_delegate), + profile_(profile) { + DCHECK(profile_); +} + +bool EditSearchEngineController::IsTitleValid( + const string16& title_input) const { + return !CollapseWhitespace(title_input, true).empty(); +} + +bool EditSearchEngineController::IsURLValid( + const std::string& url_input) const { + std::string url = GetFixedUpURL(url_input); + if (url.empty()) + return false; + + // Use TemplateURLRef to extract the search placeholder. + TemplateURLRef template_ref(url, 0, 0); + if (!template_ref.IsValid()) + return false; + + if (!template_ref.SupportsReplacement()) { + // If this is the default search engine, there must be a search term + // placeholder. + if (template_url_ == + profile_->GetTemplateURLModel()->GetDefaultSearchProvider()) + return false; + return GURL(url).is_valid(); + } + + // If the url has a search term, replace it with a random string and make + // sure the resulting URL is valid. We don't check the validity of the url + // with the search term as that is not necessarily valid. + return GURL(template_ref.ReplaceSearchTerms(TemplateURL(), ASCIIToUTF16("a"), + TemplateURLRef::NO_SUGGESTIONS_AVAILABLE, string16())).is_valid(); +} + +bool EditSearchEngineController::IsKeywordValid( + const string16& keyword_input) const { + string16 keyword_input_trimmed(CollapseWhitespace(keyword_input, true)); + if (keyword_input_trimmed.empty()) + return false; // Do not allow empty keyword. + const TemplateURL* turl_with_keyword = + profile_->GetTemplateURLModel()->GetTemplateURLForKeyword( + keyword_input_trimmed); + return (turl_with_keyword == NULL || turl_with_keyword == template_url_); +} + +void EditSearchEngineController::AcceptAddOrEdit( + const string16& title_input, + const string16& keyword_input, + const std::string& url_input) { + std::string url_string = GetFixedUpURL(url_input); + DCHECK(!url_string.empty()); + + const TemplateURL* existing = + profile_->GetTemplateURLModel()->GetTemplateURLForKeyword( + keyword_input); + if (existing && + (!edit_keyword_delegate_ || existing != template_url_)) { + // An entry may have been added with the same keyword string while the + // user edited the dialog, either automatically or by the user (if we're + // confirming a JS addition, they could have the Options dialog open at the + // same time). If so, just ignore this add. + // TODO(pamg): Really, we should modify the entry so this later one + // overwrites it. But we don't expect this case to be common. + CleanUpCancelledAdd(); + return; + } + + if (!edit_keyword_delegate_) { + // Confiming an entry we got from JS. We have a template_url_, but it + // hasn't yet been added to the model. + DCHECK(template_url_); + // const_cast is ugly, but this is the same thing the TemplateURLModel + // does in a similar situation (updating an existing TemplateURL with + // data from a new one). + TemplateURL* modifiable_url = const_cast<TemplateURL*>(template_url_); + modifiable_url->set_short_name(title_input); + modifiable_url->set_keyword(keyword_input); + modifiable_url->SetURL(url_string, 0, 0); + // TemplateURLModel takes ownership of template_url_. + profile_->GetTemplateURLModel()->Add(modifiable_url); + UserMetrics::RecordAction(UserMetricsAction("KeywordEditor_AddKeywordJS"), + profile_); + } else { + // Adding or modifying an entry via the Delegate. + edit_keyword_delegate_->OnEditedKeyword(template_url_, + title_input, + keyword_input, + url_string); + } +} + +void EditSearchEngineController::CleanUpCancelledAdd() { + if (!edit_keyword_delegate_ && template_url_) { + // When we have no Delegate, we know that the template_url_ hasn't yet been + // added to the model, so we need to clean it up. + delete template_url_; + template_url_ = NULL; + } +} + +std::string EditSearchEngineController::GetFixedUpURL( + const std::string& url_input) const { + std::string url; + TrimWhitespace(TemplateURLRef::DisplayURLToURLRef(UTF8ToUTF16(url_input)), + TRIM_ALL, &url); + if (url.empty()) + return url; + + // Parse the string as a URL to determine the scheme. If we need to, add the + // scheme. As the scheme may be expanded (as happens with {google:baseURL}) + // we need to replace the search terms before testing for the scheme. + TemplateURL t_url; + t_url.SetURL(url, 0, 0); + std::string expanded_url = + t_url.url()->ReplaceSearchTerms(t_url, ASCIIToUTF16("x"), 0, string16()); + url_parse::Parsed parts; + std::string scheme( + URLFixerUpper::SegmentURL(expanded_url, &parts)); + if (!parts.scheme.is_valid()) { + scheme.append("://"); + url.insert(0, scheme); + } + + return url; +} + diff --git a/chrome/browser/ui/search_engines/edit_search_engine_controller.h b/chrome/browser/ui/search_engines/edit_search_engine_controller.h new file mode 100644 index 0000000..5d57c1d --- /dev/null +++ b/chrome/browser/ui/search_engines/edit_search_engine_controller.h @@ -0,0 +1,91 @@ +// Copyright (c) 2011 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_UI_SEARCH_ENGINES_EDIT_SEARCH_ENGINE_CONTROLLER_H_ +#define CHROME_BROWSER_UI_SEARCH_ENGINES_EDIT_SEARCH_ENGINE_CONTROLLER_H_ +#pragma once + +#include <string> + +#include "base/string16.h" +#include "ui/gfx/native_widget_types.h" + +class Profile; +class TemplateURL; + +class EditSearchEngineControllerDelegate { + public: + // Invoked from the EditSearchEngineController when the user accepts the + // edits. NOTE: |template_url| is the value supplied to + // EditSearchEngineController's constructor, and may be NULL. A NULL value + // indicates a new TemplateURL should be created rather than modifying an + // existing TemplateURL. + virtual void OnEditedKeyword(const TemplateURL* template_url, + const string16& title, + const string16& keyword, + const std::string& url) = 0; + + protected: + virtual ~EditSearchEngineControllerDelegate() {} +}; + +// EditSearchEngineController provides the core platform independent logic +// for the Edit Search Engine dialog. +class EditSearchEngineController { + public: + // The |template_url| and/or |edit_keyword_delegate| may be NULL. + EditSearchEngineController( + const TemplateURL* template_url, + EditSearchEngineControllerDelegate* edit_keyword_delegate, + Profile* profile); + ~EditSearchEngineController() {} + + // Returns true if the value of |title_input| is a valid search engine name. + bool IsTitleValid(const string16& title_input) const; + + // Returns true if the value of |url_input| represents a valid search engine + // URL. The URL is valid if it contains no search terms and is a valid + // url, or if it contains a search term and replacing that search term with a + // character results in a valid url. + bool IsURLValid(const std::string& url_input) const; + + // Returns true if the value of |keyword_input| represents a valid keyword. + // The keyword is valid if it is non-empty and does not conflict with an + // existing entry. NOTE: this is just the keyword, not the title and url. + bool IsKeywordValid(const string16& keyword_input) const; + + // Completes the add or edit of a search engine. + void AcceptAddOrEdit(const string16& title_input, + const string16& keyword_input, + const std::string& url_input); + + // Deletes an unused TemplateURL, if its add was cancelled and it's not + // already owned by the TemplateURLModel. + void CleanUpCancelledAdd(); + + // Accessors. + const TemplateURL* template_url() const { return template_url_; } + const Profile* profile() const { return profile_; } + + private: + // Fixes up and returns the URL the user has input. The returned URL is + // suitable for use by TemplateURL. + std::string GetFixedUpURL(const std::string& url_input) const; + + // The TemplateURL we're displaying information for. It may be NULL. If we + // have a keyword_editor_view, we assume that this TemplateURL is already in + // the TemplateURLModel; if not, we assume it isn't. + const TemplateURL* template_url_; + + // We may have been created by this, in which case we will call back to it on + // success to add/modify the entry. May be NULL. + EditSearchEngineControllerDelegate* edit_keyword_delegate_; + + // Profile whose TemplateURLModel we're modifying. + Profile* profile_; + + DISALLOW_COPY_AND_ASSIGN(EditSearchEngineController); +}; + +#endif // CHROME_BROWSER_UI_SEARCH_ENGINES_EDIT_SEARCH_ENGINE_CONTROLLER_H_ diff --git a/chrome/browser/ui/search_engines/keyword_editor_controller.cc b/chrome/browser/ui/search_engines/keyword_editor_controller.cc new file mode 100644 index 0000000..0c98b0f --- /dev/null +++ b/chrome/browser/ui/search_engines/keyword_editor_controller.cc @@ -0,0 +1,114 @@ +// Copyright (c) 2011 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/ui/search_engines/keyword_editor_controller.h" + +#include "base/utf_string_conversions.h" +#include "chrome/browser/metrics/user_metrics.h" +#include "chrome/browser/prefs/pref_service.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/search_engines/template_url.h" +#include "chrome/browser/search_engines/template_url_model.h" +#include "chrome/browser/ui/search_engines/template_url_table_model.h" +#include "chrome/common/pref_names.h" + +KeywordEditorController::KeywordEditorController(Profile* profile) + : profile_(profile) { + table_model_.reset(new TemplateURLTableModel(profile->GetTemplateURLModel())); +} + +KeywordEditorController::~KeywordEditorController() { +} + +// static +// TODO(rsesek): Other platforms besides Mac should remember window +// placement. http://crbug.com/22269 +void KeywordEditorController::RegisterPrefs(PrefService* prefs) { + prefs->RegisterDictionaryPref(prefs::kKeywordEditorWindowPlacement); +} + +int KeywordEditorController::AddTemplateURL(const string16& title, + const string16& keyword, + const std::string& url) { + DCHECK(!url.empty()); + + UserMetrics::RecordAction(UserMetricsAction("KeywordEditor_AddKeyword"), + profile_); + + TemplateURL* template_url = new TemplateURL(); + template_url->set_short_name(title); + template_url->set_keyword(keyword); + template_url->SetURL(url, 0, 0); + + // There's a bug (1090726) in TableView with groups enabled such that newly + // added items in groups ALWAYS appear at the end, regardless of the index + // passed in. Worse yet, the selected rows get messed up when this happens + // causing other problems. As a work around we always add the item to the end + // of the list. + const int new_index = table_model_->RowCount(); + table_model_->Add(new_index, template_url); + + return new_index; +} + +void KeywordEditorController::ModifyTemplateURL(const TemplateURL* template_url, + const string16& title, + const string16& keyword, + const std::string& url) { + const int index = table_model_->IndexOfTemplateURL(template_url); + if (index == -1) { + // Will happen if url was deleted out from under us while the user was + // editing it. + return; + } + + // Don't do anything if the entry didn't change. + if (template_url->short_name() == title && + template_url->keyword() == keyword && + ((url.empty() && !template_url->url()) || + (!url.empty() && template_url->url() && + template_url->url()->url() == url))) { + return; + } + + table_model_->ModifyTemplateURL(index, title, keyword, url); + + UserMetrics::RecordAction(UserMetricsAction("KeywordEditor_ModifiedKeyword"), + profile_); +} + +bool KeywordEditorController::CanEdit(const TemplateURL* url) const { + return !url_model()->is_default_search_managed() || + url != url_model()->GetDefaultSearchProvider(); +} + +bool KeywordEditorController::CanMakeDefault(const TemplateURL* url) const { + return url_model()->CanMakeDefault(url); +} + +bool KeywordEditorController::CanRemove(const TemplateURL* url) const { + return url != url_model()->GetDefaultSearchProvider(); +} + +void KeywordEditorController::RemoveTemplateURL(int index) { + table_model_->Remove(index); + UserMetrics::RecordAction(UserMetricsAction("KeywordEditor_RemoveKeyword"), + profile_); +} + +int KeywordEditorController::MakeDefaultTemplateURL(int index) { + return table_model_->MakeDefaultTemplateURL(index); +} + +bool KeywordEditorController::loaded() const { + return url_model()->loaded(); +} + +const TemplateURL* KeywordEditorController::GetTemplateURL(int index) const { + return &table_model_->GetTemplateURL(index); +} + +TemplateURLModel* KeywordEditorController::url_model() const { + return table_model_->template_url_model(); +} diff --git a/chrome/browser/ui/search_engines/keyword_editor_controller.h b/chrome/browser/ui/search_engines/keyword_editor_controller.h new file mode 100644 index 0000000..6b8ff74 --- /dev/null +++ b/chrome/browser/ui/search_engines/keyword_editor_controller.h @@ -0,0 +1,80 @@ +// Copyright (c) 2011 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_UI_SEARCH_ENGINES_KEYWORD_EDITOR_CONTROLLER_H_ +#define CHROME_BROWSER_UI_SEARCH_ENGINES_KEYWORD_EDITOR_CONTROLLER_H_ +#pragma once + +#include <string> + +#include "base/basictypes.h" +#include "base/scoped_ptr.h" +#include "base/string16.h" + +class PrefService; +class Profile; +class TemplateURL; +class TemplateURLModel; +class TemplateURLTableModel; + +class KeywordEditorController { + public: + explicit KeywordEditorController(Profile* profile); + ~KeywordEditorController(); + + static void RegisterPrefs(PrefService* prefs); + + // Invoked when the user succesfully fills out the add keyword dialog. + // Propagates the change to the TemplateURLModel and updates the table model. + // Returns the index of the added URL. + int AddTemplateURL(const string16& title, + const string16& keyword, + const std::string& url); + + // Invoked when the user modifies a TemplateURL. Updates the TemplateURLModel + // and table model appropriately. + void ModifyTemplateURL(const TemplateURL* template_url, + const string16& title, + const string16& keyword, + const std::string& url); + + // Return true if the given |url| can be edited. + bool CanEdit(const TemplateURL* url) const; + + // Return true if the given |url| can be made the default. + bool CanMakeDefault(const TemplateURL* url) const; + + // Return true if the given |url| can be removed. + bool CanRemove(const TemplateURL* url) const; + + // Remove the TemplateURL at the specified index in the TableModel. + void RemoveTemplateURL(int index); + + // Make the TemplateURL at the specified index (into the TableModel) the + // default search provider. Return the new index, or -1 if nothing was done. + int MakeDefaultTemplateURL(int index); + + // Return true if the |url_model_| data is loaded. + bool loaded() const; + + // Return the TemplateURL corresponding to the |index| in the model. + const TemplateURL* GetTemplateURL(int index) const; + + TemplateURLTableModel* table_model() { + return table_model_.get(); + } + + TemplateURLModel* url_model() const; + + private: + // The profile. + Profile* profile_; + + // Model for the TableView. + scoped_ptr<TemplateURLTableModel> table_model_; + + DISALLOW_COPY_AND_ASSIGN(KeywordEditorController); +}; + +#endif // CHROME_BROWSER_UI_SEARCH_ENGINES_KEYWORD_EDITOR_CONTROLLER_H_ diff --git a/chrome/browser/ui/search_engines/keyword_editor_controller_unittest.cc b/chrome/browser/ui/search_engines/keyword_editor_controller_unittest.cc new file mode 100644 index 0000000..72b5bc1 --- /dev/null +++ b/chrome/browser/ui/search_engines/keyword_editor_controller_unittest.cc @@ -0,0 +1,258 @@ +// Copyright (c) 2011 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 "base/string16.h" +#include "base/utf_string_conversions.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/search_engines/template_url.h" +#include "chrome/browser/search_engines/template_url_model.h" +#include "chrome/browser/ui/search_engines/keyword_editor_controller.h" +#include "chrome/browser/ui/search_engines/template_url_table_model.h" +#include "chrome/common/notification_details.h" +#include "chrome/common/notification_source.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/testing_pref_service.h" +#include "chrome/test/testing_profile.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/base/models/table_model_observer.h" + +static const string16 kA(ASCIIToUTF16("a")); +static const string16 kA1(ASCIIToUTF16("a1")); +static const string16 kB(ASCIIToUTF16("b")); +static const string16 kB1(ASCIIToUTF16("b1")); + +// Base class for keyword editor tests. Creates a profile containing an +// empty TemplateURLModel. +class KeywordEditorControllerTest : public testing::Test, + public ui::TableModelObserver { + public: + // Initializes all of the state. + void Init(bool simulate_load_failure); + + virtual void SetUp() { + Init(false); + } + + virtual void OnModelChanged() { + model_changed_count_++; + } + + virtual void OnItemsChanged(int start, int length) { + items_changed_count_++; + } + + virtual void OnItemsAdded(int start, int length) { + added_count_++; + } + + virtual void OnItemsRemoved(int start, int length) { + removed_count_++; + } + + void VerifyChangeCount(int model_changed_count, int item_changed_count, + int added_count, int removed_count) { + ASSERT_EQ(model_changed_count, model_changed_count_); + ASSERT_EQ(item_changed_count, items_changed_count_); + ASSERT_EQ(added_count, added_count_); + ASSERT_EQ(removed_count, removed_count_); + ClearChangeCount(); + } + + void ClearChangeCount() { + model_changed_count_ = items_changed_count_ = added_count_ = + removed_count_ = 0; + } + + void SimulateDefaultSearchIsManaged(const std::string& url) { + ASSERT_FALSE(url.empty()); + TestingPrefService* service = profile_->GetTestingPrefService(); + service->SetManagedPref( + prefs::kDefaultSearchProviderEnabled, + Value::CreateBooleanValue(true)); + service->SetManagedPref( + prefs::kDefaultSearchProviderSearchURL, + Value::CreateStringValue(url)); + service->SetManagedPref( + prefs::kDefaultSearchProviderName, + Value::CreateStringValue("managed")); + // Clear the IDs that are not specified via policy. + service->SetManagedPref( + prefs::kDefaultSearchProviderID, new StringValue("")); + service->SetManagedPref( + prefs::kDefaultSearchProviderPrepopulateID, new StringValue("")); + model_->Observe( + NotificationType::PREF_CHANGED, + Source<PrefService>(profile_->GetTestingPrefService()), + Details<std::string>(NULL)); + } + + TemplateURLTableModel* table_model() const { + return controller_->table_model(); + } + + protected: + MessageLoopForUI message_loop_; + scoped_ptr<TestingProfile> profile_; + scoped_ptr<KeywordEditorController> controller_; + TemplateURLModel* model_; + + int model_changed_count_; + int items_changed_count_; + int added_count_; + int removed_count_; +}; + +void KeywordEditorControllerTest::Init(bool simulate_load_failure) { + ClearChangeCount(); + + // If init is called twice, make sure that the controller is destroyed before + // the profile is. + controller_.reset(); + profile_.reset(new TestingProfile()); + profile_->CreateTemplateURLModel(); + + model_ = profile_->GetTemplateURLModel(); + if (simulate_load_failure) + model_->OnWebDataServiceRequestDone(NULL, NULL); + + controller_.reset(new KeywordEditorController(profile_.get())); + controller_->table_model()->SetObserver(this); +} + +// Tests adding a TemplateURL. +TEST_F(KeywordEditorControllerTest, Add) { + controller_->AddTemplateURL(kA, kB, "http://c"); + + // Verify the observer was notified. + VerifyChangeCount(0, 0, 1, 0); + if (HasFatalFailure()) + return; + + // Verify the TableModel has the new data. + ASSERT_EQ(1, table_model()->RowCount()); + + // Verify the TemplateURLModel has the new entry. + ASSERT_EQ(1U, model_->GetTemplateURLs().size()); + + // Verify the entry is what we added. + const TemplateURL* turl = model_->GetTemplateURLs()[0]; + EXPECT_EQ(ASCIIToUTF16("a"), turl->short_name()); + EXPECT_EQ(ASCIIToUTF16("b"), turl->keyword()); + ASSERT_TRUE(turl->url() != NULL); + EXPECT_EQ("http://c", turl->url()->url()); +} + +// Tests modifying a TemplateURL. +TEST_F(KeywordEditorControllerTest, Modify) { + controller_->AddTemplateURL(kA, kB, "http://c"); + ClearChangeCount(); + + // Modify the entry. + const TemplateURL* turl = model_->GetTemplateURLs()[0]; + controller_->ModifyTemplateURL(turl, kA1, kB1, "http://c1"); + + // Make sure it was updated appropriately. + VerifyChangeCount(0, 1, 0, 0); + EXPECT_EQ(ASCIIToUTF16("a1"), turl->short_name()); + EXPECT_EQ(ASCIIToUTF16("b1"), turl->keyword()); + ASSERT_TRUE(turl->url() != NULL); + EXPECT_EQ("http://c1", turl->url()->url()); +} + +// Tests making a TemplateURL the default search provider. +TEST_F(KeywordEditorControllerTest, MakeDefault) { + controller_->AddTemplateURL(kA, kB, "http://c{searchTerms}"); + ClearChangeCount(); + + const TemplateURL* turl = model_->GetTemplateURLs()[0]; + int new_default = controller_->MakeDefaultTemplateURL(0); + EXPECT_EQ(0, new_default); + // Making an item the default sends a handful of changes. Which are sent isn't + // important, what is important is 'something' is sent. + ASSERT_TRUE(items_changed_count_ > 0 || added_count_ > 0 || + removed_count_ > 0); + ASSERT_TRUE(model_->GetDefaultSearchProvider() == turl); + + // Making it default a second time should fail. + new_default = controller_->MakeDefaultTemplateURL(0); + EXPECT_EQ(-1, new_default); +} + +// Tests that a TemplateURL can't be made the default if the default search +// provider is managed via policy. +TEST_F(KeywordEditorControllerTest, CannotSetDefaultWhileManaged) { + controller_->AddTemplateURL(kA, kB, "http://c{searchTerms}"); + controller_->AddTemplateURL(kA1, kB1, "http://d{searchTerms}"); + ClearChangeCount(); + + const TemplateURL* turl1 = + model_->GetTemplateURLForKeyword(ASCIIToUTF16("b")); + ASSERT_TRUE(turl1 != NULL); + const TemplateURL* turl2 = + model_->GetTemplateURLForKeyword(ASCIIToUTF16("b1")); + ASSERT_TRUE(turl2 != NULL); + + EXPECT_TRUE(controller_->CanMakeDefault(turl1)); + EXPECT_TRUE(controller_->CanMakeDefault(turl2)); + + SimulateDefaultSearchIsManaged(turl2->url()->url()); + EXPECT_TRUE(model_->is_default_search_managed()); + + EXPECT_FALSE(controller_->CanMakeDefault(turl1)); + EXPECT_FALSE(controller_->CanMakeDefault(turl2)); +} + +// Tests that a TemplateURL can't be edited if it is the managed default search +// provider. +TEST_F(KeywordEditorControllerTest, EditManagedDefault) { + controller_->AddTemplateURL(kA, kB, "http://c{searchTerms}"); + controller_->AddTemplateURL(kA1, kB1, "http://d{searchTerms}"); + ClearChangeCount(); + + const TemplateURL* turl1 = + model_->GetTemplateURLForKeyword(ASCIIToUTF16("b")); + ASSERT_TRUE(turl1 != NULL); + const TemplateURL* turl2 = + model_->GetTemplateURLForKeyword(ASCIIToUTF16("b1")); + ASSERT_TRUE(turl2 != NULL); + + EXPECT_TRUE(controller_->CanEdit(turl1)); + EXPECT_TRUE(controller_->CanEdit(turl2)); + + // Simulate setting a managed default. This will add another template URL to + // the model. + SimulateDefaultSearchIsManaged(turl2->url()->url()); + EXPECT_TRUE(model_->is_default_search_managed()); + EXPECT_TRUE(controller_->CanEdit(turl1)); + EXPECT_TRUE(controller_->CanEdit(turl2)); + EXPECT_FALSE(controller_->CanEdit(model_->GetDefaultSearchProvider())); +} + +TEST_F(KeywordEditorControllerTest, MakeDefaultNoWebData) { + // Simulate a failure to load Web Data. + Init(true); + + controller_->AddTemplateURL(kA, kB, "http://c{searchTerms}"); + ClearChangeCount(); + + // This should not result in a crash. + int new_default = controller_->MakeDefaultTemplateURL(0); + EXPECT_EQ(0, new_default); +} + +// Mutates the TemplateURLModel and make sure table model is updating +// appropriately. +TEST_F(KeywordEditorControllerTest, MutateTemplateURLModel) { + TemplateURL* turl = new TemplateURL(); + turl->set_keyword(ASCIIToUTF16("a")); + turl->set_short_name(ASCIIToUTF16("b")); + model_->Add(turl); + + // Table model should have updated. + VerifyChangeCount(1, 0, 0, 0); + + // And should contain the newly added TemplateURL. + ASSERT_EQ(1, table_model()->RowCount()); + ASSERT_EQ(0, table_model()->IndexOfTemplateURL(turl)); +} diff --git a/chrome/browser/ui/search_engines/template_url_table_model.cc b/chrome/browser/ui/search_engines/template_url_table_model.cc new file mode 100644 index 0000000..6c82c85 --- /dev/null +++ b/chrome/browser/ui/search_engines/template_url_table_model.cc @@ -0,0 +1,381 @@ +// Copyright (c) 2011 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/ui/search_engines/template_url_table_model.h" + +#include "base/callback.h" +#include "base/i18n/rtl.h" +#include "base/stl_util-inl.h" +#include "base/utf_string_conversions.h" +#include "chrome/browser/favicon_service.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/search_engines/template_url.h" +#include "chrome/browser/search_engines/template_url_model.h" +#include "grit/app_resources.h" +#include "grit/generated_resources.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/base/models/table_model_observer.h" +#include "ui/base/resource/resource_bundle.h" +#include "ui/gfx/codec/png_codec.h" + +// Group IDs used by TemplateURLTableModel. +static const int kMainGroupID = 0; +static const int kOtherGroupID = 1; + +// ModelEntry ---------------------------------------------------- + +// ModelEntry wraps a TemplateURL as returned from the TemplateURL. +// ModelEntry also tracks state information about the URL. + +// Icon used while loading, or if a specific favicon can't be found. +static SkBitmap* default_icon = NULL; + +class ModelEntry { + public: + explicit ModelEntry(TemplateURLTableModel* model, + const TemplateURL& template_url) + : template_url_(template_url), + load_state_(NOT_LOADED), + model_(model) { + if (!default_icon) { + default_icon = ResourceBundle::GetSharedInstance(). + GetBitmapNamed(IDR_DEFAULT_FAVICON); + } + } + + const TemplateURL& template_url() { + return template_url_; + } + + SkBitmap GetIcon() { + if (load_state_ == NOT_LOADED) + LoadFavIcon(); + if (!fav_icon_.isNull()) + return fav_icon_; + return *default_icon; + } + + // Resets internal status so that the next time the icon is asked for its + // fetched again. This should be invoked if the url is modified. + void ResetIcon() { + load_state_ = NOT_LOADED; + fav_icon_ = SkBitmap(); + } + + private: + // State of the favicon. + enum LoadState { + NOT_LOADED, + LOADING, + LOADED + }; + + void LoadFavIcon() { + load_state_ = LOADED; + FaviconService* favicon_service = + model_->template_url_model()->profile()->GetFaviconService( + Profile::EXPLICIT_ACCESS); + if (!favicon_service) + return; + GURL fav_icon_url = template_url().GetFavIconURL(); + if (!fav_icon_url.is_valid()) { + // The favicon url isn't always set. Guess at one here. + if (template_url_.url() && template_url_.url()->IsValid()) { + GURL url = GURL(template_url_.url()->url()); + if (url.is_valid()) + fav_icon_url = TemplateURL::GenerateFaviconURL(url); + } + if (!fav_icon_url.is_valid()) + return; + } + load_state_ = LOADING; + favicon_service->GetFavicon(fav_icon_url, + &request_consumer_, + NewCallback(this, &ModelEntry::OnFavIconDataAvailable)); + } + + void OnFavIconDataAvailable( + FaviconService::Handle handle, + bool know_favicon, + scoped_refptr<RefCountedMemory> data, + bool expired, + GURL icon_url) { + load_state_ = LOADED; + if (know_favicon && data.get() && + gfx::PNGCodec::Decode(data->front(), data->size(), &fav_icon_)) { + model_->FavIconAvailable(this); + } + } + + const TemplateURL& template_url_; + SkBitmap fav_icon_; + LoadState load_state_; + TemplateURLTableModel* model_; + CancelableRequestConsumer request_consumer_; + + DISALLOW_COPY_AND_ASSIGN(ModelEntry); +}; + +// TemplateURLTableModel ----------------------------------------- + +TemplateURLTableModel::TemplateURLTableModel( + TemplateURLModel* template_url_model) + : observer_(NULL), + template_url_model_(template_url_model) { + DCHECK(template_url_model); + template_url_model_->Load(); + template_url_model_->AddObserver(this); + Reload(); +} + +TemplateURLTableModel::~TemplateURLTableModel() { + template_url_model_->RemoveObserver(this); + STLDeleteElements(&entries_); + entries_.clear(); +} + +void TemplateURLTableModel::Reload() { + STLDeleteElements(&entries_); + entries_.clear(); + + std::vector<const TemplateURL*> urls = template_url_model_->GetTemplateURLs(); + + // Keywords that can be made the default first. + for (std::vector<const TemplateURL*>::iterator i = urls.begin(); + i != urls.end(); ++i) { + const TemplateURL& template_url = *(*i); + // NOTE: we don't use ShowInDefaultList here to avoid items bouncing around + // the lists while editing. + if (template_url.show_in_default_list()) + entries_.push_back(new ModelEntry(this, template_url)); + } + + last_search_engine_index_ = static_cast<int>(entries_.size()); + + // Then the rest. + for (std::vector<const TemplateURL*>::iterator i = urls.begin(); + i != urls.end(); ++i) { + 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() && + !template_url->IsExtensionKeyword()) { + entries_.push_back(new ModelEntry(this, *template_url)); + } + } + + if (observer_) + observer_->OnModelChanged(); +} + +int TemplateURLTableModel::RowCount() { + return static_cast<int>(entries_.size()); +} + +string16 TemplateURLTableModel::GetText(int row, int col_id) { + DCHECK(row >= 0 && row < RowCount()); + const TemplateURL& url = entries_[row]->template_url(); + + switch (col_id) { + case IDS_SEARCH_ENGINES_EDITOR_DESCRIPTION_COLUMN: { + string16 url_short_name = url.short_name(); + // TODO(xji): Consider adding a special case if the short name is a URL, + // since those should always be displayed LTR. Please refer to + // http://crbug.com/6726 for more information. + base::i18n::AdjustStringForLocaleDirection(&url_short_name); + if (template_url_model_->GetDefaultSearchProvider() == &url) { + return l10n_util::GetStringFUTF16( + IDS_SEARCH_ENGINES_EDITOR_DEFAULT_ENGINE, + url_short_name); + } + return url_short_name; + } + + case IDS_SEARCH_ENGINES_EDITOR_KEYWORD_COLUMN: { + // Keyword should be domain name. Force it to have LTR directionality. + string16 keyword = url.keyword(); + keyword = base::i18n::GetDisplayStringInLTRDirectionality(keyword); + return keyword; + } + + default: + NOTREACHED(); + return string16(); + } +} + +SkBitmap TemplateURLTableModel::GetIcon(int row) { + DCHECK(row >= 0 && row < RowCount()); + return entries_[row]->GetIcon(); +} + +void TemplateURLTableModel::SetObserver(ui::TableModelObserver* observer) { + observer_ = observer; +} + +bool TemplateURLTableModel::HasGroups() { + return true; +} + +TemplateURLTableModel::Groups TemplateURLTableModel::GetGroups() { + Groups groups; + + Group search_engine_group; + search_engine_group.title = + l10n_util::GetStringUTF16(IDS_SEARCH_ENGINES_EDITOR_MAIN_SEPARATOR); + search_engine_group.id = kMainGroupID; + groups.push_back(search_engine_group); + + Group other_group; + other_group.title = + l10n_util::GetStringUTF16(IDS_SEARCH_ENGINES_EDITOR_OTHER_SEPARATOR); + other_group.id = kOtherGroupID; + groups.push_back(other_group); + + return groups; +} + +int TemplateURLTableModel::GetGroupID(int row) { + DCHECK(row >= 0 && row < RowCount()); + return row < last_search_engine_index_ ? kMainGroupID : kOtherGroupID; +} + +void TemplateURLTableModel::Remove(int index) { + // Remove the observer while we modify the model, that way we don't need to + // worry about the model calling us back when we mutate it. + template_url_model_->RemoveObserver(this); + const TemplateURL* template_url = &GetTemplateURL(index); + + scoped_ptr<ModelEntry> entry(entries_[index]); + entries_.erase(entries_.begin() + index); + if (index < last_search_engine_index_) + last_search_engine_index_--; + if (observer_) + observer_->OnItemsRemoved(index, 1); + + // Make sure to remove from the table model first, otherwise the + // TemplateURL would be freed. + template_url_model_->Remove(template_url); + template_url_model_->AddObserver(this); +} + +void TemplateURLTableModel::Add(int index, TemplateURL* template_url) { + DCHECK(index >= 0 && index <= RowCount()); + ModelEntry* entry = new ModelEntry(this, *template_url); + entries_.insert(entries_.begin() + index, entry); + if (observer_) + observer_->OnItemsAdded(index, 1); + template_url_model_->RemoveObserver(this); + template_url_model_->Add(template_url); + template_url_model_->AddObserver(this); +} + +void TemplateURLTableModel::ModifyTemplateURL(int index, + const string16& title, + const string16& keyword, + const std::string& url) { + DCHECK(index >= 0 && index <= RowCount()); + const TemplateURL* template_url = &GetTemplateURL(index); + template_url_model_->RemoveObserver(this); + template_url_model_->ResetTemplateURL(template_url, title, keyword, url); + if (template_url_model_->GetDefaultSearchProvider() == template_url && + !TemplateURL::SupportsReplacement(template_url)) { + // The entry was the default search provider, but the url has been modified + // so that it no longer supports replacement. Reset the default search + // provider so that it doesn't point to a bogus entry. + template_url_model_->SetDefaultSearchProvider(NULL); + } + template_url_model_->AddObserver(this); + ReloadIcon(index); // Also calls NotifyChanged(). +} + +void TemplateURLTableModel::ReloadIcon(int index) { + DCHECK(index >= 0 && index < RowCount()); + + entries_[index]->ResetIcon(); + + NotifyChanged(index); +} + +const TemplateURL& TemplateURLTableModel::GetTemplateURL(int index) { + return entries_[index]->template_url(); +} + +int TemplateURLTableModel::IndexOfTemplateURL( + const TemplateURL* template_url) { + for (std::vector<ModelEntry*>::iterator i = entries_.begin(); + i != entries_.end(); ++i) { + ModelEntry* entry = *i; + if (&(entry->template_url()) == template_url) + return static_cast<int>(i - entries_.begin()); + } + return -1; +} + +int TemplateURLTableModel::MoveToMainGroup(int index) { + if (index < last_search_engine_index_) + return index; // Already in the main group. + + ModelEntry* current_entry = entries_[index]; + entries_.erase(index + entries_.begin()); + if (observer_) + observer_->OnItemsRemoved(index, 1); + + const int new_index = last_search_engine_index_++; + entries_.insert(entries_.begin() + new_index, current_entry); + if (observer_) + observer_->OnItemsAdded(new_index, 1); + return new_index; +} + +int TemplateURLTableModel::MakeDefaultTemplateURL(int index) { + if (index < 0 || index >= RowCount()) { + NOTREACHED(); + return -1; + } + + const TemplateURL* keyword = &GetTemplateURL(index); + const TemplateURL* current_default = + template_url_model_->GetDefaultSearchProvider(); + if (current_default == keyword) + return -1; + + template_url_model_->RemoveObserver(this); + template_url_model_->SetDefaultSearchProvider(keyword); + template_url_model_->AddObserver(this); + + // The formatting of the default engine is different; notify the table that + // both old and new entries have changed. + if (current_default != NULL) { + int old_index = IndexOfTemplateURL(current_default); + // current_default may not be in the list of TemplateURLs if the database is + // corrupt and the default TemplateURL is used from preferences + if (old_index >= 0) + NotifyChanged(old_index); + } + const int new_index = IndexOfTemplateURL(keyword); + NotifyChanged(new_index); + + // Make sure the new default is in the main group. + return MoveToMainGroup(index); +} + +void TemplateURLTableModel::NotifyChanged(int index) { + if (observer_) { + DCHECK_GE(index, 0); + observer_->OnItemsChanged(index, 1); + } +} + +void TemplateURLTableModel::FavIconAvailable(ModelEntry* entry) { + std::vector<ModelEntry*>::iterator i = + find(entries_.begin(), entries_.end(), entry); + DCHECK(i != entries_.end()); + NotifyChanged(static_cast<int>(i - entries_.begin())); +} + +void TemplateURLTableModel::OnTemplateURLModelChanged() { + Reload(); +} diff --git a/chrome/browser/ui/search_engines/template_url_table_model.h b/chrome/browser/ui/search_engines/template_url_table_model.h new file mode 100644 index 0000000..c002908 --- /dev/null +++ b/chrome/browser/ui/search_engines/template_url_table_model.h @@ -0,0 +1,117 @@ +// Copyright (c) 2011 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_UI_SEARCH_ENGINES_TEMPLATE_URL_TABLE_MODEL_H_ +#define CHROME_BROWSER_UI_SEARCH_ENGINES_TEMPLATE_URL_TABLE_MODEL_H_ +#pragma once + +#include <string> +#include <vector> + +#include "base/compiler_specific.h" +#include "base/string16.h" +#include "chrome/browser/search_engines/template_url_model_observer.h" +#include "ui/base/models/table_model.h" + +class ModelEntry; +class SkBitmap; +class TemplateURL; +class TemplateURLModel; + +// TemplateURLTableModel is the TableModel implementation used by +// KeywordEditorView to show the keywords in a TableView. +// +// TemplateURLTableModel has two columns, the first showing the description, +// the second the keyword. +// +// TemplateURLTableModel maintains a vector of ModelEntrys that correspond to +// each row in the tableview. Each ModelEntry wraps a TemplateURL, providing +// the favicon. The entries in the model are sorted such that non-generated +// appear first (grouped together) and are followed by generated keywords. + +class TemplateURLTableModel : public ui::TableModel, + TemplateURLModelObserver { + public: + explicit TemplateURLTableModel(TemplateURLModel* template_url_model); + + virtual ~TemplateURLTableModel(); + + // Reloads the entries from the TemplateURLModel. This should ONLY be invoked + // if the TemplateURLModel wasn't initially loaded and has been loaded. + void Reload(); + + // ui::TableModel overrides. + virtual int RowCount() OVERRIDE; + virtual string16 GetText(int row, int column) OVERRIDE; + virtual SkBitmap GetIcon(int row) OVERRIDE; + virtual void SetObserver(ui::TableModelObserver* observer) OVERRIDE; + virtual bool HasGroups() OVERRIDE; + virtual Groups GetGroups() OVERRIDE; + virtual int GetGroupID(int row) OVERRIDE; + + // Removes the entry at the specified index. + void Remove(int index); + + // Adds a new entry at the specified index. + void Add(int index, TemplateURL* template_url); + + // Update the entry at the specified index. + void ModifyTemplateURL(int index, + const string16& title, + const string16& keyword, + const std::string& url); + + // Reloads the icon at the specified index. + void ReloadIcon(int index); + + // Returns The TemplateURL at the specified index. + const TemplateURL& GetTemplateURL(int index); + + // Returns the index of the TemplateURL, or -1 if it the TemplateURL is not + // found. + int IndexOfTemplateURL(const TemplateURL* template_url); + + // Moves the keyword at the specified index to be at the end of the main + // group. Returns the new index. If the entry is already in the main group, + // no action is taken, and the current index is returned. + int MoveToMainGroup(int index); + + // Make the TemplateURL at |index| the default. Returns the new index, or -1 + // if the index is invalid or it is already the default. + int MakeDefaultTemplateURL(int index); + + // If there is an observer, it's notified the selected row has changed. + void NotifyChanged(int index); + + TemplateURLModel* template_url_model() const { return template_url_model_; } + + // Returns the index of the last entry shown in the search engines group. + int last_search_engine_index() const { return last_search_engine_index_; } + + private: + friend class ModelEntry; + + // Notification that a model entry has fetched its icon. + void FavIconAvailable(ModelEntry* entry); + + // TemplateURLModelObserver notification. + virtual void OnTemplateURLModelChanged(); + + ui::TableModelObserver* observer_; + + // The entries. + std::vector<ModelEntry*> entries_; + + // The model we're displaying entries from. + TemplateURLModel* template_url_model_; + + // Index of the last search engine in entries_. This is used to determine the + // group boundaries. + int last_search_engine_index_; + + DISALLOW_COPY_AND_ASSIGN(TemplateURLTableModel); +}; + + +#endif // CHROME_BROWSER_UI_SEARCH_ENGINES_TEMPLATE_URL_TABLE_MODEL_H_ |
