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. 4 5 #include "chrome/browser/renderer_context_menu/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/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" 33 34 using content::BrowserThread; 35 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()); 47 } 48 } 49 50 SpellingMenuObserver::~SpellingMenuObserver() { 51 } 52 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()); 57 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; 63 64 // Exit if there is no misspelled word. 65 if (params.misspelled_word.empty()) 66 return; 67 68 suggestions_ = params.dictionary_suggestions; 69 misspelled_word_ = params.misspelled_word; 70 misspelling_hash_ = params.misspelling_hash; 71 72 bool use_suggestions = SpellingServiceClient::IsAvailable( 73 profile, SpellingServiceClient::SUGGEST); 74 75 if (!suggestions_.empty() || use_suggestions) 76 proxy_->AddSeparator(); 77 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]); 84 } 85 86 // The service types |SpellingServiceClient::SPELLCHECK| and 87 // |SpellingServiceClient::SUGGEST| are mutually exclusive. Only one is 88 // available at at time. 89 // 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. 97 // 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; 120 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); 140 } 141 } 142 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(); 153 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); 160 } 161 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)); 166 167 proxy_->AddCheckItem(IDC_CONTENT_CONTEXT_SPELLING_TOGGLE, 168 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_ASK_GOOGLE)); 169 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)); 174 } 175 176 proxy_->AddSeparator(); 177 } 178 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; 183 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; 191 192 default: 193 return false; 194 } 195 } 196 197 bool SpellingMenuObserver::IsCommandIdChecked(int command_id) { 198 DCHECK(IsCommandIdSupported(command_id)); 199 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; 207 } 208 209 bool SpellingMenuObserver::IsCommandIdEnabled(int command_id) { 210 DCHECK(IsCommandIdSupported(command_id)); 211 212 if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 && 213 command_id <= IDC_SPELLCHECK_SUGGESTION_LAST) 214 return true; 215 216 switch (command_id) { 217 case IDC_SPELLCHECK_ADD_TO_DICTIONARY: 218 return !misspelled_word_.empty(); 219 220 case IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS: 221 return false; 222 223 case IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION: 224 return succeeded_; 225 226 case IDC_CONTENT_CONTEXT_SPELLING_TOGGLE: 227 return integrate_spelling_service_.IsUserModifiable() && 228 !proxy_->GetProfile()->IsOffTheRecord(); 229 230 case IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE: 231 return integrate_spelling_service_.IsUserModifiable() && 232 !proxy_->GetProfile()->IsOffTheRecord(); 233 234 default: 235 return false; 236 } 237 } 238 239 void SpellingMenuObserver::ExecuteCommand(int command_id) { 240 DCHECK(IsCommandIdSupported(command_id)); 241 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); 258 } 259 } 260 return; 261 } 262 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_; 269 } 270 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_); 283 } 284 } 285 #if defined(OS_MACOSX) 286 spellcheck_mac::AddWord(misspelled_word_); 287 #endif 288 } 289 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); 317 } 318 } 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); 341 } 342 } 343 } 344 } 345 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_); 355 } 356 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(); 363 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); 376 } 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; 383 } 384 } 385 } 386 if (type != SpellingServiceClient::SPELLCHECK) { 387 if (!succeeded_) { 388 result_ = l10n_util::GetStringUTF16( 389 IDS_CONTENT_CONTEXT_SPELLING_NO_SUGGESTIONS_FROM_GOOGLE); 390 } 391 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_); 396 } 397 } 398 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_,'.'); 404 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); 409 } 410