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::GetForProfile(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::GetForProfile(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::GetForProfile(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::GetForProfile(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 } 420