|  | // Copyright 2014 The Chromium Authors | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "chrome/browser/renderer_context_menu/spelling_menu_observer.h" | 
|  |  | 
|  | #include <utility> | 
|  |  | 
|  | #include "base/barrier_closure.h" | 
|  | #include "base/bind.h" | 
|  | #include "base/command_line.h" | 
|  | #include "base/i18n/case_conversion.h" | 
|  | #include "base/strings/utf_string_conversions.h" | 
|  | #include "build/build_config.h" | 
|  | #include "chrome/app/chrome_command_ids.h" | 
|  | #include "chrome/browser/profiles/profile.h" | 
|  | #include "chrome/browser/renderer_context_menu/render_view_context_menu.h" | 
|  | #include "chrome/browser/renderer_context_menu/spelling_bubble_model.h" | 
|  | #include "chrome/browser/spellchecker/spellcheck_factory.h" | 
|  | #include "chrome/browser/spellchecker/spellcheck_service.h" | 
|  | #include "chrome/browser/ui/confirm_bubble.h" | 
|  | #include "chrome/common/chrome_switches.h" | 
|  | #include "chrome/common/pref_names.h" | 
|  | #include "chrome/grit/generated_resources.h" | 
|  | #include "components/prefs/pref_service.h" | 
|  | #include "components/spellcheck/browser/pref_names.h" | 
|  | #include "components/spellcheck/browser/spellcheck_host_metrics.h" | 
|  | #include "components/spellcheck/browser/spellcheck_platform.h" | 
|  | #include "components/spellcheck/browser/spelling_service_client.h" | 
|  | #include "components/spellcheck/common/spellcheck_common.h" | 
|  | #include "components/spellcheck/common/spellcheck_features.h" | 
|  | #include "components/spellcheck/common/spellcheck_result.h" | 
|  | #include "components/spellcheck/spellcheck_buildflags.h" | 
|  | #include "content/public/browser/browser_thread.h" | 
|  | #include "content/public/browser/context_menu_params.h" | 
|  | #include "content/public/browser/render_view_host.h" | 
|  | #include "content/public/browser/render_widget_host.h" | 
|  | #include "content/public/browser/render_widget_host_view.h" | 
|  | #include "content/public/browser/web_contents.h" | 
|  | #include "extensions/browser/view_type_utils.h" | 
|  | #include "ui/base/l10n/l10n_util.h" | 
|  | #include "ui/gfx/geometry/rect.h" | 
|  |  | 
|  | using content::BrowserThread; | 
|  |  | 
|  | const int kMaxSpellingSuggestions = 3; | 
|  |  | 
|  | SpellingMenuObserver::SpellingMenuObserver(RenderViewContextMenuProxy* proxy) | 
|  | : proxy_(proxy), | 
|  | loading_frame_(0), | 
|  | succeeded_(false), | 
|  | client_(new SpellingServiceClient) { | 
|  | if (proxy_ && proxy_->GetBrowserContext()) { | 
|  | Profile* profile = Profile::FromBrowserContext(proxy_->GetBrowserContext()); | 
|  | integrate_spelling_service_.Init( | 
|  | spellcheck::prefs::kSpellCheckUseSpellingService, profile->GetPrefs()); | 
|  | } | 
|  | } | 
|  |  | 
|  | SpellingMenuObserver::~SpellingMenuObserver() { | 
|  | } | 
|  |  | 
|  | void SpellingMenuObserver::InitMenu(const content::ContextMenuParams& params) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  | DCHECK(!params.misspelled_word.empty() || | 
|  | params.dictionary_suggestions.empty()); | 
|  |  | 
|  | // Exit if we are not in an editable element because we add a menu item only | 
|  | // for editable elements. | 
|  | content::BrowserContext* browser_context = proxy_->GetBrowserContext(); | 
|  | if (!params.is_editable || !browser_context) | 
|  | return; | 
|  |  | 
|  | // Exit if there is no misspelled word. | 
|  | if (params.misspelled_word.empty()) | 
|  | return; | 
|  |  | 
|  | // Note that for Windows, suggestions_ will initially only contain those | 
|  | // suggestions obtained using Hunspell. | 
|  | suggestions_ = params.dictionary_suggestions; | 
|  | misspelled_word_ = params.misspelled_word; | 
|  |  | 
|  | use_remote_suggestions_ = SpellingServiceClient::IsAvailable( | 
|  | browser_context, SpellingServiceClient::SUGGEST); | 
|  |  | 
|  | #if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) | 
|  | use_platform_suggestions_ = spellcheck::UseBrowserSpellChecker(); | 
|  | if (use_platform_suggestions_) { | 
|  | // Need to asynchronously retrieve suggestions from the platform | 
|  | // spellchecker, which requires the SpellcheckService. | 
|  | SpellcheckService* spellcheck_service = | 
|  | SpellcheckServiceFactory::GetForContext(browser_context); | 
|  | if (!spellcheck_service) | 
|  | return; | 
|  |  | 
|  | // If there are no local suggestions or the spellcheck cloud service isn't | 
|  | // used, this separator will be removed later. | 
|  | proxy_->AddSeparator(); | 
|  |  | 
|  | // Append placeholders for maximum number of suggestions. Note that all but | 
|  | // the first placeholder will be made hidden in OnContextMenuShown override. | 
|  | // It can't be done here because UpdateMenuItem can only be called after | 
|  | // ToolkitDelegateViews is initialized, which happens after | 
|  | // RenderViewContextMenu::InitMenu. | 
|  | for (int i = 0; | 
|  | i < kMaxSpellingSuggestions && | 
|  | IDC_SPELLCHECK_SUGGESTION_0 + i <= IDC_SPELLCHECK_SUGGESTION_LAST; | 
|  | ++i) { | 
|  | proxy_->AddMenuItem(IDC_SPELLCHECK_SUGGESTION_0 + i, | 
|  | /*title=*/std::u16string()); | 
|  | } | 
|  |  | 
|  | // Completion barrier for local (and possibly remote) retrieval of | 
|  | // suggestions. Remote suggestion cannot be displayed until local | 
|  | // suggestions have been retrieved, so that duplicates can be accounted for. | 
|  | completion_barrier_ = base::BarrierClosure( | 
|  | use_remote_suggestions_ ? 2 : 1, | 
|  | base::BindOnce(&SpellingMenuObserver::OnGetSuggestionsComplete, | 
|  | weak_ptr_factory_.GetWeakPtr())); | 
|  |  | 
|  | // Asynchronously retrieve suggestions from the platform spellchecker. | 
|  | spellcheck_platform::GetPerLanguageSuggestions( | 
|  | spellcheck_service->platform_spell_checker(), misspelled_word_, | 
|  | base::BindOnce(&SpellingMenuObserver::OnGetPlatformSuggestionsComplete, | 
|  | weak_ptr_factory_.GetWeakPtr())); | 
|  |  | 
|  | if (use_remote_suggestions_) { | 
|  | // Asynchronously retrieve remote suggestions in parallel. | 
|  | GetRemoteSuggestions(); | 
|  | } else { | 
|  | // Animate first suggestion placeholder while retrieving suggestions. | 
|  | loading_message_ = | 
|  | l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_CHECKING); | 
|  | loading_frame_ = 0; | 
|  | animation_timer_.Start( | 
|  | FROM_HERE, base::Seconds(1), | 
|  | base::BindRepeating(&SpellingMenuObserver::OnAnimationTimerExpired, | 
|  | weak_ptr_factory_.GetWeakPtr(), | 
|  | IDC_SPELLCHECK_SUGGESTION_0)); | 
|  | } | 
|  |  | 
|  | // If there are no suggestions, this separator between suggestions and "Add | 
|  | // to dictionary" will be removed later. | 
|  | proxy_->AddSeparator(); | 
|  | } else { | 
|  | #endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) | 
|  | if (!suggestions_.empty() || use_remote_suggestions_) | 
|  | proxy_->AddSeparator(); | 
|  |  | 
|  | // Append Dictionary spell check suggestions. | 
|  | int length = | 
|  | std::min(kMaxSpellingSuggestions, | 
|  | static_cast<int>(params.dictionary_suggestions.size())); | 
|  | for (int i = 0; i < length && IDC_SPELLCHECK_SUGGESTION_0 + i <= | 
|  | IDC_SPELLCHECK_SUGGESTION_LAST; | 
|  | ++i) { | 
|  | proxy_->AddMenuItem(IDC_SPELLCHECK_SUGGESTION_0 + i, | 
|  | params.dictionary_suggestions[i]); | 
|  | } | 
|  |  | 
|  | if (use_remote_suggestions_) | 
|  | GetRemoteSuggestions(); | 
|  |  | 
|  | if (!params.dictionary_suggestions.empty()) { | 
|  | // |spellcheck_service| can be null when the suggested word is | 
|  | // provided by Web SpellCheck API. | 
|  | SpellcheckService* spellcheck_service = | 
|  | SpellcheckServiceFactory::GetForContext(browser_context); | 
|  | if (spellcheck_service && spellcheck_service->GetMetrics()) | 
|  | spellcheck_service->GetMetrics()->RecordSuggestionStats(1); | 
|  | proxy_->AddSeparator(); | 
|  | } | 
|  | #if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) | 
|  |  | 
|  | // If word is misspelled, give option for "Add to dictionary" and, if | 
|  | // multilingual spellchecking is not enabled, a check item "Ask Google for | 
|  | // suggestions". | 
|  | proxy_->AddMenuItem( | 
|  | IDC_SPELLCHECK_ADD_TO_DICTIONARY, | 
|  | l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_ADD_TO_DICTIONARY)); | 
|  | proxy_->AddSpellCheckServiceItem(integrate_spelling_service_.GetValue()); | 
|  | } | 
|  |  | 
|  | #if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) | 
|  | void SpellingMenuObserver::OnContextMenuShown( | 
|  | const content::ContextMenuParams& /*params*/, | 
|  | const gfx::Rect& /*bounds_in_screen*/) { | 
|  | if (!use_platform_suggestions_) | 
|  | return; | 
|  |  | 
|  | // Disable the first place holder but keep it visible for animation if not | 
|  | // retrieving remote suggestions. Note that UpdateMenuItem does nothing if the | 
|  | // command_id is not found, e.g. if there is an early exit from InitMenu. | 
|  | proxy_->UpdateMenuItem(IDC_SPELLCHECK_SUGGESTION_0, | 
|  | /*enabled=*/false, | 
|  | /*hidden=*/(use_remote_suggestions_ ? true : false), | 
|  | loading_message_); | 
|  |  | 
|  | // Disable and hide the rest of the placeholders until suggestions obtained. | 
|  | for (int i = 1; | 
|  | i < kMaxSpellingSuggestions && | 
|  | IDC_SPELLCHECK_SUGGESTION_0 + i <= IDC_SPELLCHECK_SUGGESTION_LAST; | 
|  | ++i) { | 
|  | proxy_->UpdateMenuItem(IDC_SPELLCHECK_SUGGESTION_0 + i, | 
|  | /*enabled=*/false, /*hidden=*/true, | 
|  | /*title=*/std::u16string()); | 
|  | } | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) | 
|  |  | 
|  | bool SpellingMenuObserver::IsCommandIdSupported(int command_id) { | 
|  | if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 && | 
|  | command_id <= IDC_SPELLCHECK_SUGGESTION_LAST) | 
|  | return true; | 
|  |  | 
|  | switch (command_id) { | 
|  | case IDC_SPELLCHECK_ADD_TO_DICTIONARY: | 
|  | case IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS: | 
|  | case IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION: | 
|  | case IDC_CONTENT_CONTEXT_SPELLING_TOGGLE: | 
|  | return true; | 
|  |  | 
|  | default: | 
|  | return false; | 
|  | } | 
|  | } | 
|  |  | 
|  | bool SpellingMenuObserver::IsCommandIdChecked(int command_id) { | 
|  | DCHECK(IsCommandIdSupported(command_id)); | 
|  | Profile* profile = Profile::FromBrowserContext(proxy_->GetBrowserContext()); | 
|  |  | 
|  | if (command_id == IDC_CONTENT_CONTEXT_SPELLING_TOGGLE) | 
|  | return integrate_spelling_service_.GetValue() && | 
|  | !profile->IsOffTheRecord(); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | bool SpellingMenuObserver::IsCommandIdEnabled(int command_id) { | 
|  | DCHECK(IsCommandIdSupported(command_id)); | 
|  |  | 
|  | if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 && | 
|  | command_id <= IDC_SPELLCHECK_SUGGESTION_LAST) | 
|  | return true; | 
|  |  | 
|  | Profile* profile = Profile::FromBrowserContext(proxy_->GetBrowserContext()); | 
|  | switch (command_id) { | 
|  | case IDC_SPELLCHECK_ADD_TO_DICTIONARY: | 
|  | return !misspelled_word_.empty(); | 
|  |  | 
|  | case IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS: | 
|  | return false; | 
|  |  | 
|  | case IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION: | 
|  | return succeeded_; | 
|  |  | 
|  | case IDC_CONTENT_CONTEXT_SPELLING_TOGGLE: | 
|  | return integrate_spelling_service_.IsUserModifiable() && | 
|  | !profile->IsOffTheRecord(); | 
|  |  | 
|  | default: | 
|  | return false; | 
|  | } | 
|  | } | 
|  |  | 
|  | void SpellingMenuObserver::ExecuteCommand(int command_id) { | 
|  | DCHECK(IsCommandIdSupported(command_id)); | 
|  |  | 
|  | if (command_id >= IDC_SPELLCHECK_SUGGESTION_0 && | 
|  | command_id <= IDC_SPELLCHECK_SUGGESTION_LAST) { | 
|  | int suggestion_index = command_id - IDC_SPELLCHECK_SUGGESTION_0; | 
|  | proxy_->GetWebContents()->ReplaceMisspelling( | 
|  | suggestions_[suggestion_index]); | 
|  | // GetSpellCheckHost() can return null when the suggested word is provided | 
|  | // by Web SpellCheck API. | 
|  | content::BrowserContext* browser_context = proxy_->GetBrowserContext(); | 
|  | if (browser_context) { | 
|  | SpellcheckService* spellcheck = | 
|  | SpellcheckServiceFactory::GetForContext(browser_context); | 
|  | if (spellcheck) { | 
|  | if (spellcheck->GetMetrics()) | 
|  | spellcheck->GetMetrics()->RecordReplacedWordStats(1); | 
|  | } | 
|  | } | 
|  | return; | 
|  | } | 
|  |  | 
|  | // When we choose the suggestion sent from the Spelling service, we replace | 
|  | // the misspelled word with the suggestion and add it to our custom-word | 
|  | // dictionary so this word is not marked as misspelled any longer. | 
|  | if (command_id == IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION) { | 
|  | proxy_->GetWebContents()->ReplaceMisspelling(result_); | 
|  | misspelled_word_ = result_; | 
|  | } | 
|  |  | 
|  | if (command_id == IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION || | 
|  | command_id == IDC_SPELLCHECK_ADD_TO_DICTIONARY) { | 
|  | // GetHostForProfile() can return null when the suggested word is provided | 
|  | // by Web SpellCheck API. | 
|  | content::BrowserContext* browser_context = proxy_->GetBrowserContext(); | 
|  | if (browser_context) { | 
|  | SpellcheckService* spellcheck = | 
|  | SpellcheckServiceFactory::GetForContext(browser_context); | 
|  | if (spellcheck) { | 
|  | spellcheck->GetCustomDictionary()->AddWord(base::UTF16ToUTF8( | 
|  | misspelled_word_)); | 
|  |  | 
|  | #if BUILDFLAG(USE_BROWSER_SPELLCHECKER) | 
|  | if (spellcheck::UseBrowserSpellChecker()) { | 
|  | spellcheck_platform::AddWord(spellcheck->platform_spell_checker(), | 
|  | misspelled_word_); | 
|  | } | 
|  | #endif  // BUILDFLAG(USE_BROWSER_SPELLCHECKER) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | Profile* profile = Profile::FromBrowserContext(proxy_->GetBrowserContext()); | 
|  |  | 
|  | // The spelling service can be toggled by the user only if it is not managed. | 
|  | if (command_id == IDC_CONTENT_CONTEXT_SPELLING_TOGGLE && | 
|  | integrate_spelling_service_.IsUserModifiable()) { | 
|  | bool spellcheckEnabled = | 
|  | profile && | 
|  | profile->GetPrefs()->GetBoolean(spellcheck::prefs::kSpellCheckEnable); | 
|  |  | 
|  | // When a user enables the "Use enhanced spell check" item, we check to see | 
|  | // if the user has spellcheck disabled. If the user has spellcheck disabled | 
|  | // but has already enabled the spelling service, we just enable spellcheck. | 
|  | // If spellcheck is enabled but the spelling service is not, we show a | 
|  | // bubble to confirm it. On the other hand, when a user disables this | 
|  | // item, we directly update/ the profile and stop integrating the spelling | 
|  | // service immediately. | 
|  | if (!spellcheckEnabled && integrate_spelling_service_.GetValue()) { | 
|  | if (profile) { | 
|  | profile->GetPrefs()->SetBoolean(spellcheck::prefs::kSpellCheckEnable, | 
|  | true); | 
|  | } | 
|  | } else if (!integrate_spelling_service_.GetValue()) { | 
|  | content::RenderViewHost* rvh = proxy_->GetRenderViewHost(); | 
|  | gfx::Rect rect = rvh->GetWidget()->GetView()->GetViewBounds(); | 
|  | std::unique_ptr<SpellingBubbleModel> model( | 
|  | new SpellingBubbleModel(profile, proxy_->GetWebContents())); | 
|  | chrome::ShowConfirmBubble( | 
|  | proxy_->GetWebContents()->GetTopLevelNativeWindow(), | 
|  | rvh->GetWidget()->GetView()->GetNativeView(), | 
|  | gfx::Point(rect.CenterPoint().x(), rect.y()), std::move(model)); | 
|  | } else { | 
|  | if (profile) { | 
|  | profile->GetPrefs()->SetBoolean( | 
|  | spellcheck::prefs::kSpellCheckUseSpellingService, false); | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | #if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) | 
|  | void SpellingMenuObserver::OnGetPlatformSuggestionsComplete( | 
|  | const spellcheck::PerLanguageSuggestions& | 
|  | platform_per_language_suggestions) { | 
|  | // Prioritize the platform results, since presumably the first user language | 
|  | // will have a Windows language pack installed. Treat the Hunspell suggestions | 
|  | // as if a single language. | 
|  | spellcheck::PerLanguageSuggestions per_language_suggestions = | 
|  | platform_per_language_suggestions; | 
|  | per_language_suggestions.push_back(suggestions_); | 
|  |  | 
|  | std::vector<std::u16string> combined_suggestions; | 
|  | spellcheck::FillSuggestions(per_language_suggestions, &combined_suggestions); | 
|  | // suggestions_ will now include those from both the platform spellchecker and | 
|  | // Hunspell. | 
|  | suggestions_ = combined_suggestions; | 
|  |  | 
|  | // The menu can be updated with local suggestions without waiting for the | 
|  | // request for remote suggestions to complete. | 
|  | if (suggestions_.empty() && !use_remote_suggestions_) | 
|  | proxy_->RemoveSeparatorBeforeMenuItem(IDC_SPELLCHECK_SUGGESTION_0); | 
|  |  | 
|  | // Update spell check suggestions displayed on the menu. | 
|  | int length = | 
|  | std::min(kMaxSpellingSuggestions, static_cast<int>(suggestions_.size())); | 
|  |  | 
|  | for (int i = 0; i < length && IDC_SPELLCHECK_SUGGESTION_0 + i <= | 
|  | IDC_SPELLCHECK_SUGGESTION_LAST; | 
|  | ++i) { | 
|  | proxy_->UpdateMenuItem(IDC_SPELLCHECK_SUGGESTION_0 + i, | 
|  | /*enabled=*/true, /*hidden=*/false, suggestions_[i]); | 
|  | } | 
|  |  | 
|  | for (int i = suggestions_.size(); i < kMaxSpellingSuggestions; ++i) { | 
|  | // There were fewer suggestions returned than placeholder slots, remove the | 
|  | // empty menu items. | 
|  | proxy_->RemoveMenuItem(IDC_SPELLCHECK_SUGGESTION_0 + i); | 
|  | } | 
|  |  | 
|  | if (suggestions_.empty()) { | 
|  | proxy_->RemoveSeparatorBeforeMenuItem(IDC_SPELLCHECK_ADD_TO_DICTIONARY); | 
|  | } else { | 
|  | // |spellcheck_service| can be null when the suggested word is | 
|  | // provided by Web SpellCheck API. | 
|  | SpellcheckService* spellcheck_service = | 
|  | SpellcheckServiceFactory::GetForContext(proxy_->GetBrowserContext()); | 
|  | if (spellcheck_service && spellcheck_service->GetMetrics()) | 
|  | spellcheck_service->GetMetrics()->RecordSuggestionStats(1); | 
|  | } | 
|  |  | 
|  | completion_barrier_.Run(); | 
|  | } | 
|  |  | 
|  | void SpellingMenuObserver::OnGetSuggestionsComplete() { | 
|  | animation_timer_.Stop(); | 
|  |  | 
|  | if (use_remote_suggestions_) { | 
|  | // Update remote suggestion too using cached values from | 
|  | // OnGetRemoteSuggestionsComplete. | 
|  | UpdateRemoteSuggestion(remote_service_type_, succeeded_, remote_results_); | 
|  | } | 
|  |  | 
|  | FireSuggestionsCompleteCallbackIfNeededForTesting(); | 
|  | } | 
|  |  | 
|  | void SpellingMenuObserver::RegisterSuggestionsCompleteCallbackForTesting( | 
|  | base::OnceClosure callback) { | 
|  | suggestions_complete_callback_for_testing_ = std::move(callback); | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) | 
|  |  | 
|  | void SpellingMenuObserver::GetRemoteSuggestions() { | 
|  | // The service types |SpellingServiceClient::SPELLCHECK| and | 
|  | // |SpellingServiceClient::SUGGEST| are mutually exclusive. Only one is | 
|  | // available at a time. | 
|  | // | 
|  | // When |SpellingServiceClient::SPELLCHECK| is available, the contextual | 
|  | // suggestions from |SpellingServiceClient| are already stored in | 
|  | // |params.dictionary_suggestions|.  |SpellingMenuObserver| places these | 
|  | // suggestions in the slots |IDC_SPELLCHECK_SUGGESTION_[0-LAST]|. If | 
|  | // |SpellingMenuObserver| queried |SpellingServiceClient| again, then | 
|  | // quality of suggestions would be reduced by lack of context around the | 
|  | // misspelled word. | 
|  | // | 
|  | // When |SpellingServiceClient::SUGGEST| is available, | 
|  | // |params.dictionary_suggestions| contains suggestions only from Hunspell | 
|  | // dictionary. |SpellingMenuObserver| queries |SpellingServiceClient| with | 
|  | // the misspelled word without the surrounding context. Spellcheck | 
|  | // suggestions from |SpellingServiceClient::SUGGEST| are not available until | 
|  | // |SpellingServiceClient| responds to the query. While | 
|  | // |SpellingMenuObserver| waits for |SpellingServiceClient|, it shows a | 
|  | // placeholder text "Loading suggestion..." in the | 
|  | // |IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION| slot. After | 
|  | // |SpellingServiceClient| responds to the query, |SpellingMenuObserver| | 
|  | // replaces the placeholder text with either the spelling suggestion or the | 
|  | // message "No more suggestions from Google." The "No more suggestions" | 
|  | // message is there when |SpellingServiceClient| returned the same | 
|  | // suggestion as Hunspell. | 
|  | // | 
|  | // Append a placeholder item for the suggestion from the Spelling service | 
|  | // and send a request to the service if we can retrieve suggestions from it. | 
|  | // Also, see if we can use the spelling service to get an ideal suggestion. | 
|  | // Otherwise, we'll fall back to the set of suggestions.  Initialize | 
|  | // variables used in OnTextCheckComplete(). We copy the input text to the | 
|  | // result text so we can replace its misspelled regions with suggestions. | 
|  | succeeded_ = false; | 
|  | result_ = misspelled_word_; | 
|  |  | 
|  | // Add a placeholder item. This item will be updated when we receive a | 
|  | // response from the Spelling service. (We do not have to disable this | 
|  | // item now since Chrome will call IsCommandIdEnabled() and disable it.) | 
|  | loading_message_ = | 
|  | l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_CHECKING); | 
|  | proxy_->AddMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, | 
|  | loading_message_); | 
|  |  | 
|  | // Invoke a JSON-RPC call to the Spelling service in the background so we | 
|  | // can update the placeholder item when we receive its response. It also | 
|  | // starts the animation timer so we can show animation until we receive | 
|  | // it. | 
|  | content::BrowserContext* browser_context = proxy_->GetBrowserContext(); | 
|  | if (!browser_context) | 
|  | return; | 
|  | bool result = client_->RequestTextCheck( | 
|  | browser_context, SpellingServiceClient::SUGGEST, misspelled_word_, | 
|  | base::BindOnce(&SpellingMenuObserver::OnGetRemoteSuggestionsComplete, | 
|  | weak_ptr_factory_.GetWeakPtr(), | 
|  | SpellingServiceClient::SUGGEST)); | 
|  | if (result) { | 
|  | loading_frame_ = 0; | 
|  | animation_timer_.Start( | 
|  | FROM_HERE, base::Seconds(1), | 
|  | base::BindRepeating(&SpellingMenuObserver::OnAnimationTimerExpired, | 
|  | weak_ptr_factory_.GetWeakPtr(), | 
|  | IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION)); | 
|  | } | 
|  | } | 
|  |  | 
|  | void SpellingMenuObserver::OnGetRemoteSuggestionsComplete( | 
|  | SpellingServiceClient::ServiceType type, | 
|  | bool success, | 
|  | const std::u16string& /*text*/, | 
|  | const std::vector<SpellCheckResult>& results) { | 
|  | #if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) | 
|  | if (use_platform_suggestions_) { | 
|  | // Cache results since we need the parallel retrieval of local suggestions | 
|  | // to also complete in order to proceed. | 
|  | remote_service_type_ = type; | 
|  | succeeded_ = success; | 
|  | // Parameter |text| is unused. | 
|  | remote_results_ = results; | 
|  |  | 
|  | completion_barrier_.Run(); | 
|  | } else { | 
|  | #endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) | 
|  | animation_timer_.Stop(); | 
|  | UpdateRemoteSuggestion(type, success, results); | 
|  | #if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) | 
|  | } | 
|  |  | 
|  | void SpellingMenuObserver::UpdateRemoteSuggestion( | 
|  | SpellingServiceClient::ServiceType type, | 
|  | bool success, | 
|  | const std::vector<SpellCheckResult>& results) { | 
|  | // Scan the text-check results and replace the misspelled regions with | 
|  | // suggested words. If the replaced text is included in the suggestion list | 
|  | // provided by the local spellchecker, we show a "No suggestions from Google" | 
|  | // message. | 
|  | succeeded_ = success; | 
|  | if (results.empty()) { | 
|  | succeeded_ = false; | 
|  | } else { | 
|  | for (auto it = results.begin(); it != results.end(); ++it) { | 
|  | // If there's more than one replacement, we can't automatically apply it | 
|  | if (it->replacements.size() == 1) | 
|  | result_.replace(it->location, it->length, it->replacements[0]); | 
|  | } | 
|  | std::u16string result = base::i18n::ToLower(result_); | 
|  | for (std::vector<std::u16string>::const_iterator it = suggestions_.begin(); | 
|  | it != suggestions_.end(); ++it) { | 
|  | if (result == base::i18n::ToLower(*it)) { | 
|  | succeeded_ = false; | 
|  | break; | 
|  | } | 
|  | } | 
|  | } | 
|  | if (type != SpellingServiceClient::SPELLCHECK) { | 
|  | if (!succeeded_) { | 
|  | result_ = l10n_util::GetStringUTF16( | 
|  | IDS_CONTENT_CONTEXT_SPELLING_NO_SUGGESTIONS_FROM_GOOGLE); | 
|  | } | 
|  |  | 
|  | // Update the menu item with the result text. We disable this item and hide | 
|  | // it when the spelling service does not provide valid suggestions. | 
|  | proxy_->UpdateMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, succeeded_, | 
|  | false, result_); | 
|  | } | 
|  | } | 
|  |  | 
|  | #if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) | 
|  | void SpellingMenuObserver::FireSuggestionsCompleteCallbackIfNeededForTesting() { | 
|  | if (suggestions_complete_callback_for_testing_) | 
|  | std::move(suggestions_complete_callback_for_testing_).Run(); | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) | 
|  |  | 
|  | void SpellingMenuObserver::OnAnimationTimerExpired(int command_id) { | 
|  | // Append '.' characters to the end of "Checking". | 
|  | loading_frame_ = (loading_frame_ + 1) & 3; | 
|  | std::u16string loading_message = | 
|  | loading_message_ + std::u16string(loading_frame_, '.'); | 
|  |  | 
|  | // Update the menu item with the text. We disable this item to prevent users | 
|  | // from selecting it. | 
|  | proxy_->UpdateMenuItem(command_id, | 
|  | /*enabled=*/false, /*hidden=*/false, loading_message); | 
|  | } |