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.
5 #include "chrome/browser/renderer_context_menu/spelling_menu_observer.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/feedback_sender.h"
17 #include "chrome/browser/spellchecker/spellcheck_factory.h"
18 #include "chrome/browser/spellchecker/spellcheck_host_metrics.h"
19 #include "chrome/browser/spellchecker/spellcheck_platform.h"
20 #include "chrome/browser/spellchecker/spellcheck_service.h"
21 #include "chrome/browser/spellchecker/spelling_service_client.h"
22 #include "chrome/browser/ui/confirm_bubble.h"
23 #include "chrome/common/chrome_switches.h"
24 #include "chrome/common/pref_names.h"
25 #include "chrome/common/spellcheck_common.h"
26 #include "chrome/common/spellcheck_result.h"
27 #include "chrome/grit/generated_resources.h"
28 #include "content/public/browser/render_view_host.h"
29 #include "content/public/browser/render_widget_host_view.h"
30 #include "content/public/browser/web_contents.h"
31 #include "content/public/common/context_menu_params.h"
32 #include "extensions/browser/view_type_utils.h"
33 #include "ui/base/l10n/l10n_util.h"
34 #include "ui/gfx/geometry/rect.h"
36 using content::BrowserThread
;
38 SpellingMenuObserver::SpellingMenuObserver(RenderViewContextMenuProxy
* proxy
)
43 client_(new SpellingServiceClient
) {
44 if (proxy_
&& proxy_
->GetBrowserContext()) {
45 Profile
* profile
= Profile::FromBrowserContext(proxy_
->GetBrowserContext());
46 integrate_spelling_service_
.Init(prefs::kSpellCheckUseSpellingService
,
48 autocorrect_spelling_
.Init(prefs::kEnableAutoSpellCorrect
,
53 SpellingMenuObserver::~SpellingMenuObserver() {
56 void SpellingMenuObserver::InitMenu(const content::ContextMenuParams
& params
) {
57 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
58 DCHECK(!params
.misspelled_word
.empty() ||
59 params
.dictionary_suggestions
.empty());
61 // Exit if we are not in an editable element because we add a menu item only
62 // for editable elements.
63 content::BrowserContext
* browser_context
= proxy_
->GetBrowserContext();
64 if (!params
.is_editable
|| !browser_context
)
67 // Exit if there is no misspelled word.
68 if (params
.misspelled_word
.empty())
71 suggestions_
= params
.dictionary_suggestions
;
72 misspelled_word_
= params
.misspelled_word
;
73 misspelling_hash_
= params
.misspelling_hash
;
75 bool use_suggestions
= SpellingServiceClient::IsAvailable(
76 browser_context
, SpellingServiceClient::SUGGEST
);
78 if (!suggestions_
.empty() || use_suggestions
)
79 proxy_
->AddSeparator();
81 // Append Dictionary spell check suggestions.
82 for (size_t i
= 0; i
< params
.dictionary_suggestions
.size() &&
83 IDC_SPELLCHECK_SUGGESTION_0
+ i
<= IDC_SPELLCHECK_SUGGESTION_LAST
;
85 proxy_
->AddMenuItem(IDC_SPELLCHECK_SUGGESTION_0
+ static_cast<int>(i
),
86 params
.dictionary_suggestions
[i
]);
89 // The service types |SpellingServiceClient::SPELLCHECK| and
90 // |SpellingServiceClient::SUGGEST| are mutually exclusive. Only one is
91 // available at at time.
93 // When |SpellingServiceClient::SPELLCHECK| is available, the contextual
94 // suggestions from |SpellingServiceClient| are already stored in
95 // |params.dictionary_suggestions|. |SpellingMenuObserver| places these
96 // suggestions in the slots |IDC_SPELLCHECK_SUGGESTION_[0-LAST]|. If
97 // |SpellingMenuObserver| queried |SpellingServiceClient| again, then quality
98 // of suggestions would be reduced by lack of context around the misspelled
101 // When |SpellingServiceClient::SUGGEST| is available,
102 // |params.dictionary_suggestions| contains suggestions only from Hunspell
103 // dictionary. |SpellingMenuObserver| queries |SpellingServiceClient| with the
104 // misspelled word without the surrounding context. Spellcheck suggestions
105 // from |SpellingServiceClient::SUGGEST| are not available until
106 // |SpellingServiceClient| responds to the query. While |SpellingMenuObserver|
107 // waits for |SpellingServiceClient|, it shows a placeholder text "Loading
108 // suggestion..." in the |IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION| slot. After
109 // |SpellingServiceClient| responds to the query, |SpellingMenuObserver|
110 // replaces the placeholder text with either the spelling suggestion or the
111 // message "No more suggestions from Google." The "No more suggestions"
112 // message is there when |SpellingServiceClient| returned the same suggestion
114 if (use_suggestions
) {
115 // Append a placeholder item for the suggestion from the Spelling service
116 // and send a request to the service if we can retrieve suggestions from it.
117 // Also, see if we can use the spelling service to get an ideal suggestion.
118 // Otherwise, we'll fall back to the set of suggestions. Initialize
119 // variables used in OnTextCheckComplete(). We copy the input text to the
120 // result text so we can replace its misspelled regions with suggestions.
122 result_
= params
.misspelled_word
;
124 // Add a placeholder item. This item will be updated when we receive a
125 // response from the Spelling service. (We do not have to disable this
126 // item now since Chrome will call IsCommandIdEnabled() and disable it.)
128 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_CHECKING
);
129 proxy_
->AddMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION
,
131 // Invoke a JSON-RPC call to the Spelling service in the background so we
132 // can update the placeholder item when we receive its response. It also
133 // starts the animation timer so we can show animation until we receive
135 bool result
= client_
->RequestTextCheck(
137 SpellingServiceClient::SUGGEST
,
138 params
.misspelled_word
,
139 base::Bind(&SpellingMenuObserver::OnTextCheckComplete
,
140 base::Unretained(this),
141 SpellingServiceClient::SUGGEST
));
144 animation_timer_
.Start(FROM_HERE
, base::TimeDelta::FromSeconds(1),
145 this, &SpellingMenuObserver::OnAnimationTimerExpired
);
149 if (params
.dictionary_suggestions
.empty()) {
151 IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS
,
152 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS
));
153 bool use_spelling_service
= SpellingServiceClient::IsAvailable(
154 browser_context
, SpellingServiceClient::SPELLCHECK
);
155 if (use_suggestions
|| use_spelling_service
)
156 proxy_
->AddSeparator();
158 proxy_
->AddSeparator();
160 // |spellcheck_service| can be null when the suggested word is
161 // provided by Web SpellCheck API.
162 SpellcheckService
* spellcheck_service
=
163 SpellcheckServiceFactory::GetForContext(browser_context
);
164 if (spellcheck_service
&& spellcheck_service
->GetMetrics())
165 spellcheck_service
->GetMetrics()->RecordSuggestionStats(1);
168 // If word is misspelled, give option for "Add to dictionary" and, if
169 // multilingual spellchecking is not enabled, a check item "Ask Google for
171 proxy_
->AddMenuItem(IDC_SPELLCHECK_ADD_TO_DICTIONARY
,
172 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_ADD_TO_DICTIONARY
));
174 proxy_
->AddCheckItem(
175 IDC_CONTENT_CONTEXT_SPELLING_TOGGLE
,
176 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_ASK_GOOGLE
));
178 const base::CommandLine
* command_line
=
179 base::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
));
185 proxy_
->AddSeparator();
188 bool SpellingMenuObserver::IsCommandIdSupported(int command_id
) {
189 if (command_id
>= IDC_SPELLCHECK_SUGGESTION_0
&&
190 command_id
<= IDC_SPELLCHECK_SUGGESTION_LAST
)
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
:
206 bool SpellingMenuObserver::IsCommandIdChecked(int command_id
) {
207 DCHECK(IsCommandIdSupported(command_id
));
208 Profile
* profile
= Profile::FromBrowserContext(proxy_
->GetBrowserContext());
210 if (command_id
== IDC_CONTENT_CONTEXT_SPELLING_TOGGLE
)
211 return integrate_spelling_service_
.GetValue() &&
212 !profile
->IsOffTheRecord();
213 if (command_id
== IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE
)
214 return autocorrect_spelling_
.GetValue() && !profile
->IsOffTheRecord();
218 bool SpellingMenuObserver::IsCommandIdEnabled(int command_id
) {
219 DCHECK(IsCommandIdSupported(command_id
));
221 if (command_id
>= IDC_SPELLCHECK_SUGGESTION_0
&&
222 command_id
<= IDC_SPELLCHECK_SUGGESTION_LAST
)
225 Profile
* profile
= Profile::FromBrowserContext(proxy_
->GetBrowserContext());
226 switch (command_id
) {
227 case IDC_SPELLCHECK_ADD_TO_DICTIONARY
:
228 return !misspelled_word_
.empty();
230 case IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS
:
233 case IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION
:
236 case IDC_CONTENT_CONTEXT_SPELLING_TOGGLE
:
237 return integrate_spelling_service_
.IsUserModifiable() &&
238 !profile
->IsOffTheRecord();
240 case IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE
:
241 return integrate_spelling_service_
.IsUserModifiable() &&
242 !profile
->IsOffTheRecord();
249 void SpellingMenuObserver::ExecuteCommand(int command_id
) {
250 DCHECK(IsCommandIdSupported(command_id
));
252 if (command_id
>= IDC_SPELLCHECK_SUGGESTION_0
&&
253 command_id
<= IDC_SPELLCHECK_SUGGESTION_LAST
) {
254 int suggestion_index
= command_id
- IDC_SPELLCHECK_SUGGESTION_0
;
255 proxy_
->GetWebContents()->ReplaceMisspelling(
256 suggestions_
[suggestion_index
]);
257 // GetSpellCheckHost() can return null when the suggested word is provided
258 // by Web SpellCheck API.
259 content::BrowserContext
* browser_context
= proxy_
->GetBrowserContext();
260 if (browser_context
) {
261 SpellcheckService
* spellcheck
=
262 SpellcheckServiceFactory::GetForContext(browser_context
);
264 if (spellcheck
->GetMetrics())
265 spellcheck
->GetMetrics()->RecordReplacedWordStats(1);
266 spellcheck
->GetFeedbackSender()->SelectedSuggestion(
267 misspelling_hash_
, suggestion_index
);
273 // When we choose the suggestion sent from the Spelling service, we replace
274 // the misspelled word with the suggestion and add it to our custom-word
275 // dictionary so this word is not marked as misspelled any longer.
276 if (command_id
== IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION
) {
277 proxy_
->GetWebContents()->ReplaceMisspelling(result_
);
278 misspelled_word_
= result_
;
281 if (command_id
== IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION
||
282 command_id
== IDC_SPELLCHECK_ADD_TO_DICTIONARY
) {
283 // GetHostForProfile() can return null when the suggested word is provided
284 // by Web SpellCheck API.
285 content::BrowserContext
* browser_context
= proxy_
->GetBrowserContext();
286 if (browser_context
) {
287 SpellcheckService
* spellcheck
=
288 SpellcheckServiceFactory::GetForContext(browser_context
);
290 spellcheck
->GetCustomDictionary()->AddWord(base::UTF16ToUTF8(
292 spellcheck
->GetFeedbackSender()->AddedToDictionary(misspelling_hash_
);
295 #if defined(USE_BROWSER_SPELLCHECKER)
296 spellcheck_platform::AddWord(misspelled_word_
);
300 Profile
* profile
= Profile::FromBrowserContext(proxy_
->GetBrowserContext());
302 // The spelling service can be toggled by the user only if it is not managed.
303 if (command_id
== IDC_CONTENT_CONTEXT_SPELLING_TOGGLE
&&
304 integrate_spelling_service_
.IsUserModifiable()) {
305 // When a user enables the "Ask Google for spelling suggestions" item, we
306 // show a bubble to confirm it. On the other hand, when a user disables this
307 // item, we directly update/ the profile and stop integrating the spelling
308 // service immediately.
309 if (!integrate_spelling_service_
.GetValue()) {
310 content::RenderViewHost
* rvh
= proxy_
->GetRenderViewHost();
311 gfx::Rect rect
= rvh
->GetView()->GetViewBounds();
312 scoped_ptr
<SpellingBubbleModel
> model(
313 new SpellingBubbleModel(profile
, proxy_
->GetWebContents(), false));
314 chrome::ShowConfirmBubble(
315 proxy_
->GetWebContents()->GetTopLevelNativeWindow(),
316 rvh
->GetView()->GetNativeView(),
317 gfx::Point(rect
.CenterPoint().x(), rect
.y()),
321 profile
->GetPrefs()->SetBoolean(prefs::kSpellCheckUseSpellingService
,
323 profile
->GetPrefs()->SetBoolean(prefs::kEnableAutoSpellCorrect
,
328 // Autocorrect requires use of the spelling service and the spelling service
329 // can be toggled by the user only if it is not managed.
330 if (command_id
== IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE
&&
331 integrate_spelling_service_
.IsUserModifiable()) {
332 // When the user enables autocorrect, we'll need to make sure that we can
333 // ask Google for suggestions since that service is required. So we show
334 // the bubble and just make sure to enable autocorrect as well.
335 if (!integrate_spelling_service_
.GetValue()) {
336 content::RenderViewHost
* rvh
= proxy_
->GetRenderViewHost();
337 gfx::Rect rect
= rvh
->GetView()->GetViewBounds();
338 scoped_ptr
<SpellingBubbleModel
> model(
339 new SpellingBubbleModel(profile
, proxy_
->GetWebContents(), true));
340 chrome::ShowConfirmBubble(
341 proxy_
->GetWebContents()->GetTopLevelNativeWindow(),
342 rvh
->GetView()->GetNativeView(),
343 gfx::Point(rect
.CenterPoint().x(), rect
.y()),
347 bool current_value
= autocorrect_spelling_
.GetValue();
348 profile
->GetPrefs()->SetBoolean(prefs::kEnableAutoSpellCorrect
,
355 void SpellingMenuObserver::OnMenuCancel() {
356 content::BrowserContext
* browser_context
= proxy_
->GetBrowserContext();
357 if (!browser_context
)
359 SpellcheckService
* spellcheck
=
360 SpellcheckServiceFactory::GetForContext(browser_context
);
363 spellcheck
->GetFeedbackSender()->IgnoredSuggestions(misspelling_hash_
);
366 void SpellingMenuObserver::OnTextCheckComplete(
367 SpellingServiceClient::ServiceType type
,
369 const base::string16
& text
,
370 const std::vector
<SpellCheckResult
>& results
) {
371 animation_timer_
.Stop();
373 // Scan the text-check results and replace the misspelled regions with
374 // suggested words. If the replaced text is included in the suggestion list
375 // provided by the local spellchecker, we show a "No suggestions from Google"
377 succeeded_
= success
;
378 if (results
.empty()) {
381 typedef std::vector
<SpellCheckResult
> SpellCheckResults
;
382 for (SpellCheckResults::const_iterator it
= results
.begin();
383 it
!= results
.end(); ++it
) {
384 result_
.replace(it
->location
, it
->length
, it
->replacement
);
386 base::string16 result
= base::i18n::ToLower(result_
);
387 for (std::vector
<base::string16
>::const_iterator it
= suggestions_
.begin();
388 it
!= suggestions_
.end(); ++it
) {
389 if (result
== base::i18n::ToLower(*it
)) {
395 if (type
!= SpellingServiceClient::SPELLCHECK
) {
397 result_
= l10n_util::GetStringUTF16(
398 IDS_CONTENT_CONTEXT_SPELLING_NO_SUGGESTIONS_FROM_GOOGLE
);
401 // Update the menu item with the result text. We disable this item and hide
402 // it when the spelling service does not provide valid suggestions.
403 proxy_
->UpdateMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION
, succeeded_
,
408 void SpellingMenuObserver::OnAnimationTimerExpired() {
409 // Append '.' characters to the end of "Checking".
410 loading_frame_
= (loading_frame_
+ 1) & 3;
411 base::string16 loading_message
=
412 loading_message_
+ base::string16(loading_frame_
,'.');
414 // Update the menu item with the text. We disable this item to prevent users
415 // from selecting it.
416 proxy_
->UpdateMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION
, false, false,