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 "chrome/grit/generated_resources.h"
     26 #include "content/public/browser/render_view_host.h"
     27 #include "content/public/browser/render_widget_host_view.h"
     28 #include "content/public/browser/web_contents.h"
     29 #include "content/public/common/context_menu_params.h"
     30 #include "extensions/browser/view_type_utils.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_->GetBrowserContext()) {
     43     Profile* profile = Profile::FromBrowserContext(proxy_->GetBrowserContext());
     44     integrate_spelling_service_.Init(prefs::kSpellCheckUseSpellingService,
     45                                      profile->GetPrefs());
     46     autocorrect_spelling_.Init(prefs::kEnableAutoSpellCorrect,
     47                                profile->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   content::BrowserContext* browser_context = proxy_->GetBrowserContext();
     62   if (!params.is_editable || !browser_context)
     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       browser_context, 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         browser_context,
    135         SpellingServiceClient::SUGGEST,
    136         params.misspelled_word,
    137         base::Bind(&SpellingMenuObserver::OnTextCheckComplete,
    138                    base::Unretained(this),
    139                    SpellingServiceClient::SUGGEST));
    140     if (result) {
    141       loading_frame_ = 0;
    142       animation_timer_.Start(FROM_HERE, base::TimeDelta::FromSeconds(1),
    143           this, &SpellingMenuObserver::OnAnimationTimerExpired);
    144     }
    145   }
    146 
    147   if (params.dictionary_suggestions.empty()) {
    148     proxy_->AddMenuItem(
    149         IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS,
    150         l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS));
    151     bool use_spelling_service = SpellingServiceClient::IsAvailable(
    152         browser_context, SpellingServiceClient::SPELLCHECK);
    153     if (use_suggestions || use_spelling_service)
    154       proxy_->AddSeparator();
    155   } else {
    156     proxy_->AddSeparator();
    157 
    158     // |spellcheck_service| can be null when the suggested word is
    159     // provided by Web SpellCheck API.
    160     SpellcheckService* spellcheck_service =
    161         SpellcheckServiceFactory::GetForContext(browser_context);
    162     if (spellcheck_service && spellcheck_service->GetMetrics())
    163       spellcheck_service->GetMetrics()->RecordSuggestionStats(1);
    164   }
    165 
    166   // If word is misspelled, give option for "Add to dictionary" and a check item
    167   // "Ask Google for suggestions".
    168   proxy_->AddMenuItem(IDC_SPELLCHECK_ADD_TO_DICTIONARY,
    169       l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_ADD_TO_DICTIONARY));
    170 
    171   proxy_->AddCheckItem(IDC_CONTENT_CONTEXT_SPELLING_TOGGLE,
    172       l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_ASK_GOOGLE));
    173 
    174   const CommandLine* command_line = CommandLine::ForCurrentProcess();
    175   if (command_line->HasSwitch(switches::kEnableSpellingAutoCorrect)) {
    176     proxy_->AddCheckItem(IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE,
    177         l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_AUTOCORRECT));
    178   }
    179 
    180   proxy_->AddSeparator();
    181 }
    182 
    183 bool SpellingMenuObserver::IsCommandIdSupported(int command_id) {
    184   if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 &&
    185       command_id <= IDC_SPELLCHECK_SUGGESTION_LAST)
    186     return true;
    187 
    188   switch (command_id) {
    189     case IDC_SPELLCHECK_ADD_TO_DICTIONARY:
    190     case IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS:
    191     case IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION:
    192     case IDC_CONTENT_CONTEXT_SPELLING_TOGGLE:
    193     case IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE:
    194       return true;
    195 
    196     default:
    197       return false;
    198   }
    199 }
    200 
    201 bool SpellingMenuObserver::IsCommandIdChecked(int command_id) {
    202   DCHECK(IsCommandIdSupported(command_id));
    203   Profile* profile = Profile::FromBrowserContext(proxy_->GetBrowserContext());
    204 
    205   if (command_id == IDC_CONTENT_CONTEXT_SPELLING_TOGGLE)
    206     return integrate_spelling_service_.GetValue() && !profile->IsOffTheRecord();
    207   if (command_id == IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE)
    208     return autocorrect_spelling_.GetValue() && !profile->IsOffTheRecord();
    209   return false;
    210 }
    211 
    212 bool SpellingMenuObserver::IsCommandIdEnabled(int command_id) {
    213   DCHECK(IsCommandIdSupported(command_id));
    214 
    215   if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 &&
    216       command_id <= IDC_SPELLCHECK_SUGGESTION_LAST)
    217     return true;
    218 
    219   Profile* profile = Profile::FromBrowserContext(proxy_->GetBrowserContext());
    220   switch (command_id) {
    221     case IDC_SPELLCHECK_ADD_TO_DICTIONARY:
    222       return !misspelled_word_.empty();
    223 
    224     case IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS:
    225       return false;
    226 
    227     case IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION:
    228       return succeeded_;
    229 
    230     case IDC_CONTENT_CONTEXT_SPELLING_TOGGLE:
    231       return integrate_spelling_service_.IsUserModifiable() &&
    232              !profile->IsOffTheRecord();
    233 
    234     case IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE:
    235       return integrate_spelling_service_.IsUserModifiable() &&
    236              !profile->IsOffTheRecord();
    237 
    238     default:
    239       return false;
    240   }
    241 }
    242 
    243 void SpellingMenuObserver::ExecuteCommand(int command_id) {
    244   DCHECK(IsCommandIdSupported(command_id));
    245 
    246   if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 &&
    247       command_id <= IDC_SPELLCHECK_SUGGESTION_LAST) {
    248     int suggestion_index = command_id - IDC_SPELLCHECK_SUGGESTION_0;
    249     proxy_->GetWebContents()->ReplaceMisspelling(
    250         suggestions_[suggestion_index]);
    251     // GetSpellCheckHost() can return null when the suggested word is provided
    252     // by Web SpellCheck API.
    253     content::BrowserContext* browser_context = proxy_->GetBrowserContext();
    254     if (browser_context) {
    255       SpellcheckService* spellcheck =
    256           SpellcheckServiceFactory::GetForContext(browser_context);
    257       if (spellcheck) {
    258         if (spellcheck->GetMetrics())
    259           spellcheck->GetMetrics()->RecordReplacedWordStats(1);
    260         spellcheck->GetFeedbackSender()->SelectedSuggestion(
    261             misspelling_hash_, suggestion_index);
    262       }
    263     }
    264     return;
    265   }
    266 
    267   // When we choose the suggestion sent from the Spelling service, we replace
    268   // the misspelled word with the suggestion and add it to our custom-word
    269   // dictionary so this word is not marked as misspelled any longer.
    270   if (command_id == IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION) {
    271     proxy_->GetWebContents()->ReplaceMisspelling(result_);
    272     misspelled_word_ = result_;
    273   }
    274 
    275   if (command_id == IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION ||
    276       command_id == IDC_SPELLCHECK_ADD_TO_DICTIONARY) {
    277     // GetHostForProfile() can return null when the suggested word is provided
    278     // by Web SpellCheck API.
    279     content::BrowserContext* browser_context = proxy_->GetBrowserContext();
    280     if (browser_context) {
    281       SpellcheckService* spellcheck =
    282           SpellcheckServiceFactory::GetForContext(browser_context);
    283       if (spellcheck) {
    284         spellcheck->GetCustomDictionary()->AddWord(base::UTF16ToUTF8(
    285             misspelled_word_));
    286         spellcheck->GetFeedbackSender()->AddedToDictionary(misspelling_hash_);
    287       }
    288     }
    289 #if defined(OS_MACOSX)
    290     spellcheck_mac::AddWord(misspelled_word_);
    291 #endif
    292   }
    293 
    294   Profile* profile = Profile::FromBrowserContext(proxy_->GetBrowserContext());
    295 
    296   // The spelling service can be toggled by the user only if it is not managed.
    297   if (command_id == IDC_CONTENT_CONTEXT_SPELLING_TOGGLE &&
    298       integrate_spelling_service_.IsUserModifiable()) {
    299     // When a user enables the "Ask Google for spelling suggestions" item, we
    300     // show a bubble to confirm it. On the other hand, when a user disables this
    301     // item, we directly update/ the profile and stop integrating the spelling
    302     // service immediately.
    303     if (!integrate_spelling_service_.GetValue()) {
    304       content::RenderViewHost* rvh = proxy_->GetRenderViewHost();
    305       gfx::Rect rect = rvh->GetView()->GetViewBounds();
    306       chrome::ShowConfirmBubble(
    307           proxy_->GetWebContents()->GetTopLevelNativeWindow(),
    308           rvh->GetView()->GetNativeView(),
    309           gfx::Point(rect.CenterPoint().x(), rect.y()),
    310           new SpellingBubbleModel(profile, proxy_->GetWebContents(), false));
    311     } else {
    312       if (profile) {
    313         profile->GetPrefs()->SetBoolean(prefs::kSpellCheckUseSpellingService,
    314                                         false);
    315         profile->GetPrefs()->SetBoolean(prefs::kEnableAutoSpellCorrect,
    316                                         false);
    317       }
    318     }
    319   }
    320   // Autocorrect requires use of the spelling service and the spelling service
    321   // can be toggled by the user only if it is not managed.
    322   if (command_id == IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE &&
    323       integrate_spelling_service_.IsUserModifiable()) {
    324     // When the user enables autocorrect, we'll need to make sure that we can
    325     // ask Google for suggestions since that service is required. So we show
    326     // the bubble and just make sure to enable autocorrect as well.
    327     if (!integrate_spelling_service_.GetValue()) {
    328       content::RenderViewHost* rvh = proxy_->GetRenderViewHost();
    329       gfx::Rect rect = rvh->GetView()->GetViewBounds();
    330       chrome::ShowConfirmBubble(
    331           proxy_->GetWebContents()->GetTopLevelNativeWindow(),
    332           rvh->GetView()->GetNativeView(),
    333           gfx::Point(rect.CenterPoint().x(), rect.y()),
    334           new SpellingBubbleModel(profile, proxy_->GetWebContents(), true));
    335     } else {
    336       if (profile) {
    337         bool current_value = autocorrect_spelling_.GetValue();
    338         profile->GetPrefs()->SetBoolean(prefs::kEnableAutoSpellCorrect,
    339                                         !current_value);
    340       }
    341     }
    342   }
    343 }
    344 
    345 void SpellingMenuObserver::OnMenuCancel() {
    346   content::BrowserContext* browser_context = proxy_->GetBrowserContext();
    347   if (!browser_context)
    348     return;
    349   SpellcheckService* spellcheck =
    350       SpellcheckServiceFactory::GetForContext(browser_context);
    351   if (!spellcheck)
    352     return;
    353   spellcheck->GetFeedbackSender()->IgnoredSuggestions(misspelling_hash_);
    354 }
    355 
    356 void SpellingMenuObserver::OnTextCheckComplete(
    357     SpellingServiceClient::ServiceType type,
    358     bool success,
    359     const base::string16& text,
    360     const std::vector<SpellCheckResult>& results) {
    361   animation_timer_.Stop();
    362 
    363   // Scan the text-check results and replace the misspelled regions with
    364   // suggested words. If the replaced text is included in the suggestion list
    365   // provided by the local spellchecker, we show a "No suggestions from Google"
    366   // message.
    367   succeeded_ = success;
    368   if (results.empty()) {
    369     succeeded_ = false;
    370   } else {
    371     typedef std::vector<SpellCheckResult> SpellCheckResults;
    372     for (SpellCheckResults::const_iterator it = results.begin();
    373          it != results.end(); ++it) {
    374       result_.replace(it->location, it->length, it->replacement);
    375     }
    376     base::string16 result = base::i18n::ToLower(result_);
    377     for (std::vector<base::string16>::const_iterator it = suggestions_.begin();
    378          it != suggestions_.end(); ++it) {
    379       if (result == base::i18n::ToLower(*it)) {
    380         succeeded_ = false;
    381         break;
    382       }
    383     }
    384   }
    385   if (type != SpellingServiceClient::SPELLCHECK) {
    386     if (!succeeded_) {
    387       result_ = l10n_util::GetStringUTF16(
    388           IDS_CONTENT_CONTEXT_SPELLING_NO_SUGGESTIONS_FROM_GOOGLE);
    389     }
    390 
    391     // Update the menu item with the result text. We disable this item and hide
    392     // it when the spelling service does not provide valid suggestions.
    393     proxy_->UpdateMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, succeeded_,
    394                            false, result_);
    395   }
    396 }
    397 
    398 void SpellingMenuObserver::OnAnimationTimerExpired() {
    399   // Append '.' characters to the end of "Checking".
    400   loading_frame_ = (loading_frame_ + 1) & 3;
    401   base::string16 loading_message =
    402       loading_message_ + base::string16(loading_frame_,'.');
    403 
    404   // Update the menu item with the text. We disable this item to prevent users
    405   // from selecting it.
    406   proxy_->UpdateMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, false, false,
    407                          loading_message);
    408 }
    409