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
34using content::BrowserThread;
35
36SpellingMenuObserver::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
51SpellingMenuObserver::~SpellingMenuObserver() {
52}
53
54void 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
183bool 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
201bool 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
212bool 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
243void 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
345void 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
356void 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
398void 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