Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / chrome / browser / renderer_context_menu / spelling_menu_observer.cc
blob351a783d6ca9b3ee36be3de43a342e2b3b16b9cc
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"
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/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)
39 : proxy_(proxy),
40 loading_frame_(0),
41 succeeded_(false),
42 misspelling_hash_(0),
43 client_(new SpellingServiceClient) {
44 if (proxy_ && proxy_->GetBrowserContext()) {
45 Profile* profile = Profile::FromBrowserContext(proxy_->GetBrowserContext());
46 integrate_spelling_service_.Init(prefs::kSpellCheckUseSpellingService,
47 profile->GetPrefs());
48 autocorrect_spelling_.Init(prefs::kEnableAutoSpellCorrect,
49 profile->GetPrefs());
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)
65 return;
67 // Exit if there is no misspelled word.
68 if (params.misspelled_word.empty())
69 return;
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;
84 ++i) {
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
99 // word.
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
113 // as Hunspell.
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.
121 succeeded_ = false;
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.)
127 loading_message_ =
128 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_CHECKING);
129 proxy_->AddMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION,
130 loading_message_);
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
134 // it.
135 bool result = client_->RequestTextCheck(
136 browser_context,
137 SpellingServiceClient::SUGGEST,
138 params.misspelled_word,
139 base::Bind(&SpellingMenuObserver::OnTextCheckComplete,
140 base::Unretained(this),
141 SpellingServiceClient::SUGGEST));
142 if (result) {
143 loading_frame_ = 0;
144 animation_timer_.Start(FROM_HERE, base::TimeDelta::FromSeconds(1),
145 this, &SpellingMenuObserver::OnAnimationTimerExpired);
149 if (params.dictionary_suggestions.empty()) {
150 proxy_->AddMenuItem(
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();
157 } else {
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
170 // suggestions".
171 proxy_->AddMenuItem(IDC_SPELLCHECK_ADD_TO_DICTIONARY,
172 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_ADD_TO_DICTIONARY));
174 if (!chrome::spellcheck_common::IsMultilingualSpellcheckEnabled()) {
175 proxy_->AddCheckItem(
176 IDC_CONTENT_CONTEXT_SPELLING_TOGGLE,
177 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_ASK_GOOGLE));
180 const base::CommandLine* command_line =
181 base::CommandLine::ForCurrentProcess();
182 if (command_line->HasSwitch(switches::kEnableSpellingAutoCorrect)) {
183 proxy_->AddCheckItem(IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE,
184 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_AUTOCORRECT));
187 proxy_->AddSeparator();
190 bool SpellingMenuObserver::IsCommandIdSupported(int command_id) {
191 if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 &&
192 command_id <= IDC_SPELLCHECK_SUGGESTION_LAST)
193 return true;
195 switch (command_id) {
196 case IDC_SPELLCHECK_ADD_TO_DICTIONARY:
197 case IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS:
198 case IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION:
199 case IDC_CONTENT_CONTEXT_SPELLING_TOGGLE:
200 case IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE:
201 return true;
203 default:
204 return false;
208 bool SpellingMenuObserver::IsCommandIdChecked(int command_id) {
209 DCHECK(IsCommandIdSupported(command_id));
210 Profile* profile = Profile::FromBrowserContext(proxy_->GetBrowserContext());
212 if (command_id == IDC_CONTENT_CONTEXT_SPELLING_TOGGLE)
213 return integrate_spelling_service_.GetValue() &&
214 !profile->IsOffTheRecord() &&
215 !chrome::spellcheck_common::IsMultilingualSpellcheckEnabled();
216 if (command_id == IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE)
217 return autocorrect_spelling_.GetValue() && !profile->IsOffTheRecord();
218 return false;
221 bool SpellingMenuObserver::IsCommandIdEnabled(int command_id) {
222 DCHECK(IsCommandIdSupported(command_id));
224 if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 &&
225 command_id <= IDC_SPELLCHECK_SUGGESTION_LAST)
226 return true;
228 Profile* profile = Profile::FromBrowserContext(proxy_->GetBrowserContext());
229 switch (command_id) {
230 case IDC_SPELLCHECK_ADD_TO_DICTIONARY:
231 return !misspelled_word_.empty();
233 case IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS:
234 return false;
236 case IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION:
237 return succeeded_;
239 case IDC_CONTENT_CONTEXT_SPELLING_TOGGLE:
240 return integrate_spelling_service_.IsUserModifiable() &&
241 !profile->IsOffTheRecord() &&
242 !chrome::spellcheck_common::IsMultilingualSpellcheckEnabled();
244 case IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE:
245 return integrate_spelling_service_.IsUserModifiable() &&
246 !profile->IsOffTheRecord();
248 default:
249 return false;
253 void SpellingMenuObserver::ExecuteCommand(int command_id) {
254 DCHECK(IsCommandIdSupported(command_id));
256 if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 &&
257 command_id <= IDC_SPELLCHECK_SUGGESTION_LAST) {
258 int suggestion_index = command_id - IDC_SPELLCHECK_SUGGESTION_0;
259 proxy_->GetWebContents()->ReplaceMisspelling(
260 suggestions_[suggestion_index]);
261 // GetSpellCheckHost() can return null when the suggested word is provided
262 // by Web SpellCheck API.
263 content::BrowserContext* browser_context = proxy_->GetBrowserContext();
264 if (browser_context) {
265 SpellcheckService* spellcheck =
266 SpellcheckServiceFactory::GetForContext(browser_context);
267 if (spellcheck) {
268 if (spellcheck->GetMetrics())
269 spellcheck->GetMetrics()->RecordReplacedWordStats(1);
270 spellcheck->GetFeedbackSender()->SelectedSuggestion(
271 misspelling_hash_, suggestion_index);
274 return;
277 // When we choose the suggestion sent from the Spelling service, we replace
278 // the misspelled word with the suggestion and add it to our custom-word
279 // dictionary so this word is not marked as misspelled any longer.
280 if (command_id == IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION) {
281 proxy_->GetWebContents()->ReplaceMisspelling(result_);
282 misspelled_word_ = result_;
285 if (command_id == IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION ||
286 command_id == IDC_SPELLCHECK_ADD_TO_DICTIONARY) {
287 // GetHostForProfile() can return null when the suggested word is provided
288 // by Web SpellCheck API.
289 content::BrowserContext* browser_context = proxy_->GetBrowserContext();
290 if (browser_context) {
291 SpellcheckService* spellcheck =
292 SpellcheckServiceFactory::GetForContext(browser_context);
293 if (spellcheck) {
294 spellcheck->GetCustomDictionary()->AddWord(base::UTF16ToUTF8(
295 misspelled_word_));
296 spellcheck->GetFeedbackSender()->AddedToDictionary(misspelling_hash_);
299 #if defined(USE_PLATFORM_SPELLCHECKER)
300 spellcheck_platform::AddWord(misspelled_word_);
301 #endif
304 Profile* profile = Profile::FromBrowserContext(proxy_->GetBrowserContext());
306 // The spelling service can be toggled by the user only if it is not managed.
307 if (command_id == IDC_CONTENT_CONTEXT_SPELLING_TOGGLE &&
308 integrate_spelling_service_.IsUserModifiable()) {
309 // When a user enables the "Ask Google for spelling suggestions" item, we
310 // show a bubble to confirm it. On the other hand, when a user disables this
311 // item, we directly update/ the profile and stop integrating the spelling
312 // service immediately.
313 if (!integrate_spelling_service_.GetValue()) {
314 content::RenderViewHost* rvh = proxy_->GetRenderViewHost();
315 gfx::Rect rect = rvh->GetView()->GetViewBounds();
316 scoped_ptr<SpellingBubbleModel> model(
317 new SpellingBubbleModel(profile, proxy_->GetWebContents(), false));
318 chrome::ShowConfirmBubble(
319 proxy_->GetWebContents()->GetTopLevelNativeWindow(),
320 rvh->GetView()->GetNativeView(),
321 gfx::Point(rect.CenterPoint().x(), rect.y()),
322 model.Pass());
323 } else {
324 if (profile) {
325 profile->GetPrefs()->SetBoolean(prefs::kSpellCheckUseSpellingService,
326 false);
327 profile->GetPrefs()->SetBoolean(prefs::kEnableAutoSpellCorrect,
328 false);
332 // Autocorrect requires use of the spelling service and the spelling service
333 // can be toggled by the user only if it is not managed.
334 if (command_id == IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE &&
335 integrate_spelling_service_.IsUserModifiable()) {
336 // When the user enables autocorrect, we'll need to make sure that we can
337 // ask Google for suggestions since that service is required. So we show
338 // the bubble and just make sure to enable autocorrect as well.
339 if (!integrate_spelling_service_.GetValue()) {
340 content::RenderViewHost* rvh = proxy_->GetRenderViewHost();
341 gfx::Rect rect = rvh->GetView()->GetViewBounds();
342 scoped_ptr<SpellingBubbleModel> model(
343 new SpellingBubbleModel(profile, proxy_->GetWebContents(), true));
344 chrome::ShowConfirmBubble(
345 proxy_->GetWebContents()->GetTopLevelNativeWindow(),
346 rvh->GetView()->GetNativeView(),
347 gfx::Point(rect.CenterPoint().x(), rect.y()),
348 model.Pass());
349 } else {
350 if (profile) {
351 bool current_value = autocorrect_spelling_.GetValue();
352 profile->GetPrefs()->SetBoolean(prefs::kEnableAutoSpellCorrect,
353 !current_value);
359 void SpellingMenuObserver::OnMenuCancel() {
360 content::BrowserContext* browser_context = proxy_->GetBrowserContext();
361 if (!browser_context)
362 return;
363 SpellcheckService* spellcheck =
364 SpellcheckServiceFactory::GetForContext(browser_context);
365 if (!spellcheck)
366 return;
367 spellcheck->GetFeedbackSender()->IgnoredSuggestions(misspelling_hash_);
370 void SpellingMenuObserver::OnTextCheckComplete(
371 SpellingServiceClient::ServiceType type,
372 bool success,
373 const base::string16& text,
374 const std::vector<SpellCheckResult>& results) {
375 animation_timer_.Stop();
377 // Scan the text-check results and replace the misspelled regions with
378 // suggested words. If the replaced text is included in the suggestion list
379 // provided by the local spellchecker, we show a "No suggestions from Google"
380 // message.
381 succeeded_ = success;
382 if (results.empty()) {
383 succeeded_ = false;
384 } else {
385 typedef std::vector<SpellCheckResult> SpellCheckResults;
386 for (SpellCheckResults::const_iterator it = results.begin();
387 it != results.end(); ++it) {
388 result_.replace(it->location, it->length, it->replacement);
390 base::string16 result = base::i18n::ToLower(result_);
391 for (std::vector<base::string16>::const_iterator it = suggestions_.begin();
392 it != suggestions_.end(); ++it) {
393 if (result == base::i18n::ToLower(*it)) {
394 succeeded_ = false;
395 break;
399 if (type != SpellingServiceClient::SPELLCHECK) {
400 if (!succeeded_) {
401 result_ = l10n_util::GetStringUTF16(
402 IDS_CONTENT_CONTEXT_SPELLING_NO_SUGGESTIONS_FROM_GOOGLE);
405 // Update the menu item with the result text. We disable this item and hide
406 // it when the spelling service does not provide valid suggestions.
407 proxy_->UpdateMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, succeeded_,
408 false, result_);
412 void SpellingMenuObserver::OnAnimationTimerExpired() {
413 // Append '.' characters to the end of "Checking".
414 loading_frame_ = (loading_frame_ + 1) & 3;
415 base::string16 loading_message =
416 loading_message_ + base::string16(loading_frame_,'.');
418 // Update the menu item with the text. We disable this item to prevent users
419 // from selecting it.
420 proxy_->UpdateMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, false, false,
421 loading_message);