Home | History | Annotate | Download | only in tab_contents
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "chrome/browser/tab_contents/spelling_menu_observer.h"
      6 
      7 #include "base/bind.h"
      8 #include "base/command_line.h"
      9 #include "base/i18n/case_conversion.h"
     10 #include "base/prefs/pref_service.h"
     11 #include "base/strings/utf_string_conversions.h"
     12 #include "chrome/app/chrome_command_ids.h"
     13 #include "chrome/browser/profiles/profile.h"
     14 #include "chrome/browser/spellchecker/spellcheck_factory.h"
     15 #include "chrome/browser/spellchecker/spellcheck_host_metrics.h"
     16 #include "chrome/browser/spellchecker/spellcheck_platform_mac.h"
     17 #include "chrome/browser/spellchecker/spellcheck_service.h"
     18 #include "chrome/browser/spellchecker/spelling_service_client.h"
     19 #include "chrome/browser/tab_contents/render_view_context_menu.h"
     20 #include "chrome/browser/tab_contents/spelling_bubble_model.h"
     21 #include "chrome/browser/ui/confirm_bubble.h"
     22 #include "chrome/common/chrome_switches.h"
     23 #include "chrome/common/pref_names.h"
     24 #include "chrome/common/spellcheck_result.h"
     25 #include "content/public/browser/render_view_host.h"
     26 #include "content/public/browser/render_widget_host_view.h"
     27 #include "content/public/browser/web_contents.h"
     28 #include "content/public/browser/web_contents_view.h"
     29 #include "content/public/common/context_menu_params.h"
     30 #include "extensions/browser/view_type_utils.h"
     31 #include "grit/generated_resources.h"
     32 #include "ui/base/l10n/l10n_util.h"
     33 #include "ui/gfx/rect.h"
     34 
     35 using content::BrowserThread;
     36 
     37 SpellingMenuObserver::SpellingMenuObserver(RenderViewContextMenuProxy* proxy)
     38     : proxy_(proxy),
     39       loading_frame_(0),
     40       succeeded_(false),
     41       misspelling_hash_(0),
     42       client_(new SpellingServiceClient) {
     43   if (proxy && proxy->GetProfile()) {
     44     integrate_spelling_service_.Init(prefs::kSpellCheckUseSpellingService,
     45                                      proxy->GetProfile()->GetPrefs());
     46     autocorrect_spelling_.Init(prefs::kEnableAutoSpellCorrect,
     47                                proxy->GetProfile()->GetPrefs());
     48   }
     49 }
     50 
     51 SpellingMenuObserver::~SpellingMenuObserver() {
     52 }
     53 
     54 void SpellingMenuObserver::InitMenu(const content::ContextMenuParams& params) {
     55   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
     56   DCHECK(!params.misspelled_word.empty() ||
     57       params.dictionary_suggestions.empty());
     58 
     59   // Exit if we are not in an editable element because we add a menu item only
     60   // for editable elements.
     61   Profile* profile = proxy_->GetProfile();
     62   if (!params.is_editable || !profile)
     63     return;
     64 
     65   // Exit if there is no misspelled word.
     66   if (params.misspelled_word.empty())
     67     return;
     68 
     69   suggestions_ = params.dictionary_suggestions;
     70   misspelled_word_ = params.misspelled_word;
     71   misspelling_hash_ = params.misspelling_hash;
     72 
     73   bool use_suggestions = SpellingServiceClient::IsAvailable(
     74       profile, SpellingServiceClient::SUGGEST);
     75 
     76   if (!suggestions_.empty() || use_suggestions)
     77     proxy_->AddSeparator();
     78 
     79   // Append Dictionary spell check suggestions.
     80   for (size_t i = 0; i < params.dictionary_suggestions.size() &&
     81        IDC_SPELLCHECK_SUGGESTION_0 + i <= IDC_SPELLCHECK_SUGGESTION_LAST;
     82        ++i) {
     83     proxy_->AddMenuItem(IDC_SPELLCHECK_SUGGESTION_0 + static_cast<int>(i),
     84                         params.dictionary_suggestions[i]);
     85   }
     86 
     87   // The service types |SpellingServiceClient::SPELLCHECK| and
     88   // |SpellingServiceClient::SUGGEST| are mutually exclusive. Only one is
     89   // available at at time.
     90   //
     91   // When |SpellingServiceClient::SPELLCHECK| is available, the contextual
     92   // suggestions from |SpellingServiceClient| are already stored in
     93   // |params.dictionary_suggestions|.  |SpellingMenuObserver| places these
     94   // suggestions in the slots |IDC_SPELLCHECK_SUGGESTION_[0-LAST]|. If
     95   // |SpellingMenuObserver| queried |SpellingServiceClient| again, then quality
     96   // of suggestions would be reduced by lack of context around the misspelled
     97   // word.
     98   //
     99   // When |SpellingServiceClient::SUGGEST| is available,
    100   // |params.dictionary_suggestions| contains suggestions only from Hunspell
    101   // dictionary. |SpellingMenuObserver| queries |SpellingServiceClient| with the
    102   // misspelled word without the surrounding context. Spellcheck suggestions
    103   // from |SpellingServiceClient::SUGGEST| are not available until
    104   // |SpellingServiceClient| responds to the query. While |SpellingMenuObserver|
    105   // waits for |SpellingServiceClient|, it shows a placeholder text "Loading
    106   // suggestion..." in the |IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION| slot. After
    107   // |SpellingServiceClient| responds to the query, |SpellingMenuObserver|
    108   // replaces the placeholder text with either the spelling suggestion or the
    109   // message "No more suggestions from Google." The "No more suggestions"
    110   // message is there when |SpellingServiceClient| returned the same suggestion
    111   // as Hunspell.
    112   if (use_suggestions) {
    113     // Append a placeholder item for the suggestion from the Spelling service
    114     // and send a request to the service if we can retrieve suggestions from it.
    115     // Also, see if we can use the spelling service to get an ideal suggestion.
    116     // Otherwise, we'll fall back to the set of suggestions.  Initialize
    117     // variables used in OnTextCheckComplete(). We copy the input text to the
    118     // result text so we can replace its misspelled regions with suggestions.
    119     succeeded_ = false;
    120     result_ = params.misspelled_word;
    121 
    122     // Add a placeholder item. This item will be updated when we receive a
    123     // response from the Spelling service. (We do not have to disable this
    124     // item now since Chrome will call IsCommandIdEnabled() and disable it.)
    125     loading_message_ =
    126         l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_CHECKING);
    127     proxy_->AddMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION,
    128                         loading_message_);
    129     // Invoke a JSON-RPC call to the Spelling service in the background so we
    130     // can update the placeholder item when we receive its response. It also
    131     // starts the animation timer so we can show animation until we receive
    132     // it.
    133     bool result = client_->RequestTextCheck(
    134         profile, SpellingServiceClient::SUGGEST, params.misspelled_word,
    135         base::Bind(&SpellingMenuObserver::OnTextCheckComplete,
    136                    base::Unretained(this), SpellingServiceClient::SUGGEST));
    137     if (result) {
    138       loading_frame_ = 0;
    139       animation_timer_.Start(FROM_HERE, base::TimeDelta::FromSeconds(1),
    140           this, &SpellingMenuObserver::OnAnimationTimerExpired);
    141     }
    142   }
    143 
    144   if (params.dictionary_suggestions.empty()) {
    145     proxy_->AddMenuItem(
    146         IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS,
    147         l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS));
    148     bool use_spelling_service = SpellingServiceClient::IsAvailable(
    149         profile, SpellingServiceClient::SPELLCHECK);
    150     if (use_suggestions || use_spelling_service)
    151       proxy_->AddSeparator();
    152   } else {
    153     proxy_->AddSeparator();
    154 
    155     // |spellcheck_service| can be null when the suggested word is
    156     // provided by Web SpellCheck API.
    157     SpellcheckService* spellcheck_service =
    158         SpellcheckServiceFactory::GetForProfile(profile);
    159     if (spellcheck_service && spellcheck_service->GetMetrics())
    160       spellcheck_service->GetMetrics()->RecordSuggestionStats(1);
    161   }
    162 
    163   // If word is misspelled, give option for "Add to dictionary" and a check item
    164   // "Ask Google for suggestions".
    165   proxy_->AddMenuItem(IDC_SPELLCHECK_ADD_TO_DICTIONARY,
    166       l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_ADD_TO_DICTIONARY));
    167 
    168 #if defined(TOOLKIT_GTK)
    169   extensions::ViewType view_type =
    170       extensions::GetViewType(proxy_->GetWebContents());
    171   if (view_type != extensions::VIEW_TYPE_PANEL) {
    172 #endif
    173     proxy_->AddCheckItem(IDC_CONTENT_CONTEXT_SPELLING_TOGGLE,
    174         l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_ASK_GOOGLE));
    175 #if defined(TOOLKIT_GTK)
    176   }
    177 #endif
    178 
    179   const CommandLine* command_line = CommandLine::ForCurrentProcess();
    180   if (command_line->HasSwitch(switches::kEnableSpellingAutoCorrect)) {
    181     proxy_->AddCheckItem(IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE,
    182         l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_AUTOCORRECT));
    183   }
    184 
    185   proxy_->AddSeparator();
    186 }
    187 
    188 bool SpellingMenuObserver::IsCommandIdSupported(int command_id) {
    189   if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 &&
    190       command_id <= IDC_SPELLCHECK_SUGGESTION_LAST)
    191     return true;
    192 
    193   switch (command_id) {
    194     case IDC_SPELLCHECK_ADD_TO_DICTIONARY:
    195     case IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS:
    196     case IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION:
    197     case IDC_CONTENT_CONTEXT_SPELLING_TOGGLE:
    198     case IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE:
    199       return true;
    200 
    201     default:
    202       return false;
    203   }
    204   return false;
    205 }
    206 
    207 bool SpellingMenuObserver::IsCommandIdChecked(int command_id) {
    208   DCHECK(IsCommandIdSupported(command_id));
    209 
    210   if (command_id == IDC_CONTENT_CONTEXT_SPELLING_TOGGLE)
    211     return integrate_spelling_service_.GetValue() &&
    212         !proxy_->GetProfile()->IsOffTheRecord();
    213   else if (command_id == IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE)
    214     return autocorrect_spelling_.GetValue() &&
    215         !proxy_->GetProfile()->IsOffTheRecord();
    216   return false;
    217 }
    218 
    219 bool SpellingMenuObserver::IsCommandIdEnabled(int command_id) {
    220   DCHECK(IsCommandIdSupported(command_id));
    221 
    222   if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 &&
    223       command_id <= IDC_SPELLCHECK_SUGGESTION_LAST)
    224     return true;
    225 
    226   switch (command_id) {
    227     case IDC_SPELLCHECK_ADD_TO_DICTIONARY:
    228       return !misspelled_word_.empty();
    229 
    230     case IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS:
    231       return false;
    232 
    233     case IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION:
    234       return succeeded_;
    235 
    236     case IDC_CONTENT_CONTEXT_SPELLING_TOGGLE:
    237       return integrate_spelling_service_.IsUserModifiable() &&
    238           !proxy_->GetProfile()->IsOffTheRecord();
    239 
    240     case IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE:
    241       return integrate_spelling_service_.IsUserModifiable() &&
    242           !proxy_->GetProfile()->IsOffTheRecord();
    243 
    244     default:
    245       return false;
    246   }
    247   return false;
    248 }
    249 
    250 void SpellingMenuObserver::ExecuteCommand(int command_id) {
    251   DCHECK(IsCommandIdSupported(command_id));
    252 
    253   if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 &&
    254       command_id <= IDC_SPELLCHECK_SUGGESTION_LAST) {
    255     int suggestion_index = command_id - IDC_SPELLCHECK_SUGGESTION_0;
    256     proxy_->GetRenderViewHost()->ReplaceMisspelling(
    257         suggestions_[suggestion_index]);
    258     // GetSpellCheckHost() can return null when the suggested word is provided
    259     // by Web SpellCheck API.
    260     Profile* profile = proxy_->GetProfile();
    261     if (profile) {
    262       SpellcheckService* spellcheck =
    263           SpellcheckServiceFactory::GetForProfile(profile);
    264       if (spellcheck) {
    265         if (spellcheck->GetMetrics())
    266           spellcheck->GetMetrics()->RecordReplacedWordStats(1);
    267         spellcheck->GetFeedbackSender()->SelectedSuggestion(
    268             misspelling_hash_, suggestion_index);
    269       }
    270     }
    271     return;
    272   }
    273 
    274   // When we choose the suggestion sent from the Spelling service, we replace
    275   // the misspelled word with the suggestion and add it to our custom-word
    276   // dictionary so this word is not marked as misspelled any longer.
    277   if (command_id == IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION) {
    278     proxy_->GetRenderViewHost()->ReplaceMisspelling(result_);
    279     misspelled_word_ = result_;
    280   }
    281 
    282   if (command_id == IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION ||
    283       command_id == IDC_SPELLCHECK_ADD_TO_DICTIONARY) {
    284     // GetHostForProfile() can return null when the suggested word is provided
    285     // by Web SpellCheck API.
    286     Profile* profile = proxy_->GetProfile();
    287     if (profile) {
    288       SpellcheckService* spellcheck =
    289           SpellcheckServiceFactory::GetForProfile(profile);
    290       if (spellcheck) {
    291         spellcheck->GetCustomDictionary()->AddWord(UTF16ToUTF8(
    292             misspelled_word_));
    293         spellcheck->GetFeedbackSender()->AddedToDictionary(misspelling_hash_);
    294       }
    295     }
    296 #if defined(OS_MACOSX)
    297     spellcheck_mac::AddWord(misspelled_word_);
    298 #endif
    299   }
    300 
    301   // The spelling service can be toggled by the user only if it is not managed.
    302   if (command_id == IDC_CONTENT_CONTEXT_SPELLING_TOGGLE &&
    303       integrate_spelling_service_.IsUserModifiable()) {
    304     // When a user enables the "Ask Google for spelling suggestions" item, we
    305     // show a bubble to confirm it. On the other hand, when a user disables this
    306     // item, we directly update/ the profile and stop integrating the spelling
    307     // service immediately.
    308     if (!integrate_spelling_service_.GetValue()) {
    309       content::RenderViewHost* rvh = proxy_->GetRenderViewHost();
    310       gfx::Rect rect = rvh->GetView()->GetViewBounds();
    311       chrome::ShowConfirmBubble(
    312 #if defined(TOOLKIT_VIEWS)
    313           proxy_->GetWebContents()->GetView()->GetTopLevelNativeWindow(),
    314 #else
    315           rvh->GetView()->GetNativeView(),
    316 #endif
    317           gfx::Point(rect.CenterPoint().x(), rect.y()),
    318           new SpellingBubbleModel(proxy_->GetProfile(),
    319                                   proxy_->GetWebContents(),
    320                                   false));
    321     } else {
    322       Profile* profile = proxy_->GetProfile();
    323       if (profile)
    324         profile->GetPrefs()->SetBoolean(prefs::kSpellCheckUseSpellingService,
    325                                         false);
    326         profile->GetPrefs()->SetBoolean(prefs::kEnableAutoSpellCorrect,
    327                                         false);
    328     }
    329   }
    330   // Autocorrect requires use of the spelling service and the spelling service
    331   // can be toggled by the user only if it is not managed.
    332   if (command_id == IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE &&
    333       integrate_spelling_service_.IsUserModifiable()) {
    334     // When the user enables autocorrect, we'll need to make sure that we can
    335     // ask Google for suggestions since that service is required. So we show
    336     // the bubble and just make sure to enable autocorrect as well.
    337     if (!integrate_spelling_service_.GetValue()) {
    338       content::RenderViewHost* rvh = proxy_->GetRenderViewHost();
    339       gfx::Rect rect = rvh->GetView()->GetViewBounds();
    340       chrome::ShowConfirmBubble(rvh->GetView()->GetNativeView(),
    341                                 gfx::Point(rect.CenterPoint().x(), rect.y()),
    342                                 new SpellingBubbleModel(
    343                                     proxy_->GetProfile(),
    344                                     proxy_->GetWebContents(),
    345                                     true));
    346     } else {
    347       Profile* profile = proxy_->GetProfile();
    348       if (profile) {
    349         bool current_value = autocorrect_spelling_.GetValue();
    350         profile->GetPrefs()->SetBoolean(prefs::kEnableAutoSpellCorrect,
    351                                         !current_value);
    352       }
    353     }
    354   }
    355 }
    356 
    357 void SpellingMenuObserver::OnMenuCancel() {
    358   Profile* profile = proxy_->GetProfile();
    359   if (!profile)
    360     return;
    361   SpellcheckService* spellcheck =
    362       SpellcheckServiceFactory::GetForProfile(profile);
    363   if (!spellcheck)
    364     return;
    365   spellcheck->GetFeedbackSender()->IgnoredSuggestions(misspelling_hash_);
    366 }
    367 
    368 void SpellingMenuObserver::OnTextCheckComplete(
    369     SpellingServiceClient::ServiceType type,
    370     bool success,
    371     const string16& text,
    372     const std::vector<SpellCheckResult>& results) {
    373   animation_timer_.Stop();
    374 
    375   // Scan the text-check results and replace the misspelled regions with
    376   // suggested words. If the replaced text is included in the suggestion list
    377   // provided by the local spellchecker, we show a "No suggestions from Google"
    378   // message.
    379   succeeded_ = success;
    380   if (results.empty()) {
    381     succeeded_ = false;
    382   } else {
    383     typedef std::vector<SpellCheckResult> SpellCheckResults;
    384     for (SpellCheckResults::const_iterator it = results.begin();
    385          it != results.end(); ++it) {
    386       result_.replace(it->location, it->length, it->replacement);
    387     }
    388     string16 result = base::i18n::ToLower(result_);
    389     for (std::vector<string16>::const_iterator it = suggestions_.begin();
    390          it != suggestions_.end(); ++it) {
    391       if (result == base::i18n::ToLower(*it)) {
    392         succeeded_ = false;
    393         break;
    394       }
    395     }
    396   }
    397   if (type != SpellingServiceClient::SPELLCHECK) {
    398     if (!succeeded_) {
    399       result_ = l10n_util::GetStringUTF16(
    400           IDS_CONTENT_CONTEXT_SPELLING_NO_SUGGESTIONS_FROM_GOOGLE);
    401     }
    402 
    403     // Update the menu item with the result text. We disable this item and hide
    404     // it when the spelling service does not provide valid suggestions.
    405     proxy_->UpdateMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, succeeded_,
    406                            false, result_);
    407   }
    408 }
    409 
    410 void SpellingMenuObserver::OnAnimationTimerExpired() {
    411   // Append '.' characters to the end of "Checking".
    412   loading_frame_ = (loading_frame_ + 1) & 3;
    413   string16 loading_message = loading_message_ + string16(loading_frame_,'.');
    414 
    415   // Update the menu item with the text. We disable this item to prevent users
    416   // from selecting it.
    417   proxy_->UpdateMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, false, false,
    418                          loading_message);
    419 }
    420