diff options
author | hbono@chromium.org <hbono@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-09-05 09:26:08 +0000 |
---|---|---|
committer | hbono@chromium.org <hbono@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-09-05 09:26:08 +0000 |
commit | c8a7266a5d9b9c9ff798def26d7f3bedd4b46713 (patch) | |
tree | 180ed72a3d13604088ce8e8be6b4673d9acd5a39 | |
parent | 5758a947293b9429378f1782f808567ccde38ea5 (diff) | |
download | chromium_src-c8a7266a5d9b9c9ff798def26d7f3bedd4b46713.zip chromium_src-c8a7266a5d9b9c9ff798def26d7f3bedd4b46713.tar.gz chromium_src-c8a7266a5d9b9c9ff798def26d7f3bedd4b46713.tar.bz2 |
Integrate the Spelling service to Chrome.
This change integrates the Spelling service (a.k.a. "Did you mean") to a context-menu of Chrome so we can see the spell-check result retrieved from the Spelling service. This change sends a JSON-RPC request to the service in the background and update a context-menu item while showing to prevent junkiness.
BUG=29191,93746
TEST=right-click a misspelled word and see it is updated.
Review URL: http://codereview.chromium.org/7713033
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@99633 0039d316-1c4b-4281-b951-d872f2087c98
18 files changed, 832 insertions, 7 deletions
diff --git a/chrome/app/chrome_command_ids.h b/chrome/app/chrome_command_ids.h index b3cad45..ee27d73 100644 --- a/chrome/app/chrome_command_ids.h +++ b/chrome/app/chrome_command_ids.h @@ -265,6 +265,7 @@ #define IDC_CONTENT_CONTEXT_LANGUAGE_SETTINGS 50153 #define IDC_CONTENT_CONTEXT_LOOK_UP_IN_DICTIONARY 50154 #define IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS 50155 +#define IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION 50156 // Frame items. #define IDC_CONTENT_CONTEXT_RELOADFRAME 50160 #define IDC_CONTENT_CONTEXT_VIEWFRAMESOURCE 50161 diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd index 3fdbaef..98c3099 100644 --- a/chrome/app/generated_resources.grd +++ b/chrome/app/generated_resources.grd @@ -540,6 +540,12 @@ are declared in build/common.gypi. <message name="IDS_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS" desc="The name of the No Spelling Suggestions display in the content area context menu"> &No spelling suggestions </message> + <message name="IDS_CONTENT_CONTEXT_SPELLING_CHECKING" desc="The place-holder message shown while the Spelling service is checking text"> + Loading spelling suggestion + </message> + <message name="IDS_CONTENT_CONTEXT_SPELLING_CORRECT" desc="The message shown when the selection text is correct."> + No suggestions found + </message> <message name="IDS_CONTENT_CONTEXT_SPELLCHECK_MENU" desc="The name of the Spellcheck Options submenu in the content area context menu"> &Spell-checker options </message> @@ -734,6 +740,12 @@ are declared in build/common.gypi. <message name="IDS_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS" desc="In Title Case: The name of the No Spelling Suggestions display in the content area context menu"> &No Spelling Suggestions </message> + <message name="IDS_CONTENT_CONTEXT_SPELLING_CHECKING" desc="The place-holder message shown while the Spelling service is checking text"> + Loading spelling suggestion + </message> + <message name="IDS_CONTENT_CONTEXT_SPELLING_CORRECT" desc="The message shown when the selection text is correct."> + No suggestions found + </message> <message name="IDS_CONTENT_CONTEXT_SPELLCHECK_MENU" desc="In Title Case: The name of the Spellcheck Options submenu in the content area context menu"> &Spell-checker Options </message> @@ -9001,7 +9013,9 @@ Keep your key file in a safe place. You will need it to create new versions of y <message name="IDS_OPTIONS_SUGGEST_PREF" desc="The documentation string of the 'Use Suggest' preference"> Use a prediction service to help complete searches and URLs typed in the address bar </message> - + <message name="IDS_OPTIONS_SPELLING_PREF" desc="The documentation string of the 'Use Spelling' preference"> + Use a web service to help resolve spelling errors + </message> <message name="IDS_OPTIONS_ENABLE_LOGGING" desc="The label of the checkbox to enable/disable crash and user metrics logging"> Automatically send usage statistics and crash reports to Google </message> diff --git a/chrome/browser/profiles/profile.cc b/chrome/browser/profiles/profile.cc index 997c966..22d1e85 100644 --- a/chrome/browser/profiles/profile.cc +++ b/chrome/browser/profiles/profile.cc @@ -129,6 +129,13 @@ void Profile::RegisterUserPrefs(PrefService* prefs) { prefs->RegisterLocalizedStringPref(prefs::kSpellCheckDictionary, IDS_SPELLCHECK_DICTIONARY, PrefService::UNSYNCABLE_PREF); + prefs->RegisterBooleanPref(prefs::kSpellCheckUseSpellingService, +#if defined(GOOGLE_CHROME_BUILD) + true, +#else + false, +#endif + PrefService::UNSYNCABLE_PREF); prefs->RegisterBooleanPref(prefs::kEnableSpellCheck, true, PrefService::SYNCABLE_PREF); diff --git a/chrome/browser/tab_contents/render_view_context_menu.cc b/chrome/browser/tab_contents/render_view_context_menu.cc index 77ba463..3f24801 100644 --- a/chrome/browser/tab_contents/render_view_context_menu.cc +++ b/chrome/browser/tab_contents/render_view_context_menu.cc @@ -37,6 +37,7 @@ #include "chrome/browser/spellchecker/spellcheck_host.h" #include "chrome/browser/spellchecker/spellcheck_host_metrics.h" #include "chrome/browser/spellchecker/spellchecker_platform_engine.h" +#include "chrome/browser/tab_contents/spelling_menu_observer.h" #include "chrome/browser/translate/translate_manager.h" #include "chrome/browser/translate/translate_prefs.h" #include "chrome/browser/translate/translate_tab_helper.h" @@ -552,6 +553,27 @@ void RenderViewContextMenu::InitMenu() { #endif } + if (params_.is_editable) { + // Add a menu item that shows suggestions from the Spelling service. + const CommandLine& command_line = *CommandLine::ForCurrentProcess(); + if (command_line.HasSwitch(switches::kExperimentalSpellcheckerFeatures)) { + PrefService* pref = profile_->GetPrefs(); + bool use_spelling_service = + pref && pref->GetBoolean(prefs::kSpellCheckUseSpellingService); + if (use_spelling_service) { + if (!spelling_menu_observer_.get()) + spelling_menu_observer_.reset(new SpellingMenuObserver(this)); + + if (spelling_menu_observer_.get()) + observers_.AddObserver(spelling_menu_observer_.get()); + } + } + } + + // Ask our observers to add their menu items. + FOR_EACH_OBSERVER(RenderViewContextMenuObserver, observers_, + InitMenu(params_)); + if (params_.is_editable) AppendEditableItems(); else if (has_selection) @@ -571,6 +593,27 @@ void RenderViewContextMenu::LookUpInDictionary() { NOTREACHED(); } +void RenderViewContextMenu::AddMenuItem(int command_id, + const string16& title) { + menu_model_.AddItem(command_id, title); +} + +void RenderViewContextMenu::UpdateMenuItem(int command_id, + bool enabled, + const string16& label) { + // This function needs platform-specific implementation. + NOTIMPLEMENTED(); +} + + +RenderViewHost* RenderViewContextMenu::GetRenderViewHost() const { + return source_tab_contents_->render_view_host(); +} + +Profile* RenderViewContextMenu::GetProfile() const { + return profile_; +} + bool RenderViewContextMenu::AppendCustomItems() { size_t total_items = 0; AddCustomItemsToMenu(params_.custom_items, 0, &total_items, this, @@ -794,7 +837,7 @@ void RenderViewContextMenu::AppendEditableItems() { } // If word is misspelled, give option for "Add to dictionary" - if (!params_.misspelled_word.empty()) { + if (!spelling_menu_observer_.get() && !params_.misspelled_word.empty()) { if (params_.dictionary_suggestions.empty()) { menu_model_.AddItem(IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS, l10n_util::GetStringUTF16( @@ -961,6 +1004,15 @@ ExtensionMenuItem* RenderViewContextMenu::GetExtensionMenuItem(int id) const { // Menu delegate functions ----------------------------------------------------- bool RenderViewContextMenu::IsCommandIdEnabled(int id) const { + // If this command is is added by one of our observers, we dispatch it to the + // observer. + ObserverListBase<RenderViewContextMenuObserver>::Iterator it(observers_); + RenderViewContextMenuObserver* observer; + while ((observer = it.GetNext()) != NULL) { + if (observer->IsCommandIdSupported(id)) + return observer->IsCommandIdEnabled(id); + } + if (id == IDC_PRINT && (source_tab_contents_->content_restrictions() & CONTENT_RESTRICTION_PRINT)) { @@ -1329,6 +1381,15 @@ bool RenderViewContextMenu::IsCommandIdChecked(int id) const { } void RenderViewContextMenu::ExecuteCommand(int id) { + // If this command is is added by one of our observers, we dispatch it to the + // observer. + ObserverListBase<RenderViewContextMenuObserver>::Iterator it(observers_); + RenderViewContextMenuObserver* observer; + while ((observer = it.GetNext()) != NULL) { + if (observer->IsCommandIdSupported(id)) + return observer->ExecuteCommand(id); + } + // Check to see if one of the spell check language ids have been clicked. if (id >= IDC_SPELLCHECK_LANGUAGES_FIRST && id < IDC_SPELLCHECK_LANGUAGES_LAST) { diff --git a/chrome/browser/tab_contents/render_view_context_menu.h b/chrome/browser/tab_contents/render_view_context_menu.h index 65bb3dd..513ab0a 100644 --- a/chrome/browser/tab_contents/render_view_context_menu.h +++ b/chrome/browser/tab_contents/render_view_context_menu.h @@ -12,9 +12,11 @@ #include "base/memory/scoped_ptr.h" #include "base/memory/scoped_vector.h" +#include "base/observer_list.h" #include "base/string16.h" #include "chrome/browser/custom_handlers/protocol_handler_registry.h" #include "chrome/browser/extensions/extension_menu_manager.h" +#include "chrome/browser/tab_contents/render_view_context_menu_observer.h" #include "content/common/page_transition_types.h" #include "ui/base/models/simple_menu_model.h" #include "webkit/glue/context_menu.h" @@ -22,7 +24,9 @@ class ExtensionMenuItem; class Profile; +class RenderViewHost; class TabContents; +class SpellingMenuObserver; namespace gfx { class Point; @@ -32,7 +36,74 @@ namespace WebKit { struct WebMediaPlayerAction; } -class RenderViewContextMenu : public ui::SimpleMenuModel::Delegate { +// An interface that controls a RenderViewContextMenu instance from observers. +// This interface is designed mainly for controlling the instance while showing +// so we can add a context-menu item that takes long time to create its text, +// such as retrieving the item text from a server. The simplest usage is: +// 1. Adding an item with temporary text; +// 2. Posting a background task that creates the item text, and; +// 3. Calling UpdateMenuItem() in the callback function. +// The following snippet describes the simple usage that updates a context-menu +// item with this interface. +// +// class MyTask : public URLFetcher::Delegate { +// public: +// MyTask(RenderViewContextMenuProxy* proxy, int id) +// : proxy_(proxy), +// id_(id) { +// } +// virtual ~MyTask() { +// } +// virtual void OnURLFetchComplete(const URLFetcher* source, +// const GURL& url, +// const net::URLRequestStatus& status, +// int response, +// const net::ResponseCookies& cookies, +// const std::string& data) { +// bool enabled = response == 200; +// const char* text = enabled ? "OK" : "ERROR"; +// proxy_->UpdateMenuItem(id_, enabled, ASCIIToUTF16(text)); +// } +// void Start(const GURL* url, net::URLRequestContextGetter* context) { +// fetcher_.reset(new URLFetcher(url, URLFetcher::GET, this)); +// fetcher_->set_request_context(context); +// fetcher_->Start(); +// } +// +// private: +// URLFetcher fetcher_; +// RenderViewContextMenuProxy* proxy_; +// int id_; +// }; +// +// void RenderViewContextMenu::AppendEditableItems() { +// // Add a menu item with temporary text shown while we create the final +// // text. +// menu_model_.AddItemWithStringId(IDC_MY_ITEM, IDC_MY_TEXT); +// +// // Start a task that creates the final text. +// my_task_ = new MyTask(this, IDC_MY_ITEM); +// my_task_->Start(...); +// } +// +class RenderViewContextMenuProxy { + public: + // Add a menu item to a context menu. + virtual void AddMenuItem(int command_id, const string16& title) = 0; + + // Update the status and text of the specified context-menu item. + virtual void UpdateMenuItem(int command_id, + bool enabled, + const string16& title) = 0; + + // Retrieve the RenderViewHost (or Profile) instance associated with a context + // menu, respectively. + virtual RenderViewHost* GetRenderViewHost() const = 0; + virtual Profile* GetProfile() const = 0; +}; + +class RenderViewContextMenu : public ui::SimpleMenuModel::Delegate, + public RenderViewContextMenuProxy { public: static const size_t kMaxExtensionItemTitleLength; static const size_t kMaxSelectionTextLength; @@ -55,6 +126,14 @@ class RenderViewContextMenu : public ui::SimpleMenuModel::Delegate { virtual void MenuWillShow(ui::SimpleMenuModel* source) OVERRIDE; virtual void MenuClosed(ui::SimpleMenuModel* source) OVERRIDE; + // RenderViewContextMenuDelegate implementation. + virtual void AddMenuItem(int command_id, const string16& title) OVERRIDE; + virtual void UpdateMenuItem(int command_id, + bool enabled, + const string16& title) OVERRIDE; + virtual RenderViewHost* GetRenderViewHost() const; + virtual Profile* GetProfile() const; + protected: void InitMenu(); @@ -170,6 +249,12 @@ class RenderViewContextMenu : public ui::SimpleMenuModel::Delegate { ScopedVector<ui::SimpleMenuModel> extension_menu_models_; scoped_refptr<ProtocolHandlerRegistry> protocol_handler_registry_; + // An observer that handles a spelling-menu items. + scoped_ptr<SpellingMenuObserver> spelling_menu_observer_; + + // Our observers. + mutable ObserverList<RenderViewContextMenuObserver> observers_; + DISALLOW_COPY_AND_ASSIGN(RenderViewContextMenu); }; diff --git a/chrome/browser/tab_contents/render_view_context_menu_gtk.cc b/chrome/browser/tab_contents/render_view_context_menu_gtk.cc index 569bf53..24a4f99 100644 --- a/chrome/browser/tab_contents/render_view_context_menu_gtk.cc +++ b/chrome/browser/tab_contents/render_view_context_menu_gtk.cc @@ -7,6 +7,7 @@ #include <gtk/gtk.h> #include "base/string_util.h" +#include "base/utf_string_conversions.h" #include "chrome/app/chrome_command_ids.h" #include "chrome/browser/ui/gtk/gtk_util.h" #include "content/browser/renderer_host/render_widget_host_view_gtk.h" @@ -16,6 +17,60 @@ #include "ui/gfx/gtk_util.h" #include "webkit/glue/context_menu.h" +namespace { + +// A callback function for gtk_container_foreach(). This callback just checks +// the menu ID and set the given user data if it is same as the specified ID. +struct GtkWidgetAtParam { + int index; + GtkWidget* widget; +}; + +void GtkWidgetAt(GtkWidget* widget, gpointer user_data) { + GtkWidgetAtParam* param = reinterpret_cast<GtkWidgetAtParam*>(user_data); + + gpointer data = g_object_get_data(G_OBJECT(widget), "menu-id"); + if (data && (GPOINTER_TO_INT(data) - 1) == param->index && + GTK_IS_MENU_ITEM(widget)) { + param->widget = widget; + } +} + +// Retrieves a GtkWidget which has the specified command_id. This function +// traverses the given |model| in the depth-first order. When this function +// finds an item whose command_id is the same as the given |command_id|, it +// returns the GtkWidget associated with the item. This function emulates +// views::MenuItemViews::GetMenuItemByID() for GTK. +GtkWidget* GetMenuItemByID(ui::MenuModel* model, + GtkWidget* menu, + int command_id) { + if (!menu) + return NULL; + + for (int i = 0; i < model->GetItemCount(); ++i) { + if (model->GetCommandIdAt(i) == command_id) { + GtkWidgetAtParam param; + param.index = i; + param.widget = NULL; + gtk_container_foreach(GTK_CONTAINER(menu), &GtkWidgetAt, ¶m); + return param.widget; + } + + ui::MenuModel* submenu = model->GetSubmenuModelAt(i); + if (submenu) { + GtkWidget* subitem = GetMenuItemByID( + submenu, + gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu)), + command_id); + if (subitem) + return subitem; + } + } + return NULL; +} + +} // namespace + RenderViewContextMenuGtk::RenderViewContextMenuGtk( TabContents* web_contents, const ContextMenuParams& params, @@ -70,3 +125,16 @@ bool RenderViewContextMenuGtk::AlwaysShowIconForCmd(int command_id) const { return command_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST && command_id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST; } + +void RenderViewContextMenuGtk::UpdateMenuItem(int command_id, + bool enabled, + const string16& title) { + GtkWidget* item = GetMenuItemByID(&menu_model_, menu_gtk_->widget(), + command_id); + if (!item || !GTK_IS_MENU_ITEM(item)) + return; + + // Enable (or disable) the menu item and updates its text. + gtk_widget_set_sensitive(item, enabled); + gtk_menu_item_set_label(GTK_MENU_ITEM(item), UTF16ToUTF8(title).c_str()); +} diff --git a/chrome/browser/tab_contents/render_view_context_menu_gtk.h b/chrome/browser/tab_contents/render_view_context_menu_gtk.h index 11cc91c..12862f3 100644 --- a/chrome/browser/tab_contents/render_view_context_menu_gtk.h +++ b/chrome/browser/tab_contents/render_view_context_menu_gtk.h @@ -28,6 +28,11 @@ class RenderViewContextMenuGtk : public RenderViewContextMenu, // Menu::Delegate implementation --------------------------------------------- virtual bool AlwaysShowIconForCmd(int command_id) const; + // RenderViewContextMenuDelegate implementation ------------------------------ + virtual void UpdateMenuItem(int command_id, + bool enabled, + const string16& title) OVERRIDE; + protected: // RenderViewContextMenu implementation -------------------------------------- virtual void PlatformInit(); diff --git a/chrome/browser/tab_contents/render_view_context_menu_mac.h b/chrome/browser/tab_contents/render_view_context_menu_mac.h index 5c52a5b..bc6ced6 100644 --- a/chrome/browser/tab_contents/render_view_context_menu_mac.h +++ b/chrome/browser/tab_contents/render_view_context_menu_mac.h @@ -25,6 +25,11 @@ class RenderViewContextMenuMac : public RenderViewContextMenu { virtual ~RenderViewContextMenuMac(); virtual void ExecuteCommand(int id); + // RenderViewContextMenuDelegate implementation. + virtual void UpdateMenuItem(int command_id, + bool enabled, + const string16& title) OVERRIDE; + protected: // RenderViewContextMenu implementation- virtual void PlatformInit(); diff --git a/chrome/browser/tab_contents/render_view_context_menu_mac.mm b/chrome/browser/tab_contents/render_view_context_menu_mac.mm index f7c52be..6efeec3 100644 --- a/chrome/browser/tab_contents/render_view_context_menu_mac.mm +++ b/chrome/browser/tab_contents/render_view_context_menu_mac.mm @@ -14,6 +14,35 @@ #import "content/common/chrome_application_mac.h" #include "grit/generated_resources.h" +namespace { + +// Retrieves an NSMenuItem which has the specified command_id. This function +// traverses the given |model| in the depth-first order. When this function +// finds an item whose command_id is the same as the given |command_id|, it +// returns the NSMenuItem associated with the item. This function emulates +// views::MenuItemViews::GetMenuItemByID() for Mac. +NSMenuItem* GetMenuItemByID(ui::MenuModel* model, + NSMenu* menu, + int command_id) { + for (int i = 0; i < model->GetItemCount(); ++i) { + NSMenuItem* item = [menu itemAtIndex:i]; + if (model->GetCommandIdAt(i) == command_id) + return item; + + ui::MenuModel* submenu = model->GetSubmenuModelAt(i); + if (submenu && [item hasSubmenu]) { + NSMenuItem* subitem = GetMenuItemByID(submenu, + [item submenu], + command_id); + if (subitem) + return subitem; + } + } + return nil; +} + +} // namespace + // Obj-C bridge class that is the target of all items in the context menu. // Relies on the tag being set to the command id. @@ -111,3 +140,17 @@ void RenderViewContextMenuMac::LookUpInDictionary() { if (ok) NSPerformService(@"Look Up in Dictionary", pboard); } + +void RenderViewContextMenuMac::UpdateMenuItem(int command_id, + bool enabled, + const string16& title) { + NSMenuItem* item = GetMenuItemByID(&menu_model_, [menuController_ menu], + command_id); + if (!item) + return; + + // Update the returned NSMenuItem directly so we can update it immediately. + [item setEnabled:enabled]; + [item setTitle:SysUTF16ToNSString(title)]; + [[item menu] itemChanged:item]; +} diff --git a/chrome/browser/tab_contents/render_view_context_menu_observer.cc b/chrome/browser/tab_contents/render_view_context_menu_observer.cc new file mode 100644 index 0000000..b6311c6 --- /dev/null +++ b/chrome/browser/tab_contents/render_view_context_menu_observer.cc @@ -0,0 +1,19 @@ +// 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/tab_contents/render_view_context_menu_observer.h" + +void RenderViewContextMenuObserver::InitMenu(const ContextMenuParams& params) { +} + +bool RenderViewContextMenuObserver::IsCommandIdSupported(int command_id) { + return false; +} + +bool RenderViewContextMenuObserver::IsCommandIdEnabled(int command_id) { + return false; +} + +void RenderViewContextMenuObserver::ExecuteCommand(int command_id) { +} diff --git a/chrome/browser/tab_contents/render_view_context_menu_observer.h b/chrome/browser/tab_contents/render_view_context_menu_observer.h new file mode 100644 index 0000000..34823e8 --- /dev/null +++ b/chrome/browser/tab_contents/render_view_context_menu_observer.h @@ -0,0 +1,102 @@ +// 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_TAB_CONTENTS_RENDER_VIEW_CONTEXT_MENU_OBSERVER_H_ +#define CHROME_BROWSER_TAB_CONTENTS_RENDER_VIEW_CONTEXT_MENU_OBSERVER_H_ +#pragma once + +struct ContextMenuParams; + +// The interface used for implementing context-menu items. The following +// instruction describe how to implement a context-menu item with this +// interface. +// +// 1. Add command IDs for the context-menu items to 'chrome_command_ids.h'. +// +// #define IDC_MY_COMMAND 99999 +// +// 2. Add strings for the context-menu items to 'generated_sources.grd'. +// +// <message name="IDS_MY_COMMAND" desc="..."> +// My command +// </message> +// +// 3. Create a class that implements this interface. (It is a good idea to use +// the RenderViewContextMenuDelegate interface to avoid accessing the +// RenderViewContextMenu class directly.) +// +// class MyMenuObserver : public RenderViewContextMenuObserver { +// public: +// MyMenuObserver(RenderViewContextMenuDelegate* d); +// ~MyMenuObserver(); +// +// virtual void InitMenu(const ContextMenuParams& params) OVERRIDE; +// virtual bool IsCommandIdSupported(int command_id) OVERRIDE; +// virtual bool IsCommandIdEnabled(int command_id) OVERRIDE; +// virtual void ExecuteCommand(int command_id) OVERRIDE; +// +// private: +// RenderViewContextMenuDelgate* delegate_; +// } +// +// void MyMenuObserver::InitMenu(const ContextMenuParams& params) { +// delegate_->AddMenuItem(IDC_MY_COMMAND,...); +// } +// +// bool MyMenuObserver::IsCommandIdSupported(int command_id) { +// return command_id == IDC_MY_COMMAND; +// } +// +// bool MyMenuObserver::IsCommandIdEnabled(int command_id) { +// DCHECK(command_id == IDC_MY_COMMAND); +// return true; +// } +// +// void MyMenuObserver::ExecuteCommand(int command_id) { +// DCHECK(command_id == IDC_MY_COMMAND); +// } +// +// 4. Add this observer class to the RenderViewContextMenu class. (It is good +// to use scoped_ptr<> so Chrome can create its instances only when it needs.) +// +// class RenderViewContextMenu { +// ... +// private: +// scoped_ptr<MyMenuObserver> my_menu_observer_; +// }; +// +// 5. Create its instance in InitMenu() and add it to the observer list of the +// RenderViewContextMenu class. +// +// void RenderViewContextMenu::InitMenu() { +// ... +// my_menu_observer_.reset(new MyMenuObserver(this)); +// observers_.AddObserver(my_menu_observer_.get()); +// } +// +// +class RenderViewContextMenuObserver { + public: + virtual ~RenderViewContextMenuObserver() {} + + // Called when the RenderViewContextMenu class initializes a context menu. We + // usually call RenderViewContextMenuDelegate::AddMenuItem() to add menu items + // in this function. + virtual void InitMenu(const ContextMenuParams& params); + + // Called when the RenderViewContextMenu class asks whether an observer + // listens for the specified command ID. If this function returns true, the + // RenderViewContextMenu class calls IsCommandIdEnabled() or ExecuteCommand(). + virtual bool IsCommandIdSupported(int command_id); + + // Called when the RenderViewContextMenu class sets the initial status of the + // specified context-menu item. If we need to enable or disable a context-menu + // item while showing, use RenderViewContextMenuDelegate::UpdateMenuItem(). + virtual bool IsCommandIdEnabled(int command_id); + + // Called when a user selects the specified context-menu item. + virtual void ExecuteCommand(int command_id); +}; + +#endif // CHROME_BROWSER_TAB_CONTENTS_RENDER_VIEW_CONTEXT_MENU_OBSERVER_H_ diff --git a/chrome/browser/tab_contents/spelling_menu_observer.cc b/chrome/browser/tab_contents/spelling_menu_observer.cc new file mode 100644 index 0000000..0a77f77 --- /dev/null +++ b/chrome/browser/tab_contents/spelling_menu_observer.cc @@ -0,0 +1,276 @@ +// 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/tab_contents/spelling_menu_observer.h" + +#include <string> + +#include "base/json/json_reader.h" +#include "base/json/string_escape.h" +#include "base/stringprintf.h" +#include "base/utf_string_conversions.h" +#include "base/values.h" +#include "chrome/app/chrome_command_ids.h" +#include "chrome/browser/prefs/pref_service.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/tab_contents/render_view_context_menu.h" +#include "chrome/common/pref_names.h" +#include "content/browser/renderer_host/render_view_host.h" +#include "googleurl/src/gurl.h" +#include "grit/generated_resources.h" +#include "ui/base/l10n/l10n_util.h" +#include "unicode/uloc.h" +#include "webkit/glue/context_menu.h" + +#if defined(GOOGLE_CHROME_BUILD) +#include "chrome/browser/spellchecker/internal/spellcheck_internal.h" +#else +// Use a dummy URL and a key on Chromium to avoid build breaks until the +// Spelling API is released. These dummy parameters just cause a timeout and +// show 'no suggestions found'. +#define SPELLING_SERVICE_KEY +#define SPELLING_SERVICE_URL "http://127.0.0.1/rpc" +#endif + +SpellingMenuObserver::SpellingMenuObserver(RenderViewContextMenuProxy* proxy) + : proxy_(proxy), + loading_frame_(0), + succeeded_(false) { +} + +SpellingMenuObserver::~SpellingMenuObserver() { +} + +void SpellingMenuObserver::InitMenu(const ContextMenuParams& params) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + + // Exit if we are not in an editable element because we add a menu item only + // for editable elements. + if (!params.is_editable) + return; + + Profile* profile = proxy_->GetProfile(); + if (!profile || !profile->GetRequestContext()) + return; + + // Retrieve the misspelled word to be sent to the Spelling service. + string16 text = params.misspelled_word; + if (text.empty()) + return; + + // Initialize variables used in OnURLFetchComplete(). We copy the input text + // to the result text so we can replace its misspelled regions with + // suggestions. + loading_frame_ = 0; + succeeded_ = false; + result_ = text; + + // Add a placeholder item. This item will be updated when we receive a + // response from the Spelling service. (We do not have to disable this item + // now since Chrome will call IsCommandIdEnabled() and disable it.) + loading_message_ = + l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_CHECKING); + proxy_->AddMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, + loading_message_); + + // Invoke a JSON-RPC call to the Spelling service in the background so we can + // update the placeholder item when we receive its response. It also starts + // the animation timer so we can show animation until we receive it. + const PrefService* pref = profile->GetPrefs(); + std::string language = + pref ? pref->GetString(prefs::kSpellCheckDictionary) : "en-US"; + Invoke(text, language, profile->GetRequestContext()); +} + +bool SpellingMenuObserver::IsCommandIdSupported(int command_id) { + return command_id == IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION; +} + +bool SpellingMenuObserver::IsCommandIdEnabled(int command_id) { + return command_id == IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION && succeeded_; +} + +void SpellingMenuObserver::ExecuteCommand(int command_id) { + if (IsCommandIdEnabled(command_id)) + proxy_->GetRenderViewHost()->Replace(result_); +} + +bool SpellingMenuObserver::Invoke(const string16& text, + const std::string& locale, + net::URLRequestContextGetter* context) { + // Create the parameters needed by Spelling API. Spelling API needs three + // parameters: ISO language code, ISO3 country code, and text to be checked by + // the service. On the other hand, Chrome uses an ISO locale ID and it may + // not include a country ID, e.g. "fr", "de", etc. To create the input + // parameters, we convert the UI locale to a full locale ID, and convert the + // full locale ID to an ISO language code and and ISO3 country code. Also, we + // convert the given text to a JSON string, i.e. quote all its non-ASCII + // characters. + UErrorCode error = U_ZERO_ERROR; + char id[ULOC_LANG_CAPACITY + ULOC_SCRIPT_CAPACITY + ULOC_COUNTRY_CAPACITY]; + uloc_addLikelySubtags(locale.c_str(), id, arraysize(id), &error); + + error = U_ZERO_ERROR; + char language[ULOC_LANG_CAPACITY]; + uloc_getLanguage(id, language, arraysize(language), &error); + + const char* country = uloc_getISO3Country(id); + + std::string encoded_text; + base::JsonDoubleQuote(text, false, &encoded_text); + + // Format the JSON request to be sent to the Spelling service. + static const char kSpellingRequest[] = + "{" + "\"method\":\"spelling.check\"," + "\"apiVersion\":\"v1\"," + "\"params\":{" + "\"text\":\"%s\"," + "\"language\":\"%s\"," + "\"origin_country\":\"%s\"," + "\"key\":\"" SPELLING_SERVICE_KEY "\"" + "}" + "}"; + std::string request = base::StringPrintf(kSpellingRequest, + encoded_text.c_str(), + language, country); + + static const char kSpellingServiceURL[] = SPELLING_SERVICE_URL; + GURL url = GURL(kSpellingServiceURL); + fetcher_.reset(new URLFetcher(url, URLFetcher::POST, this)); + fetcher_->set_request_context(context); + fetcher_->set_upload_data("application/json", request); + fetcher_->Start(); + + animation_timer_.Start(FROM_HERE, base::TimeDelta::FromSeconds(1), + this, &SpellingMenuObserver::OnAnimationTimerExpired); + + return true; +} + +void SpellingMenuObserver::OnURLFetchComplete( + const URLFetcher* source, + const GURL& url, + const net::URLRequestStatus& status, + int response, + const net::ResponseCookies& cookies, + const std::string& data) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + + fetcher_.reset(); + animation_timer_.Stop(); + + // Parse the response JSON and replace misspelled words in the |result_| text + // with their suggestions. + succeeded_ = ParseResponse(response, data); + if (!succeeded_) + result_ = l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_CORRECT); + + // Update the menu item with the result text. We enable this item only when + // the request text has misspelled words. (We disable this item not only when + // we receive a server error but also when the input text consists only of + // well-spelled words. For either case, we do not need to replace the input + // text.) + proxy_->UpdateMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, succeeded_, + result_); +} + +bool SpellingMenuObserver::ParseResponse(int response, + const std::string& data) { + // When this JSON-RPC call finishes successfully, the Spelling service returns + // an JSON object listed below. + // * result - an envelope object representing the result from the APIARY + // server, which is the JSON-API front-end for the Spelling service. This + // object consists of the following variable: + // - spellingCheckResponse (SpellingCheckResponse). + // * SpellingCheckResponse - an object representing the result from the + // Spelling service. This object consists of the following variable: + // - misspellings (optional array of Misspelling) + // * Misspelling - an object representing a misspelling region and its + // suggestions. This object consists of the following variables: + // - charStart (number) - the beginning of the misspelled region; + // - charLength (number) - the length of the misspelled region; + // - suggestions (array of string) - the suggestions for the misspelling + // text, and; + // - canAutoCorrect (optional boolean) - whether we can use the first + // suggestion for auto-correction. + // For example, the Spelling service returns the following JSON when we send a + // spelling request for "duck goes quisk" as of 16 August, 2011. + // { + // "result": { + // "spellingCheckResponse": { + // "misspellings": [{ + // "charStart": 10, + // "charLength": 5, + // "suggestions": [{ "suggestion": "quack" }], + // "canAutoCorrect": false + // }] + // } + // } + + // When a server error happened in the APIARY server (including when we cannot + // get any responses from the server), it returns an HTTP error. + if ((response / 100) != 2) + return false; + + scoped_ptr<DictionaryValue> value( + static_cast<DictionaryValue*>(base::JSONReader::Read(data, true))); + if (!value.get() || !value->IsType(base::Value::TYPE_DICTIONARY)) + return false; + + // Retrieve the array of Misspelling objects. When the APIARY service failed + // calling the Spelling service, it returns a JSON representing the service + // error. (In this case, its HTTP status is 200.) We just return false for + // this case. + ListValue* misspellings = NULL; + const char kMisspellings[] = "result.spellingCheckResponse.misspellings"; + if (!value->GetList(kMisspellings, &misspellings)) + return false; + + // For each misspelled region, we replace it with its first suggestion. + for (size_t i = 0; i < misspellings->GetSize(); ++i) { + // Retrieve the i-th misspelling region, which represents misspelling text + // in the request text and its alternative. + DictionaryValue* misspelling = NULL; + if (!misspellings->GetDictionary(i, &misspelling)) + return false; + + int start = 0; + int length = 0; + ListValue* suggestions = NULL; + if (!misspelling->GetInteger("charStart", &start) || + !misspelling->GetInteger("charLength", &length) || + !misspelling->GetList("suggestions", &suggestions)) { + return false; + } + + // Retrieve the alternative text and replace the misspelling region with the + // alternative. + DictionaryValue* suggestion = NULL; + string16 text; + if (!suggestions->GetDictionary(0, &suggestion) || + !suggestion->GetString("suggestion", &text)) { + return false; + } + result_.replace(start, length, text); + } + + return true; +} + +void SpellingMenuObserver::OnAnimationTimerExpired() { + if (!fetcher_.get()) + return; + + // Append '.' characters to the end of "Checking". + loading_frame_ = (loading_frame_ + 1) & 3; + string16 loading_message = loading_message_; + for (int i = 0; i < loading_frame_; ++i) + loading_message.push_back('.'); + + // Update the menu item with the text. We disable this item to prevent users + // from selecting it. + proxy_->UpdateMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, false, + loading_message); +} diff --git a/chrome/browser/tab_contents/spelling_menu_observer.h b/chrome/browser/tab_contents/spelling_menu_observer.h new file mode 100644 index 0000000..60e7a94 --- /dev/null +++ b/chrome/browser/tab_contents/spelling_menu_observer.h @@ -0,0 +1,106 @@ +// 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_TAB_CONTENTS_SPELLING_MENU_OBSERVER_H_ +#define CHROME_BROWSER_TAB_CONTENTS_SPELLING_MENU_OBSERVER_H_ +#pragma once + +#include <string> + +#include "base/memory/scoped_ptr.h" +#include "base/string16.h" +#include "base/timer.h" +#include "chrome/browser/tab_contents/render_view_context_menu_observer.h" +#include "content/common/url_fetcher.h" + +class GURL; +class RenderViewContextMenuProxy; + +// An observer that listens to events from the RenderViewContextMenu class and +// shows suggestions from the Spelling ("do you mean") service to a context menu +// while we show it. This class implements two interfaces: +// * RenderViewContextMenuObserver +// This interface is used for adding a menu item and update it while showing. +// * URLFetcher::Delegate +// This interface is used for sending a JSON_RPC request to the Spelling +// service and retrieving its response. +// These interfaces allow this class to make a JSON-RPC call to the Spelling +// service in the background and update the context menu while showing. The +// following snippet describes how to add this class to the observer list of the +// RenderViewContextMenu class. +// +// void RenderViewContextMenu::InitMenu() { +// spelling_menu_observer_.reset(new SpellingMenuObserver(this)); +// if (spelling_menu_observer_.get()) +// observers_.AddObserver(spelling_menu_observer.get()); +// } +// +class SpellingMenuObserver : public RenderViewContextMenuObserver, + public URLFetcher::Delegate { + public: + explicit SpellingMenuObserver(RenderViewContextMenuProxy* proxy); + virtual ~SpellingMenuObserver(); + + // RenderViewContextMenuObserver implementation. + virtual void InitMenu(const ContextMenuParams& params) OVERRIDE; + virtual bool IsCommandIdSupported(int command_id) OVERRIDE; + virtual bool IsCommandIdEnabled(int command_id) OVERRIDE; + virtual void ExecuteCommand(int command_id) OVERRIDE; + + // URLFetcher::Delegate implementation. + virtual void OnURLFetchComplete(const URLFetcher* source, + const GURL& url, + const net::URLRequestStatus& status, + int response_code, + const net::ResponseCookies& cookies, + const std::string& data) OVERRIDE; + + private: + // Invokes a JSON-RPC call in the background. This function sends a JSON-RPC + // request to the Spelling service. Chrome will call OnURLFetchComplete() when + // it receives a response from the service. + bool Invoke(const string16& text, + const std::string& locale, + net::URLRequestContextGetter* context); + + // Parses the specified response from the Spelling service. + bool ParseResponse(int code, const std::string& data); + + // The callback function for base::RepeatingTimer<SpellingMenuClient>. This + // function updates the "loading..." animation in the context-menu item. + void OnAnimationTimerExpired(); + + // The interface to add a context-menu item and update it. This class uses + // this interface to avoid accesing context-menu items directly. + RenderViewContextMenuProxy* proxy_; + + // The string used for animation until we receive a response from the Spelling + // service. The current animation just adds periods at the end of this string: + // 'Loading' -> 'Loading.' -> 'Loading..' -> 'Loading...' (-> 'Loading') + string16 loading_message_; + int loading_frame_; + + // A flag represending whether this call finished successfully. This means we + // receive an empty JSON string or a JSON string that consists of misspelled + // words. ('spelling_menu_observer.cc' describes its format.) + bool succeeded_; + + // The string representing the result of this call. This string is a + // suggestion when this call finished successfully. Otherwise it is error + // text. Until we receive a response from the Spelling service, this string + // stores the input string. (Since the Spelling service sends only misspelled + // words, we replace these misspelled words in the input text with the + // suggested words to create suggestion text. + string16 result_; + + // The URLFetcher object used for sending a JSON-RPC request. + scoped_ptr<URLFetcher> fetcher_; + + // A timer used for loading animation. + base::RepeatingTimer<SpellingMenuObserver> animation_timer_; + + DISALLOW_COPY_AND_ASSIGN(SpellingMenuObserver); +}; + +#endif // CHROME_BROWSER_TAB_CONTENTS_SPELLING_MENU_OBSERVER_H_ diff --git a/chrome/browser/ui/views/tab_contents/render_view_context_menu_views.cc b/chrome/browser/ui/views/tab_contents/render_view_context_menu_views.cc index cdf0ba9..efd7593 100644 --- a/chrome/browser/ui/views/tab_contents/render_view_context_menu_views.cc +++ b/chrome/browser/ui/views/tab_contents/render_view_context_menu_views.cc @@ -110,3 +110,20 @@ bool RenderViewContextMenuViews::GetAcceleratorForCommandId( return false; } } + +void RenderViewContextMenuViews::UpdateMenuItem(int command_id, + bool enabled, + const string16& title) { + views::MenuItemView* item = menu_->GetMenuItemByID(command_id); + if (!item) + return; + + item->SetEnabled(enabled); + item->SetTitle(UTF16ToWide(title)); + + views::MenuItemView* parent = item->GetParentMenuItem(); + if (!parent) + return; + + parent->ChildrenChanged(); +} diff --git a/chrome/browser/ui/views/tab_contents/render_view_context_menu_views.h b/chrome/browser/ui/views/tab_contents/render_view_context_menu_views.h index 023a6c0..220768d 100644 --- a/chrome/browser/ui/views/tab_contents/render_view_context_menu_views.h +++ b/chrome/browser/ui/views/tab_contents/render_view_context_menu_views.h @@ -8,6 +8,7 @@ #include "base/memory/scoped_ptr.h" #include "base/memory/scoped_vector.h" +#include "base/string16.h" #include "chrome/browser/tab_contents/render_view_context_menu.h" namespace views { @@ -33,11 +34,18 @@ class RenderViewContextMenuViews : public RenderViewContextMenu { void UpdateMenuItemStates(); + // RenderViewContextMenuDelegate implementation. + virtual void UpdateMenuItem(int command_id, + bool enabled, + const string16& title) OVERRIDE; + protected: - // RenderViewContextMenu implementation -------------------------------------- - virtual void PlatformInit(); - virtual bool GetAcceleratorForCommandId(int command_id, - ui::Accelerator* accelerator); + // RenderViewContextMenu implementation. + virtual void PlatformInit() OVERRIDE; + virtual bool GetAcceleratorForCommandId( + int command_id, + ui::Accelerator* accelerator) OVERRIDE; + private: // The context menu itself and its contents. scoped_ptr<views::MenuModelAdapter> menu_delegate_; diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index a8bcc15..f8b362d 100644 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -2187,6 +2187,8 @@ 'browser/tab_contents/render_view_context_menu_gtk.h', 'browser/tab_contents/render_view_context_menu_mac.h', 'browser/tab_contents/render_view_context_menu_mac.mm', + 'browser/tab_contents/render_view_context_menu_observer.cc', + 'browser/tab_contents/render_view_context_menu_observer.h', 'browser/tab_contents/render_view_host_delegate_helper.cc', 'browser/tab_contents/render_view_host_delegate_helper.h', 'browser/tab_contents/tab_contents_ssl_helper.cc', @@ -2199,6 +2201,8 @@ 'browser/tab_contents/tab_util.h', 'browser/tab_contents/thumbnail_generator.cc', 'browser/tab_contents/thumbnail_generator.h', + 'browser/tab_contents/spelling_menu_observer.cc', + 'browser/tab_contents/spelling_menu_observer.h', 'browser/tab_contents/web_drag_dest_gtk.cc', 'browser/tab_contents/web_drag_dest_gtk.h', 'browser/tab_contents/web_drag_source_win.cc', diff --git a/chrome/common/pref_names.cc b/chrome/common/pref_names.cc index d406875..1ef81de 100644 --- a/chrome/common/pref_names.cc +++ b/chrome/common/pref_names.cc @@ -1080,6 +1080,9 @@ const char kPluginMessageResponseTimeout[] = // String which represents the dictionary name for our spell-checker. const char kSpellCheckDictionary[] = "spellcheck.dictionary"; +// String which represents whether we use the spelling service. +const char kSpellCheckUseSpellingService[] = "spellcheck.use_spelling_service"; + // Dictionary of schemes used by the external protocol handler. // The value is true if the scheme must be ignored. const char kExcludedSchemes[] = "protocol_handler.excluded_schemes"; diff --git a/chrome/common/pref_names.h b/chrome/common/pref_names.h index accfb4c..cd31f67 100644 --- a/chrome/common/pref_names.h +++ b/chrome/common/pref_names.h @@ -384,6 +384,7 @@ extern const char kHungPluginDetectFrequency[]; extern const char kPluginMessageResponseTimeout[]; extern const char kSpellCheckDictionary[]; +extern const char kSpellCheckUseSpellingService[]; extern const char kExcludedSchemes[]; |