[Metrics] Make MetricsStateManager take a callback param to check if UMA is enabled.
[chromium-blink-merge.git] / chrome / browser / renderer_context_menu / spelling_menu_observer.cc
blobbbce83db2019431034848424b1845074c96db150
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/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"
34 using content::BrowserThread;
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());
50 SpellingMenuObserver::~SpellingMenuObserver() {
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());
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;
64 // Exit if there is no misspelled word.
65 if (params.misspelled_word.empty())
66 return;
68 suggestions_ = params.dictionary_suggestions;
69 misspelled_word_ = params.misspelled_word;
70 misspelling_hash_ = params.misspelling_hash;
72 bool use_suggestions = SpellingServiceClient::IsAvailable(
73 profile, SpellingServiceClient::SUGGEST);
75 if (!suggestions_.empty() || use_suggestions)
76 proxy_->AddSeparator();
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]);
86 // The service types |SpellingServiceClient::SPELLCHECK| and
87 // |SpellingServiceClient::SUGGEST| are mutually exclusive. Only one is
88 // available at at time.
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.
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;
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);
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();
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);
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));
167 proxy_->AddCheckItem(IDC_CONTENT_CONTEXT_SPELLING_TOGGLE,
168 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_ASK_GOOGLE));
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));
176 proxy_->AddSeparator();
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;
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;
192 default:
193 return false;
197 bool SpellingMenuObserver::IsCommandIdChecked(int command_id) {
198 DCHECK(IsCommandIdSupported(command_id));
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;
209 bool SpellingMenuObserver::IsCommandIdEnabled(int command_id) {
210 DCHECK(IsCommandIdSupported(command_id));
212 if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 &&
213 command_id <= IDC_SPELLCHECK_SUGGESTION_LAST)
214 return true;
216 switch (command_id) {
217 case IDC_SPELLCHECK_ADD_TO_DICTIONARY:
218 return !misspelled_word_.empty();
220 case IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS:
221 return false;
223 case IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION:
224 return succeeded_;
226 case IDC_CONTENT_CONTEXT_SPELLING_TOGGLE:
227 return integrate_spelling_service_.IsUserModifiable() &&
228 !proxy_->GetProfile()->IsOffTheRecord();
230 case IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE:
231 return integrate_spelling_service_.IsUserModifiable() &&
232 !proxy_->GetProfile()->IsOffTheRecord();
234 default:
235 return false;
239 void SpellingMenuObserver::ExecuteCommand(int command_id) {
240 DCHECK(IsCommandIdSupported(command_id));
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);
260 return;
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_;
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_);
285 #if defined(OS_MACOSX)
286 spellcheck_mac::AddWord(misspelled_word_);
287 #endif
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);
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);
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_);
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();
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);
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;
386 if (type != SpellingServiceClient::SPELLCHECK) {
387 if (!succeeded_) {
388 result_ = l10n_util::GetStringUTF16(
389 IDS_CONTENT_CONTEXT_SPELLING_NO_SUGGESTIONS_FROM_GOOGLE);
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_);
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_,'.');
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);