root/chrome/browser/renderer_context_menu/spelling_menu_observer.cc

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. client_
  2. InitMenu
  3. IsCommandIdSupported
  4. IsCommandIdChecked
  5. IsCommandIdEnabled
  6. ExecuteCommand
  7. OnMenuCancel
  8. OnTextCheckComplete
  9. OnAnimationTimerExpired

// Copyright 2014 The Chromium Authors. All rights reserved.
// 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 "base/bind.h"
#include "base/command_line.h"
#include "base/i18n/case_conversion.h"
#include "base/prefs/pref_service.h"
#include "base/strings/utf_string_conversions.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_host_metrics.h"
#include "chrome/browser/spellchecker/spellcheck_platform_mac.h"
#include "chrome/browser/spellchecker/spellcheck_service.h"
#include "chrome/browser/spellchecker/spelling_service_client.h"
#include "chrome/browser/ui/confirm_bubble.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/spellcheck_result.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_view.h"
#include "content/public/common/context_menu_params.h"
#include "extensions/browser/view_type_utils.h"
#include "grit/generated_resources.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/rect.h"

using content::BrowserThread;

SpellingMenuObserver::SpellingMenuObserver(RenderViewContextMenuProxy* proxy)
    : proxy_(proxy),
      loading_frame_(0),
      succeeded_(false),
      misspelling_hash_(0),
      client_(new SpellingServiceClient) {
  if (proxy && proxy->GetProfile()) {
    integrate_spelling_service_.Init(prefs::kSpellCheckUseSpellingService,
                                     proxy->GetProfile()->GetPrefs());
    autocorrect_spelling_.Init(prefs::kEnableAutoSpellCorrect,
                               proxy->GetProfile()->GetPrefs());
  }
}

SpellingMenuObserver::~SpellingMenuObserver() {
}

void SpellingMenuObserver::InitMenu(const content::ContextMenuParams& params) {
  DCHECK(BrowserThread::CurrentlyOn(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.
  Profile* profile = proxy_->GetProfile();
  if (!params.is_editable || !profile)
    return;

  // Exit if there is no misspelled word.
  if (params.misspelled_word.empty())
    return;

  suggestions_ = params.dictionary_suggestions;
  misspelled_word_ = params.misspelled_word;
  misspelling_hash_ = params.misspelling_hash;

  bool use_suggestions = SpellingServiceClient::IsAvailable(
      profile, SpellingServiceClient::SUGGEST);

  if (!suggestions_.empty() || use_suggestions)
    proxy_->AddSeparator();

  // Append Dictionary spell check suggestions.
  for (size_t i = 0; i < params.dictionary_suggestions.size() &&
       IDC_SPELLCHECK_SUGGESTION_0 + i <= IDC_SPELLCHECK_SUGGESTION_LAST;
       ++i) {
    proxy_->AddMenuItem(IDC_SPELLCHECK_SUGGESTION_0 + static_cast<int>(i),
                        params.dictionary_suggestions[i]);
  }

  // The service types |SpellingServiceClient::SPELLCHECK| and
  // |SpellingServiceClient::SUGGEST| are mutually exclusive. Only one is
  // available at at 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.
  if (use_suggestions) {
    // 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_ = params.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.
    bool result = client_->RequestTextCheck(
        profile, SpellingServiceClient::SUGGEST, params.misspelled_word,
        base::Bind(&SpellingMenuObserver::OnTextCheckComplete,
                   base::Unretained(this), SpellingServiceClient::SUGGEST));
    if (result) {
      loading_frame_ = 0;
      animation_timer_.Start(FROM_HERE, base::TimeDelta::FromSeconds(1),
          this, &SpellingMenuObserver::OnAnimationTimerExpired);
    }
  }

  if (params.dictionary_suggestions.empty()) {
    proxy_->AddMenuItem(
        IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS,
        l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS));
    bool use_spelling_service = SpellingServiceClient::IsAvailable(
        profile, SpellingServiceClient::SPELLCHECK);
    if (use_suggestions || use_spelling_service)
      proxy_->AddSeparator();
  } else {
    proxy_->AddSeparator();

    // |spellcheck_service| can be null when the suggested word is
    // provided by Web SpellCheck API.
    SpellcheckService* spellcheck_service =
        SpellcheckServiceFactory::GetForContext(profile);
    if (spellcheck_service && spellcheck_service->GetMetrics())
      spellcheck_service->GetMetrics()->RecordSuggestionStats(1);
  }

  // If word is misspelled, give option for "Add to dictionary" and a check item
  // "Ask Google for suggestions".
  proxy_->AddMenuItem(IDC_SPELLCHECK_ADD_TO_DICTIONARY,
      l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_ADD_TO_DICTIONARY));

#if defined(TOOLKIT_GTK)
  extensions::ViewType view_type =
      extensions::GetViewType(proxy_->GetWebContents());
  if (view_type != extensions::VIEW_TYPE_PANEL) {
#endif
    proxy_->AddCheckItem(IDC_CONTENT_CONTEXT_SPELLING_TOGGLE,
        l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_ASK_GOOGLE));
#if defined(TOOLKIT_GTK)
  }
#endif

  const CommandLine* command_line = CommandLine::ForCurrentProcess();
  if (command_line->HasSwitch(switches::kEnableSpellingAutoCorrect)) {
    proxy_->AddCheckItem(IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE,
        l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPELLING_AUTOCORRECT));
  }

  proxy_->AddSeparator();
}

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:
    case IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE:
      return true;

    default:
      return false;
  }
}

