| OLD | NEW |
| (Empty) |
| 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(base::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 base::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 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; | |
| 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 base::string16 loading_message = | |
| 414 loading_message_ + base::string16(loading_frame_,'.'); | |
| 415 | |
| 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); | |
| 420 } | |
| OLD | NEW |