Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / tab_contents / spelling_menu_observer.cc
blobc1eb6cfff926eb53c67dfba102aa72e6f0ab979a
1 // Copyright (c) 2012 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/tab_contents/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/spellchecker/spellcheck_factory.h"
15 #include "chrome/browser/spellchecker/spellcheck_host_metrics.h"
16 #include "chrome/browser/spellchecker/spellcheck_platform_mac.h"
17 #include "chrome/browser/spellchecker/spellcheck_service.h"
18 #include "chrome/browser/spellchecker/spelling_service_client.h"
19 #include "chrome/browser/tab_contents/render_view_context_menu.h"
20 #include "chrome/browser/tab_contents/spelling_bubble_model.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/browser/web_contents_view.h"
29 #include "content/public/common/context_menu_params.h"
30 #include "extensions/browser/view_type_utils.h"
31 #include "grit/generated_resources.h"
32 #include "ui/base/l10n/l10n_util.h"
33 #include "ui/gfx/rect.h"
35 using content::BrowserThread;
37 SpellingMenuObserver::SpellingMenuObserver(RenderViewContextMenuProxy* proxy)
38 : proxy_(proxy),
39 loading_frame_(0),
40 succeeded_(false),
41 misspelling_hash_(0),
42 client_(new SpellingServiceClient) {
43 if (proxy && proxy->GetProfile()) {
44 integrate_spelling_service_.Init(prefs::kSpellCheckUseSpellingService,
45 proxy->GetProfile()->GetPrefs());
46 autocorrect_spelling_.Init(prefs::kEnableAutoSpellCorrect,
47 proxy->GetProfile()->GetPrefs());
51 SpellingMenuObserver::~SpellingMenuObserver() {
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());
59 // Exit if we are not in an editable element because we add a menu item only
60 // for editable elements.
61 Profile* profile = proxy_->GetProfile();
62 if (!params.is_editable || !profile)
63 return;
65 // Exit if there is no misspelled word.
66 if (params.misspelled_word.empty())
67 return;
69 suggestions_ = params.dictionary_suggestions;
70 misspelled_word_ = params.misspelled_word;
71 misspelling_hash_ = params.misspelling_hash;
73 bool use_suggestions = SpellingServiceClient::IsAvailable(
74 profile, SpellingServiceClient::SUGGEST);
76 if (!suggestions_.empty() || use_suggestions)
77 proxy_->AddSeparator();
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]);
87 // The service types |SpellingServiceClient::SPELLCHECK| and
88 // |SpellingServiceClient::SUGGEST| are mutually exclusive. Only one is
89 // available at at time.
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.
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;
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 profile, SpellingServiceClient::SUGGEST, params.misspelled_word,
135 base::Bind(&SpellingMenuObserver::OnTextCheckComplete,
136 base::Unretained(this), SpellingServiceClient::SUGGEST));
137 if (result) {
138 loading_frame_ = 0;
139 animation_timer_.Start(FROM_HERE, base::TimeDelta::FromSeconds(1),
140 this, &SpellingMenuObserver::OnAnimationTimerExpired);
144 if (params.dictionary_suggestions.empty()) {
145 proxy_->AddMenuItem(
146 IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS,
147 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS));
148 bool use_spelling_service = SpellingServiceClient::IsAvailable(
149 profile, SpellingServiceClient::SPELLCHECK);
150 if (use_suggestions || use_spelling_service)
151 proxy_->AddSeparator();
152 } else {
153 proxy_->AddSeparator();
155 // |spellcheck_service| can be null when the suggested word is
156 // provided by Web SpellCheck API.
157 SpellcheckService* spellcheck_service =
158 SpellcheckServiceFactory::GetForContext(profile);
159 if (spellcheck_service && spellcheck_service->GetMetrics())
160 spellcheck_service->GetMetrics()->RecordSuggestionStats(1);
163 // If word is misspelled, give option for "Add to dictionary" and a check item
164 // "Ask Google for suggestions".
165 proxy_->AddMenuItem(IDC_SPELLCHECK_ADD_TO_DICTIONARY,
166 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_ADD_TO_DICTIONARY));
168 #if defined(TOOLKIT_GTK)
169 extensions::ViewType view_type =
170 extensions::GetViewType(proxy_->GetWebContents());
171 if (view_type != extensions::VIEW_TYPE_PANEL) {
172 #endif
173 proxy_->AddCheckItem(IDC_CONTENT_CONTEXT_SPELLING_TOGGLE,
174 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_ASK_GOOGLE));
175 #if defined(TOOLKIT_GTK)
177 #endif
179 const CommandLine* command_line = 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)
191 return true;
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:
199 return true;
201 default:
202 return false;
204 return false;
207 bool SpellingMenuObserver::IsCommandIdChecked(int command_id) {
208 DCHECK(IsCommandIdSupported(command_id));
210 if (command_id == IDC_CONTENT_CONTEXT_SPELLING_TOGGLE)
211 return integrate_spelling_service_.GetValue() &&
212 !proxy_->GetProfile()->IsOffTheRecord();
213 else if (command_id == IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE)
214 return autocorrect_spelling_.GetValue() &&
215 !proxy_->GetProfile()->IsOffTheRecord();
216 return false;
219 bool SpellingMenuObserver::IsCommandIdEnabled(int command_id) {
220 DCHECK(IsCommandIdSupported(command_id));
222 if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 &&
223 command_id <= IDC_SPELLCHECK_SUGGESTION_LAST)
224 return true;
226 switch (command_id) {
227 case IDC_SPELLCHECK_ADD_TO_DICTIONARY:
228 return !misspelled_word_.empty();
230 case IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS:
231 return false;
233 case IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION:
234 return succeeded_;
236 case IDC_CONTENT_CONTEXT_SPELLING_TOGGLE:
237 return integrate_spelling_service_.IsUserModifiable() &&
238 !proxy_->GetProfile()->IsOffTheRecord();
240 case IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE:
241 return integrate_spelling_service_.IsUserModifiable() &&
242 !proxy_->GetProfile()->IsOffTheRecord();
244 default:
245 return false;
247 return false;
250 void SpellingMenuObserver::ExecuteCommand(int command_id) {
251 DCHECK(IsCommandIdSupported(command_id));
253 if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 &&
254 command_id <= IDC_SPELLCHECK_SUGGESTION_LAST) {
255 int suggestion_index = command_id - IDC_SPELLCHECK_SUGGESTION_0;
256 proxy_->GetRenderViewHost()->ReplaceMisspelling(
257 suggestions_[suggestion_index]);
258 // GetSpellCheckHost() can return null when the suggested word is provided
259 // by Web SpellCheck API.
260 Profile* profile = proxy_->GetProfile();
261 if (profile) {
262 SpellcheckService* spellcheck =
263 SpellcheckServiceFactory::GetForContext(profile);
264 if (spellcheck) {
265 if (spellcheck->GetMetrics())
266 spellcheck->GetMetrics()->RecordReplacedWordStats(1);
267 spellcheck->GetFeedbackSender()->SelectedSuggestion(
268 misspelling_hash_, suggestion_index);
271 return;
274 // When we choose the suggestion sent from the Spelling service, we replace
275 // the misspelled word with the suggestion and add it to our custom-word
276 // dictionary so this word is not marked as misspelled any longer.
277 if (command_id == IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION) {
278 proxy_->GetRenderViewHost()->ReplaceMisspelling(result_);
279 misspelled_word_ = result_;
282 if (command_id == IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION ||
283 command_id == IDC_SPELLCHECK_ADD_TO_DICTIONARY) {
284 // GetHostForProfile() can return null when the suggested word is provided
285 // by Web SpellCheck API.
286 Profile* profile = proxy_->GetProfile();
287 if (profile) {
288 SpellcheckService* spellcheck =
289 SpellcheckServiceFactory::GetForContext(profile);
290 if (spellcheck) {
291 spellcheck->GetCustomDictionary()->AddWord(base::UTF16ToUTF8(
292 misspelled_word_));
293 spellcheck->GetFeedbackSender()->AddedToDictionary(misspelling_hash_);
296 #if defined(OS_MACOSX)
297 spellcheck_mac::AddWord(misspelled_word_);
298 #endif
301 // The spelling service can be toggled by the user only if it is not managed.
302 if (command_id == IDC_CONTENT_CONTEXT_SPELLING_TOGGLE &&
303 integrate_spelling_service_.IsUserModifiable()) {
304 // When a user enables the "Ask Google for spelling suggestions" item, we
305 // show a bubble to confirm it. On the other hand, when a user disables this
306 // item, we directly update/ the profile and stop integrating the spelling
307 // service immediately.
308 if (!integrate_spelling_service_.GetValue()) {
309 content::RenderViewHost* rvh = proxy_->GetRenderViewHost();
310 gfx::Rect rect = rvh->GetView()->GetViewBounds();
311 chrome::ShowConfirmBubble(
312 #if defined(TOOLKIT_VIEWS)
313 proxy_->GetWebContents()->GetView()->GetTopLevelNativeWindow(),
314 #else
315 rvh->GetView()->GetNativeView(),
316 #endif
317 gfx::Point(rect.CenterPoint().x(), rect.y()),
318 new SpellingBubbleModel(proxy_->GetProfile(),
319 proxy_->GetWebContents(),
320 false));
321 } else {
322 Profile* profile = proxy_->GetProfile();
323 if (profile)
324 profile->GetPrefs()->SetBoolean(prefs::kSpellCheckUseSpellingService,
325 false);
326 profile->GetPrefs()->SetBoolean(prefs::kEnableAutoSpellCorrect,
327 false);
330 // Autocorrect requires use of the spelling service and the spelling service
331 // can be toggled by the user only if it is not managed.
332 if (command_id == IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE &&
333 integrate_spelling_service_.IsUserModifiable()) {
334 // When the user enables autocorrect, we'll need to make sure that we can
335 // ask Google for suggestions since that service is required. So we show
336 // the bubble and just make sure to enable autocorrect as well.
337 if (!integrate_spelling_service_.GetValue()) {
338 content::RenderViewHost* rvh = proxy_->GetRenderViewHost();
339 gfx::Rect rect = rvh->GetView()->GetViewBounds();
340 chrome::ShowConfirmBubble(rvh->GetView()->GetNativeView(),
341 gfx::Point(rect.CenterPoint().x(), rect.y()),
342 new SpellingBubbleModel(
343 proxy_->GetProfile(),
344 proxy_->GetWebContents(),
345 true));
346 } else {
347 Profile* profile = proxy_->GetProfile();
348 if (profile) {
349 bool current_value = autocorrect_spelling_.GetValue();
350 profile->GetPrefs()->SetBoolean(prefs::kEnableAutoSpellCorrect,
351 !current_value);
357 void SpellingMenuObserver::OnMenuCancel() {
358 Profile* profile = proxy_->GetProfile();
359 if (!profile)
360 return;
361 SpellcheckService* spellcheck =
362 SpellcheckServiceFactory::GetForContext(profile);
363 if (!spellcheck)
364 return;
365 spellcheck->GetFeedbackSender()->IgnoredSuggestions(misspelling_hash_);
368 void SpellingMenuObserver::OnTextCheckComplete(
369 SpellingServiceClient::ServiceType type,
370 bool success,
371 const base::string16& text,
372 const std::vector<SpellCheckResult>& results) {
373 animation_timer_.Stop();
375 // Scan the text-check results and replace the misspelled regions with
376 // suggested words. If the replaced text is included in the suggestion list
377 // provided by the local spellchecker, we show a "No suggestions from Google"
378 // message.
379 succeeded_ = success;
380 if (results.empty()) {
381 succeeded_ = false;
382 } else {
383 typedef std::vector<SpellCheckResult> SpellCheckResults;
384 for (SpellCheckResults::const_iterator it = results.begin();
385 it != results.end(); ++it) {
386 result_.replace(it->location, it->length, it->replacement);
388 base::string16 result = base::i18n::ToLower(result_);
389 for (std::vector<base::string16>::const_iterator it = suggestions_.begin();
390 it != suggestions_.end(); ++it) {
391 if (result == base::i18n::ToLower(*it)) {
392 succeeded_ = false;
393 break;
397 if (type != SpellingServiceClient::SPELLCHECK) {
398 if (!succeeded_) {
399 result_ = l10n_util::GetStringUTF16(
400 IDS_CONTENT_CONTEXT_SPELLING_NO_SUGGESTIONS_FROM_GOOGLE);
403 // Update the menu item with the result text. We disable this item and hide
404 // it when the spelling service does not provide valid suggestions.
405 proxy_->UpdateMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, succeeded_,
406 false, result_);
410 void SpellingMenuObserver::OnAnimationTimerExpired() {
411 // Append '.' characters to the end of "Checking".
412 loading_frame_ = (loading_frame_ + 1) & 3;
413 base::string16 loading_message =
414 loading_message_ + base::string16(loading_frame_,'.');
416 // Update the menu item with the text. We disable this item to prevent users
417 // from selecting it.
418 proxy_->UpdateMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, false, false,
419 loading_message);