blob: 588b86545f46e44a53620a4d3632654d801f96e4 [file] [log] [blame]
// 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/command_line.h"
#include "base/functional/bind.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() = default;
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_, 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()) {
// We use the web contents' primary main frame here rather than getting
// the view from the local render frame host because we want to ensure
// that it is non-null (proxy_->GetRenderFrameHost() can return nullptr if
// the frame goes away). In these cases, the spelling preference changes
// are still valid (tied to the BrowsingContext / WebContents) so we still
// want to show the confirmation bubble.
content::RenderFrameHost* rfh =
proxy_->GetWebContents()->GetPrimaryMainFrame();
gfx::Rect rect = rfh->GetRenderWidgetHost()->GetView()->GetViewBounds();
std::unique_ptr<SpellingBubbleModel> model(
new SpellingBubbleModel(profile, proxy_->GetWebContents()));
chrome::ShowConfirmBubble(
proxy_->GetWebContents()->GetTopLevelNativeWindow(),
rfh->GetRenderWidgetHost()->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);
}