bool SpellingMenuObserver::IsCommandIdChecked(int command_id) {
  DCHECK(IsCommandIdSupported(command_id));

  if (command_id == IDC_CONTENT_CONTEXT_SPELLING_TOGGLE)
    return integrate_spelling_service_.GetValue() &&
        !proxy_->GetProfile()->IsOffTheRecord();
  if (command_id == IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE)
    return autocorrect_spelling_.GetValue() &&
        !proxy_->GetProfile()->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;

  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() &&
          !proxy_->GetProfile()->IsOffTheRecord();

    case IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE:
      return integrate_spelling_service_.IsUserModifiable() &&
          !proxy_->GetProfile()->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.
    Profile* profile = proxy_->GetProfile();
    if (profile) {
      SpellcheckService* spellcheck =
          SpellcheckServiceFactory::GetForContext(profile);
      if (spellcheck) {
        if (spellcheck->GetMetrics())
          spellcheck->GetMetrics()->RecordReplacedWordStats(1);
        spellcheck->GetFeedbackSender()->SelectedSuggestion(
            misspelling_hash_, suggestion_index);
      }
    }
    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.
    Profile* profile = proxy_->GetProfile();
    if (profile) {
      SpellcheckService* spellcheck =
          SpellcheckServiceFactory::GetForContext(profile);
      if (spellcheck) {
        spellcheck->GetCustomDictionary()->AddWord(base::UTF16ToUTF8(
            misspelled_word_));
        spellcheck->GetFeedbackSender()->AddedToDictionary(misspelling_hash_);
      }
    }
#if defined(OS_MACOSX)
    spellcheck_mac::AddWord(misspelled_word_);
#endif
  }

  // 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()) {
    // When a user enables the "Ask Google for spelling suggestions" item, 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 (!integrate_spelling_service_.GetValue()) {
      content::RenderViewHost* rvh = proxy_->GetRenderViewHost();
      gfx::Rect rect = rvh->GetView()->GetViewBounds();
      chrome::ShowConfirmBubble(
#if defined(TOOLKIT_VIEWS)
          proxy_->GetWebContents()->GetView()->GetTopLevelNativeWindow(),
#else
          rvh->GetView()->GetNativeView(),
#endif
          gfx::Point(rect.CenterPoint().x(), rect.y()),
          new SpellingBubbleModel(proxy_->GetProfile(),
                                  proxy_->GetWebContents(),
                                  false));
    } else {
      Profile* profile = proxy_->GetProfile();
      if (profile)
        profile->GetPrefs()->SetBoolean(prefs::kSpellCheckUseSpellingService,
                                        false);
        profile->GetPrefs()->SetBoolean(prefs::kEnableAutoSpellCorrect,
                                        false);
    }
  }
  // Autocorrect requires use of the spelling service and the spelling service
  // can be toggled by the user only if it is not managed.
  if (command_id == IDC_CONTENT_CONTEXT_AUTOCORRECT_SPELLING_TOGGLE &&
      integrate_spelling_service_.IsUserModifiable()) {
    // When the user enables autocorrect, we'll need to make sure that we can
    // ask Google for suggestions since that service is required. So we show
    // the bubble and just make sure to enable autocorrect as well.
    if (!integrate_spelling_service_.GetValue()) {
      content::RenderViewHost* rvh = proxy_->GetRenderViewHost();
      gfx::Rect rect = rvh->GetView()->GetViewBounds();
      chrome::ShowConfirmBubble(rvh->GetView()->GetNativeView(),
                                gfx::Point(rect.CenterPoint().x(), rect.y()),
                                new SpellingBubbleModel(
                                    proxy_->GetProfile(),
                                    proxy_->GetWebContents(),
                                    true));
    } else {
      Profile* profile = proxy_->GetProfile();
      if (profile) {
        bool current_value = autocorrect_spelling_.GetValue();
        profile->GetPrefs()->SetBoolean(prefs::kEnableAutoSpellCorrect,
                                        !current_value);
      }
    }
  }
}

void SpellingMenuObserver::OnMenuCancel() {
  Profile* profile = proxy_->GetProfile();
  if (!profile)
    return;
  SpellcheckService* spellcheck =
      SpellcheckServiceFactory::GetForContext(profile);
  if (!spellcheck)
    return;
  spellcheck->GetFeedbackSender()->IgnoredSuggestions(misspelling_hash_);
}

void SpellingMenuObserver::OnTextCheckComplete(
    SpellingServiceClient::ServiceType type,
    bool success,
    const base::string16& text,
    const std::vector<SpellCheckResult>& results) {
  animation_timer_.Stop();

  // 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 {
    typedef std::vector<SpellCheckResult> SpellCheckResults;
    for (SpellCheckResults::const_iterator it = results.begin();
         it != results.end(); ++it) {
      result_.replace(it->location, it->length, it->replacement);
    }
    base::string16 result = base::i18n::ToLower(result_);
    for (std::vector<base::string16>::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_);
  }
}

void SpellingMenuObserver::OnAnimationTimerExpired() {
  // Append '.' characters to the end of "Checking".
  loading_frame_ = (loading_frame_ + 1) & 3;
  base::string16 loading_message =
      loading_message_ + base::string16(loading_frame_,'.');

  // Update the menu item with the text. We disable this item to prevent users
  // from selecting it.
  proxy_->UpdateMenuItem(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, false, false,
                         loading_message);
}

/* [<][>][^][v][top][bottom][index][help] */