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