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 "chrome/grit/generated_resources.h" 26#include "content/public/browser/render_view_host.h" 27#include "content/public/browser/render_widget_host_view.h" 28#include "content/public/browser/web_contents.h" 29#include "content/public/common/context_menu_params.h" 30#include "extensions/browser/view_type_utils.h" 31#include "ui/base/l10n/l10n_util.h" 32#include "ui/gfx/rect.h" 33 34using content::BrowserThread; 35 36SpellingMenuObserver::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_->GetBrowserContext()) { 43 Profile* profile = Profile::FromBrowserContext(proxy_->GetBrowserContext()); 44 integrate_spelling_service_.Init(prefs::kSpellCheckUseSpellingService, 45 profile->GetPrefs()); 46 autocorrect_spelling_.Init(prefs::kEnableAutoSpellCorrect, 47 profile->GetPrefs()); 48 } 49} 50 51SpellingMenuObserver::~SpellingMenuObserver() { 52} 53 54void 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 content::BrowserContext* browser_context = proxy_->GetBrowserContext(); 62 if (!params.is_editable || !browser_context) 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 browser_context, 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 browser_context, 135 SpellingServiceClient::SUGGEST, 136 params.misspelled_word, 137 base::Bind(&SpellingMenuObserver::OnTextCheckComplete, 138 base::Unretained(this), 139 SpellingServiceClient::SUGGEST)); 140 if (result) { 141 loading_frame_ = 0; 142 animation_timer_.Start(FROM_HERE, base::TimeDelta::FromSeconds(1), 143 this, &SpellingMenuObserver::OnAnimationTimerExpired); 144 } 145 } 146 147 if (params.dictionary_suggestions.empty()) { 148 proxy_->AddMenuItem( 149 IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS, 150 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS)); 151 bool use_spelling_service = SpellingServiceClient::IsAvailable( 152 browser_context, SpellingServiceClient::SPELLCHECK); 153 if (use_suggestions || use_spelling_service) 154 proxy_->AddSeparator(); 155 } else { 156 proxy_->AddSeparator(); 157 158 // |spellcheck_service| can be null when the suggested word is 159 // provided by Web SpellCheck API. 160 SpellcheckService* spellcheck_service = 161 SpellcheckServiceFactory::GetForContext(browser_context); 162 if (spellcheck_service && spellcheck_service->GetMetrics()) 163 spellcheck_service->GetMetrics()->RecordSuggestionStats(1); 164 } 165 166 // If word is misspelled, give option for "Add to dictionary" and a check item 167 // "Ask Google for suggestions". 168 proxy_->AddMenuItem(IDC_SPELLCHECK_ADD_TO_DICTIONARY, 169 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_ADD_TO_DICTIONARY)); 170 171 proxy_->AddCheckItem(IDC_CONTENT_CONTEXT_SPELLING_TOGGLE, 172 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_ASK_GOOGLE)); 173 174 const CommandLine* command_line = CommandLine::ForCurrentProcess(); 175 if (command_line->HasSwitch(switches::kEnableSpellingAutoCorrect)) { 176 proxy_->AddCheckItem(IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE, 177 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_AUTOCORRECT)); 178 } 179 180 proxy_->AddSeparator(); 181} 182 183bool SpellingMenuObserver::IsCommandIdSupported(int command_id) { 184 if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 && 185 command_id <= IDC_SPELLCHECK_SUGGESTION_LAST) 186 return true; 187 188 switch (command_id) { 189 case IDC_SPELLCHECK_ADD_TO_DICTIONARY: 190 case IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS: 191 case IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION: 192 case IDC_CONTENT_CONTEXT_SPELLING_TOGGLE: 193 case IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE: 194 return true; 195 196 default: 197 return false; 198 } 199} 200 201bool SpellingMenuObserver::IsCommandIdChecked(int command_id) { 202 DCHECK(IsCommandIdSupported(command_id)); 203 Profile* profile = Profile::FromBrowserContext(proxy_->GetBrowserContext()); 204 205 if (command_id == IDC_CONTENT_CONTEXT_SPELLING_TOGGLE) 206 return integrate_spelling_service_.GetValue() && !profile->IsOffTheRecord(); 207 if (command_id == IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE) 208 return autocorrect_spelling_.GetValue() && !profile->IsOffTheRecord(); 209 return false; 210} 211 212bool SpellingMenuObserver::IsCommandIdEnabled(int command_id) { 213 DCHECK(IsCommandIdSupported(command_id)); 214 215 if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 && 216 command_id <= IDC_SPELLCHECK_SUGGESTION_LAST) 217 return true; 218 219 Profile* profile = Profile::FromBrowserContext(proxy_->GetBrowserContext()); 220 switch (command_id) { 221 case IDC_SPELLCHECK_ADD_TO_DICTIONARY: 222 return !misspelled_word_.empty(); 223 224 case IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS: 225 return false; 226 227 case IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION: 228 return succeeded_; 229 230 case IDC_CONTENT_CONTEXT_SPELLING_TOGGLE: 231 return integrate_spelling_service_.IsUserModifiable() && 232 !profile->IsOffTheRecord(); 233 234 case IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE: 235 return integrate_spelling_service_.IsUserModifiable() && 236 !profile->IsOffTheRecord(); 237 238 default: 239 return false; 240 } 241} 242 243void SpellingMenuObserver::ExecuteCommand(int command_id) { 244 DCHECK(IsCommandIdSupported(command_id)); 245 246 if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 && 247 command_id <= IDC_SPELLCHECK_SUGGESTION_LAST) { 248 int suggestion_index = command_id - IDC_SPELLCHECK_SUGGESTION_0; 249 proxy_->GetWebContents()->ReplaceMisspelling( 250 suggestions_[suggestion_index]); 251 // GetSpellCheckHost() can return null when the suggested word is provided 252 // by Web SpellCheck API. 253 content::BrowserContext* browser_context = proxy_->GetBrowserContext(); 254 if (browser_context) { 255 SpellcheckService* spellcheck = 256 SpellcheckServiceFactory::GetForContext(browser_context); 257 if (spellcheck) { 258 if (spellcheck->GetMetrics()) 259 spellcheck->GetMetrics()->RecordReplacedWordStats(1); 260 spellcheck->GetFeedbackSender()->SelectedSuggestion( 261 misspelling_hash_, suggestion_index); 262 } 263 } 264 return; 265 } 266 267 // When we choose the suggestion sent from the Spelling service, we replace 268 // the misspelled word with the suggestion and add it to our custom-word 269 // dictionary so this word is not marked as misspelled any longer. 270 if (command_id == IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION) { 271 proxy_->GetWebContents()->ReplaceMisspelling(result_); 272 misspelled_word_ = result_; 273 } 274 275 if (command_id == IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION || 276 command_id == IDC_SPELLCHECK_ADD_TO_DICTIONARY) { 277 // GetHostForProfile() can return null when the suggested word is provided 278 // by Web SpellCheck API. 279 content::BrowserContext* browser_context = proxy_->GetBrowserContext(); 280 if (browser_context) { 281 SpellcheckService* spellcheck = 282 SpellcheckServiceFactory::GetForContext(browser_context); 283 if (spellcheck) { 284 spellcheck->GetCustomDictionary()->AddWord(base::UTF16ToUTF8( 285 misspelled_word_)); 286 spellcheck->GetFeedbackSender()->AddedToDictionary(misspelling_hash_); 287 } 288 } 289#if defined(OS_MACOSX) 290 spellcheck_mac::AddWord(misspelled_word_); 291#endif 292 } 293 294 Profile* profile = Profile::FromBrowserContext(proxy_->GetBrowserContext()); 295 296 // The spelling service can be toggled by the user only if it is not managed. 297 if (command_id == IDC_CONTENT_CONTEXT_SPELLING_TOGGLE && 298 integrate_spelling_service_.IsUserModifiable()) { 299 // When a user enables the "Ask Google for spelling suggestions" item, we 300 // show a bubble to confirm it. On the other hand, when a user disables this 301 // item, we directly update/ the profile and stop integrating the spelling 302 // service immediately. 303 if (!integrate_spelling_service_.GetValue()) { 304 content::RenderViewHost* rvh = proxy_->GetRenderViewHost(); 305 gfx::Rect rect = rvh->GetView()->GetViewBounds(); 306 chrome::ShowConfirmBubble( 307 proxy_->GetWebContents()->GetTopLevelNativeWindow(), 308 rvh->GetView()->GetNativeView(), 309 gfx::Point(rect.CenterPoint().x(), rect.y()), 310 new SpellingBubbleModel(profile, proxy_->GetWebContents(), false)); 311 } else { 312 if (profile) { 313 profile->GetPrefs()->SetBoolean(prefs::kSpellCheckUseSpellingService, 314 false); 315 profile->GetPrefs()->SetBoolean(prefs::kEnableAutoSpellCorrect, 316 false); 317 } 318 } 319 } 320 // Autocorrect requires use of the spelling service and the spelling service 321 // can be toggled by the user only if it is not managed. 322 if (command_id == IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE && 323 integrate_spelling_service_.IsUserModifiable()) { 324 // When the user enables autocorrect, we'll need to make sure that we can 325 // ask Google for suggestions since that service is required. So we show 326 // the bubble and just make sure to enable autocorrect as well. 327 if (!integrate_spelling_service_.GetValue()) { 328 content::RenderViewHost* rvh = proxy_->GetRenderViewHost(); 329 gfx::Rect rect = rvh->GetView()->GetViewBounds(); 330 chrome::ShowConfirmBubble( 331 proxy_->GetWebContents()->GetTopLevelNativeWindow(), 332 rvh->GetView()->GetNativeView(), 333 gfx::Point(rect.CenterPoint().x(), rect.y()), 334 new SpellingBubbleModel(profile, proxy_->GetWebContents(), true)); 335 } else { 336 if (profile) { 337 bool current_value = autocorrect_spelling_.GetValue(); 338 profile->GetPrefs()->SetBoolean(prefs::kEnableAutoSpellCorrect, 339 !current_value); 340 } 341 } 342 } 343} 344 345void SpellingMenuObserver::OnMenuCancel() { 346 content::BrowserContext* browser_context = proxy_->GetBrowserContext(); 347 if (!browser_context) 348 return; 349 SpellcheckService* spellcheck = 350 SpellcheckServiceFactory::GetForContext(browser_context); 351 if (!spellcheck) 352 return; 353 spellcheck->GetFeedbackSender()->IgnoredSuggestions(misspelling_hash_); 354} 355 356void SpellingMenuObserver::OnTextCheckComplete( 357 SpellingServiceClient::ServiceType type, 358 bool success, 359 const base::string16& text, 360 const std::vector<SpellCheckResult>& results) { 361 animation_timer_.Stop(); 362 363 // Scan the text-check results and replace the misspelled regions with 364 // suggested words. If the replaced text is included in the suggestion list 365 // provided by the local spellchecker, we show a "No suggestions from Google" 366 // message. 367 succeeded_ = success; 368 if (results.empty()) { 369 succeeded_ = false; 370 } else { 371 typedef std::vector<SpellCheckResult> SpellCheckResults; 372 for (SpellCheckResults::const_iterator it = results.begin(); 373 it != results.end(); ++it) { 374 result_.replace(it->location, it->length, it->replacement); 375 } 376 base::string16 result = base::i18n::ToLower(result_); 377 for (std::vector<base::string16>::const_iterator it = suggestions_.begin(); 378 it != suggestions_.end(); ++it) { 379 if (result == base::i18n::ToLower(*it)) { 380 succeeded_ = false; 381 break; 382 } 383 } 384 } 385 if (type != SpellingServiceClient::SPELLCHECK) { 386 if (!succeeded_) { 387 result_ = l10n_util::GetStringUTF16( 388 IDS_CONTENT_CONTEXT_SPELLING_NO_SUGGESTIONS_FROM_GOOGLE); 389 } 390 391 // Update the menu item with the result text. We disable this item and hide 392 // it when the spelling service does not provide valid suggestions. 393 proxy_->UpdateMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, succeeded_, 394 false, result_); 395 } 396} 397 398void SpellingMenuObserver::OnAnimationTimerExpired() { 399 // Append '.' characters to the end of "Checking". 400 loading_frame_ = (loading_frame_ + 1) & 3; 401 base::string16 loading_message = 402 loading_message_ + base::string16(loading_frame_,'.'); 403 404 // Update the menu item with the text. We disable this item to prevent users 405 // from selecting it. 406 proxy_->UpdateMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, false, false, 407 loading_message); 408} 409