- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / tab_contents / spelling_menu_observer.cc
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.
4
5 #include "chrome/browser/tab_contents/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/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"
34
35 using content::BrowserThread;
36
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());
48   }
49 }
50
51 SpellingMenuObserver::~SpellingMenuObserver() {
52 }
53
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());
58
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;
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       profile, 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         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);
141     }
142   }
143
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();
154
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);
161   }
162
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));
167
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)
176   }
177 #endif
178
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));
183   }
184
185   proxy_->AddSeparator();
186 }
187
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;
192
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;
200
201     default:
202       return false;
203   }
204   return false;
205 }
206
207 bool SpellingMenuObserver::IsCommandIdChecked(int command_id) {
208   DCHECK(IsCommandIdSupported(command_id));
209
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;
217 }
218
219 bool SpellingMenuObserver::IsCommandIdEnabled(int command_id) {
220   DCHECK(IsCommandIdSupported(command_id));
221
222   if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 &&
223       command_id <= IDC_SPELLCHECK_SUGGESTION_LAST)
224     return true;
225
226   switch (command_id) {
227     case IDC_SPELLCHECK_ADD_TO_DICTIONARY:
228       return !misspelled_word_.empty();
229
230     case IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS:
231       return false;
232
233     case IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION:
234       return succeeded_;
235
236     case IDC_CONTENT_CONTEXT_SPELLING_TOGGLE:
237       return integrate_spelling_service_.IsUserModifiable() &&
238           !proxy_->GetProfile()->IsOffTheRecord();
239
240     case IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE:
241       return integrate_spelling_service_.IsUserModifiable() &&
242           !proxy_->GetProfile()->IsOffTheRecord();
243
244     default:
245       return false;
246   }
247   return false;
248 }
249
250 void SpellingMenuObserver::ExecuteCommand(int command_id) {
251   DCHECK(IsCommandIdSupported(command_id));
252
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);
269       }
270     }
271     return;
272   }
273
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_;
280   }
281
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(UTF16ToUTF8(
292             misspelled_word_));
293         spellcheck->GetFeedbackSender()->AddedToDictionary(misspelling_hash_);
294       }
295     }
296 #if defined(OS_MACOSX)
297     spellcheck_mac::AddWord(misspelled_word_);
298 #endif
299   }
300
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);
328     }
329   }
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);
352       }
353     }
354   }
355 }
356
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_);
366 }
367
368 void SpellingMenuObserver::OnTextCheckComplete(
369     SpellingServiceClient::ServiceType type,
370     bool success,
371     const string16& text,
372     const std::vector<SpellCheckResult>& results) {
373   animation_timer_.Stop();
374
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);
387     }
388     string16 result = base::i18n::ToLower(result_);
389     for (std::vector<string16>::const_iterator it = suggestions_.begin();
390          it != suggestions_.end(); ++it) {
391       if (result == base::i18n::ToLower(*it)) {
392         succeeded_ = false;
393         break;
394       }
395     }
396   }
397   if (type != SpellingServiceClient::SPELLCHECK) {
398     if (!succeeded_) {
399       result_ = l10n_util::GetStringUTF16(
400           IDS_CONTENT_CONTEXT_SPELLING_NO_SUGGESTIONS_FROM_GOOGLE);
401     }
402
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_);
407   }
408 }
409
410 void SpellingMenuObserver::OnAnimationTimerExpired() {
411   // Append '.' characters to the end of "Checking".
412   loading_frame_ = (loading_frame_ + 1) & 3;
413   string16 loading_message = loading_message_ + string16(loading_frame_,'.');
414
415   // Update the menu item with the text. We disable this item to prevent users
416   // from selecting it.
417   proxy_->UpdateMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, false, false,
418                          loading_message);
419 }